From 15d1c5beab03a3480ba412a38f961d5030c74e64 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 11 Jan 2024 13:25:43 +0400 Subject: [PATCH 01/82] Documentation --- src/AuctionHouse.sol | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 9a5126bb..05b5677a 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -52,11 +52,20 @@ abstract contract Router is FeeManager { // ========== ATOMIC AUCTIONS ========== // - /// @param approval_ - (Optional) Permit approval signature for the quoteToken + /// @notice Purchase a lot from an auction + /// + /// @param recipient_ Address to receive payout + /// @param referrer_ Address of referrer + /// @param lotId_ Lot ID + /// @param amount_ Amount of quoteToken to purchase with (in native decimals) + /// @param minAmountOut_ Minimum amount of baseToken to receive + /// @param auctionData_ Custom data used by the auction module + /// @param approval_ Permit approval signature for the quoteToken + /// @return payout Amount of baseToken received by `recipient_` (in native decimals) function purchase( address recipient_, address referrer_, - uint256 id_, + uint256 lotId_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, @@ -107,7 +116,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== AUCTION FUNCTIONS ========== // - function calculateFees( + function _calculateFees( address referrer_, uint256 amount_ ) internal view returns (uint256 toReferrer, uint256 toProtocol) { @@ -124,12 +133,12 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { return (toReferrer, toProtocol); } - function allocateFees( + function _allocateFees( address referrer_, ERC20 quoteToken_, uint256 amount_ ) internal returns (uint256 totalFees) { - (uint256 toReferrer, uint256 toProtocol) = calculateFees(referrer_, amount_); + (uint256 toReferrer, uint256 toProtocol) = _calculateFees(referrer_, amount_); // Update fee balances if non-zero if (referrerFees[referrer_] > 0) { @@ -142,10 +151,13 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { return toReferrer + toProtocol; } + // ========== ATOMIC AUCTIONS ========== // + + /// @inheritdoc Router function purchase( address recipient_, address referrer_, - uint256 id_, + uint256 lotId_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, @@ -155,15 +167,15 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Response: No, my thought was that the module will just revert on `purchase` if it's not atomic. Vice versa // Load routing data for the lot - Routing memory routing = lotRouting[id_]; + Routing memory routing = lotRouting[lotId_]; - uint256 totalFees = allocateFees(referrer_, routing.quoteToken, amount_); + uint256 totalFees = _allocateFees(referrer_, routing.quoteToken, amount_); // Send purchase to auction house and get payout plus any extra output bytes memory auctionOutput; { - AuctionModule module = _getModuleForId(id_); - (payout, auctionOutput) = module.purchase(id_, amount_ - totalFees, auctionData_); + AuctionModule module = _getModuleForId(lotId_); + (payout, auctionOutput) = module.purchase(lotId_, amount_ - totalFees, auctionData_); } // Check that payout is at least minimum amount out @@ -171,15 +183,17 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { if (payout < minAmountOut_) revert AmountLessThanMinimum(); // Handle transfers from purchaser and seller - _handleTransfers(id_, routing, amount_, payout, totalFees, approval_); + _handleTransfers(lotId_, routing, amount_, payout, totalFees, approval_); // Handle payout to user, including creation of derivative tokens _handlePayout(routing, recipient_, payout, auctionOutput); // Emit event - emit Purchase(id_, msg.sender, referrer_, amount_, payout); + emit Purchase(lotId_, msg.sender, referrer_, amount_, payout); } + // ========== BATCH AUCTIONS ========== // + function bid( address recipient_, address referrer_, From b3805d0b2998f49688fa633be47a0330981ee4de Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 11 Jan 2024 13:48:26 +0400 Subject: [PATCH 02/82] Document purchase() --- src/AuctionHouse.sol | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 05b5677a..b1360d77 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -154,6 +154,21 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== ATOMIC AUCTIONS ========== // /// @inheritdoc Router + /// @dev This fuction handles the following: + /// 1. Calculates the fees for the purchase + /// 2. Sends the purchase amount to the auction module + /// 3. Records the purchase on the auction module + /// 4. Transfers the quote token from the caller + /// 5. Transfers the quote token to the auction owner or executes the callback + /// 6. Transfers the payout token to the recipient + /// + /// This function reverts if: + /// - The respective auction module reverts + /// - `payout` is less than `minAmountOut_` + /// - The caller does not have sufficient balance of the quote token + /// - The auction owner does not have sufficient balance of the payout token + /// - Any of the callbacks fail + /// - Any of the token transfers fail function purchase( address recipient_, address referrer_, From 7d56dfff57049103274c289daabf9017d0e7763d Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 11 Jan 2024 14:27:41 +0400 Subject: [PATCH 03/82] Modifier for valid lot --- src/AuctionHouse.sol | 3 +++ src/bases/Auctioneer.sol | 27 ++++++++++++++++----------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index b1360d77..28a0eeaa 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -120,6 +120,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { address referrer_, uint256 amount_ ) internal view returns (uint256 toReferrer, uint256 toProtocol) { + // TODO shift into FeeManager? + // Calculate fees for purchase // 1. Calculate referrer fee // 2. Calculate protocol fee as the total expected fee amount minus the referrer fee @@ -163,6 +165,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// 6. Transfers the payout token to the recipient /// /// This function reverts if: + /// - `lotId_` is invalid /// - The respective auction module reverts /// - `payout` is less than `minAmountOut_` /// - The caller does not have sufficient balance of the quote token diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index 16bf873b..91b4059e 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -83,6 +83,19 @@ abstract contract Auctioneer is WithModules { mapping(Veecode auctionRef => mapping(Veecode derivativeRef => Veecode condenserRef)) public condensers; + // ========= MODIFIERS ========= // + + /// @notice Checks that the lot ID is valid + /// @dev Reverts if the lot ID is invalid + /// + /// @param lotId_ ID of the auction lot + modifier isValidLot(uint256 lotId_) { + if (lotId_ >= lotCounter) revert InvalidLotId(lotId_); + + if (lotRouting[lotId_].owner == address(0)) revert InvalidLotId(lotId_); + _; + } + // ========== AUCTION MANAGEMENT ========== // /// @notice Creates a new auction lot @@ -228,14 +241,9 @@ abstract contract Auctioneer is WithModules { /// - The respective auction module reverts /// /// @param lotId_ ID of the auction lot - function cancel(uint256 lotId_) external { - address lotOwner = lotRouting[lotId_].owner; - - // Check that lot ID is valid - if (lotOwner == address(0)) revert InvalidLotId(lotId_); - + function cancel(uint256 lotId_) external isValidLot(lotId_) { // Check that caller is the auction owner - if (msg.sender != lotOwner) revert NotAuctionOwner(msg.sender); + if (msg.sender != lotRouting[lotId_].owner) revert NotAuctionOwner(msg.sender); AuctionModule module = _getModuleForId(lotId_); @@ -251,10 +259,7 @@ 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 returns (Routing memory) { - // Check that lot ID is valid - if (id_ >= lotCounter) revert InvalidLotId(id_); - + function getRouting(uint256 id_) external view isValidLot(id_) returns (Routing memory) { // Get routing from lot routing return lotRouting[id_]; } From d91ad15797711a76b83b47e23916246cfdbcbb58 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 11 Jan 2024 14:53:26 +0400 Subject: [PATCH 04/82] Use modifier --- src/AuctionHouse.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 28a0eeaa..ab5c830a 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -180,7 +180,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_ - ) external override returns (uint256 payout) { + ) external override isValidLot(lotId_) returns (uint256 payout) { // TODO should this not check if the auction is atomic? // Response: No, my thought was that the module will just revert on `purchase` if it's not atomic. Vice versa From e2cb3ba3ef612361bf1c8b6206c0b8fbee112204 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 11 Jan 2024 14:53:34 +0400 Subject: [PATCH 05/82] WIP tests for atomic purchase --- test/AuctionHouse/purchase.t.sol | 160 ++++++++++++++++++ .../Auction/MockAtomicAuctionModule.sol | 74 ++++++++ .../Auction/MockBatchAuctionModule.sol | 65 +++++++ 3 files changed, 299 insertions(+) create mode 100644 test/AuctionHouse/purchase.t.sol create mode 100644 test/modules/Auction/MockAtomicAuctionModule.sol create mode 100644 test/modules/Auction/MockBatchAuctionModule.sol diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol new file mode 100644 index 00000000..436c0d55 --- /dev/null +++ b/test/AuctionHouse/purchase.t.sol @@ -0,0 +1,160 @@ +// 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 {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule.sol"; +import {MockCondenserModule} from "test/modules/Condenser/MockCondenserModule.sol"; +import {MockAllowlist} from "test/modules/Auction/MockAllowlist.sol"; +import {MockHook} from "test/modules/Auction/MockHook.sol"; + +// Auctions +import {AuctionHouse} 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, + fromVeecode, + WithModules, + Module +} from "src/modules/Modules.sol"; + +contract PurchaseTest is Test { + MockERC20 internal baseToken; + MockERC20 internal quoteToken; + MockAtomicAuctionModule internal mockAuctionModule; + MockDerivativeModule internal mockDerivativeModule; + MockCondenserModule internal mockCondenserModule; + MockAllowlist internal mockAllowlist; + MockHook internal mockHook; + + AuctionHouse internal auctionHouse; + Auctioneer.RoutingParams internal routingParams; + Auction.AuctionParams internal auctionParams; + + address internal immutable protocol = address(0x2); + + uint256 internal lotId; + + function setUp() external { + baseToken = new MockERC20("Base Token", "BASE", 18); + quoteToken = new MockERC20("Quote Token", "QUOTE", 18); + + auctionHouse = new AuctionHouse(protocol); + mockAuctionModule = new MockAtomicAuctionModule(address(auctionHouse)); + mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); + mockCondenserModule = new MockCondenserModule(address(auctionHouse)); + mockAllowlist = new MockAllowlist(); + mockHook = new MockHook(); + + auctionParams = Auction.AuctionParams({ + start: uint48(block.timestamp), + duration: uint48(1 days), + capacityInQuote: false, + capacity: 10e18, + implParams: abi.encode("") + }); + + routingParams = Auctioneer.RoutingParams({ + auctionType: toKeycode("ATOM"), + baseToken: baseToken, + quoteToken: quoteToken, + hooks: IHooks(address(0)), + allowlist: IAllowlist(address(0)), + allowlistParams: abi.encode(""), + payoutData: abi.encode(""), + derivativeType: toKeycode(""), + derivativeParams: abi.encode("") + }); + + // Install the auction module + auctionHouse.installModule(mockAuctionModule); + + // Create an auction + lotId = auctionHouse.auction(routingParams, auctionParams); + } + + modifier whenDerivativeModuleIsInstalled() { + auctionHouse.installModule(mockDerivativeModule); + _; + } + + modifier whenDerivativeTypeIsSet() { + routingParams.derivativeType = toKeycode("DERV"); + _; + } + + modifier whenCondenserModuleIsInstalled() { + auctionHouse.installModule(mockCondenserModule); + _; + } + + modifier whenCondenserIsMapped() { + auctionHouse.setCondenser( + mockAuctionModule.VEECODE(), + mockDerivativeModule.VEECODE(), + mockCondenserModule.VEECODE() + ); + _; + } + + modifier whenBatchAuctionIsCreated() { + MockBatchAuctionModule mockBatchAuctionModule = + new MockBatchAuctionModule(address(auctionHouse)); + + // Install the batch auction module + auctionHouse.installModule(mockBatchAuctionModule); + + // Modify the routing params to create a batch auction + routingParams.auctionType = toKeycode("BATCH"); + + // Create the batch auction + lotId = auctionHouse.auction(routingParams, auctionParams); + _; + } + + // purchase + // [ ] reverts if the lot id is invalid + // [ ] reverts if the auction is not atomic + // [ ] reverts if the auction is not active + // [ ] reverts if the auction module reverts + // [ ] reverts if the payout amount is less than the minimum + // [ ] reverts if the caller does not have sufficient balance of the quote token + // [ ] reverts if the caller has not approved the Permit2 contract + // [ ] reverts if the auction owner does not have sufficient balance of the payout token + // [ ] reverts if there is a callback that fails + // [ ] reverts if the Permit2 approval is invalid + // [ ] allowlist + // [ ] reverts if the caller is not on the allowlist + // [ ] derivative + // [ ] mints derivative tokens to the recipient + // [ ] if specified, uses the condenser + // [ ] non-derivative + // [ ] transfers the base token to the recipient + // [ ] fees + // [ ] protocol fees recorded + // [ ] referrer fees recorded + // [ ] hooks + // [ ] reverts if pre-purchase hook reverts + // [ ] reverts if mid-purchase hook reverts + // [ ] reverts if post-purchase hook reverts + // [ ] performs pre-purchase hook + // [ ] performs pre-purchase hook with fees + // [ ] performs mid-purchase hook + // [ ] performs mid-purchase hook with fees + // [ ] performs post-purchase hook + // [ ] performs post-purchase hook with fees + // [ ] non-hooks + // [ ] success - transfers the quote token to the auction owner +} diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol new file mode 100644 index 00000000..dee1addf --- /dev/null +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Modules +import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; + +// Auctions +import {AuctionModule} from "src/modules/Auction.sol"; + +contract MockAtomicAuctionModule is AuctionModule { + mapping(uint256 => uint256) public payoutData; + + constructor(address _owner) AuctionModule(_owner) { + minAuctionDuration = 1 days; + } + + function VEECODE() public pure virtual override returns (Veecode) { + return wrapVeecode(toKeycode("ATOM"), 1); + } + + function TYPE() public pure virtual override returns (Type) { + return Type.Auction; + } + + function _auction( + uint256, + Lot memory, + bytes memory + ) internal virtual override returns (uint256) { + return 0; + } + + function _cancel(uint256 id_) internal override { + // + } + + function purchase( + uint256 id_, + uint256 amount_, + bytes calldata auctionData_ + ) external virtual override returns (uint256 payout, bytes memory auctionOutput) { + payout = payoutData[id_] * amount_; + auctionOutput = auctionData_; + } + + function setPayoutMultiplier(uint256 id_, uint256 multiplier_) external virtual { + payoutData[id_] = multiplier_; + } + + function bid(uint256, uint256, uint256, bytes calldata) external virtual override { + revert("unsupported"); + } + + function settle(uint256 id_) external virtual override returns (uint256[] memory amountsOut) {} + + function settle( + uint256 id_, + Bid[] memory bids_ + ) external virtual override returns (uint256[] memory amountsOut) {} + + 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) {} +} diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol new file mode 100644 index 00000000..c95bcb77 --- /dev/null +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Modules +import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; + +// Auctions +import {AuctionModule} from "src/modules/Auction.sol"; + +contract MockBatchAuctionModule is AuctionModule { + constructor(address _owner) AuctionModule(_owner) { + minAuctionDuration = 1 days; + } + + function VEECODE() public pure virtual override returns (Veecode) { + return wrapVeecode(toKeycode("BATCH"), 1); + } + + function TYPE() public pure virtual override returns (Type) { + return Type.Auction; + } + + function _auction( + uint256, + Lot memory, + bytes memory + ) internal virtual override returns (uint256) { + return 0; + } + + function _cancel(uint256 id_) internal override { + // + } + + function purchase( + uint256, + uint256, + bytes calldata + ) external virtual override returns (uint256, bytes memory) { + revert("unsupported"); + } + + function bid(uint256, uint256, uint256, bytes calldata) external virtual override {} + + function settle(uint256 id_) external virtual override returns (uint256[] memory amountsOut) {} + + function settle( + uint256 id_, + Bid[] memory bids_ + ) external virtual override returns (uint256[] memory amountsOut) {} + + 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) {} +} From eaf5ec73146a0a6ab143316fe87ead2aac6db9a3 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 11 Jan 2024 17:14:21 +0400 Subject: [PATCH 06/82] Shift parameters to purchase() into struct. WIP tests. --- src/AuctionHouse.sol | 75 ++++---- test/AuctionHouse/purchase.t.sol | 167 ++++++++++++++++-- .../Auction/MockAtomicAuctionModule.sol | 9 +- .../Auction/MockBatchAuctionModule.sol | 2 +- 4 files changed, 206 insertions(+), 47 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index ab5c830a..55e395c0 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -20,6 +20,27 @@ abstract contract FeeManager { } abstract contract Router is FeeManager { + // ========== 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 lotId Lot ID + /// @param amount Amount of quoteToken to purchase with (in native decimals) + /// @param minAmountOut Minimum amount of baseToken to receive + /// @param auctionData Custom data used by the auction module + /// @param approval Permit approval signature for the quoteToken + struct PurchaseParams { + address recipient; + address referrer; + uint256 lotId; + uint256 amount; + uint256 minAmountOut; + bytes auctionData; + bytes approval; + } + // ========== 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). @@ -54,23 +75,9 @@ abstract contract Router is FeeManager { /// @notice Purchase a lot from an auction /// - /// @param recipient_ Address to receive payout - /// @param referrer_ Address of referrer - /// @param lotId_ Lot ID - /// @param amount_ Amount of quoteToken to purchase with (in native decimals) - /// @param minAmountOut_ Minimum amount of baseToken to receive - /// @param auctionData_ Custom data used by the auction module - /// @param approval_ Permit approval signature for the quoteToken + /// @param params_ Purchase parameters /// @return payout Amount of baseToken received by `recipient_` (in native decimals) - function purchase( - address recipient_, - address referrer_, - uint256 lotId_, - uint256 amount_, - uint256 minAmountOut_, - bytes calldata auctionData_, - bytes calldata approval_ - ) external virtual returns (uint256 payout); + function purchase(PurchaseParams memory params_) external virtual returns (uint256 payout); // ========== BATCH AUCTIONS ========== // @@ -172,42 +179,42 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// - The auction owner does not have sufficient balance of the payout token /// - Any of the callbacks fail /// - Any of the token transfers fail - function purchase( - address recipient_, - address referrer_, - uint256 lotId_, - uint256 amount_, - uint256 minAmountOut_, - bytes calldata auctionData_, - bytes calldata approval_ - ) external override isValidLot(lotId_) returns (uint256 payout) { + function purchase(PurchaseParams memory params_) + external + override + isValidLot(params_.lotId) + returns (uint256 payout) + { // TODO should this not check if the auction is atomic? // Response: No, my thought was that the module will just revert on `purchase` if it's not atomic. Vice versa // Load routing data for the lot - Routing memory routing = lotRouting[lotId_]; + Routing memory routing = lotRouting[params_.lotId]; - uint256 totalFees = _allocateFees(referrer_, routing.quoteToken, amount_); + uint256 totalFees = _allocateFees(params_.referrer, routing.quoteToken, params_.amount); // Send purchase to auction house and get payout plus any extra output bytes memory auctionOutput; { - AuctionModule module = _getModuleForId(lotId_); - (payout, auctionOutput) = module.purchase(lotId_, amount_ - totalFees, auctionData_); + AuctionModule module = _getModuleForId(params_.lotId); + (payout, auctionOutput) = + module.purchase(params_.lotId, params_.amount - totalFees, params_.auctionData); } // Check that payout is at least minimum amount out // @dev Moved the slippage check from the auction to the AuctionHouse to allow different routing and purchase logic - if (payout < minAmountOut_) revert AmountLessThanMinimum(); + if (payout < params_.minAmountOut) revert AmountLessThanMinimum(); // Handle transfers from purchaser and seller - _handleTransfers(lotId_, routing, amount_, payout, totalFees, approval_); + _handleTransfers( + params_.lotId, routing, params_.amount, payout, totalFees, params_.approval + ); // Handle payout to user, including creation of derivative tokens - _handlePayout(routing, recipient_, payout, auctionOutput); + _handlePayout(routing, params_.recipient, payout, auctionOutput); // Emit event - emit Purchase(lotId_, msg.sender, referrer_, amount_, payout); + emit Purchase(params_.lotId, msg.sender, params_.referrer, params_.amount, payout); } // ========== BATCH AUCTIONS ========== // @@ -307,7 +314,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 amount_, uint256 payout_, uint256 feePaid_, - bytes calldata approval_ + bytes memory approval_ ) internal { // Calculate amount net of fees uint256 amountLessFee = amount_ - feePaid_; diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 436c0d55..050dd640 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -15,7 +15,7 @@ import {MockAllowlist} from "test/modules/Auction/MockAllowlist.sol"; import {MockHook} from "test/modules/Auction/MockHook.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"; @@ -42,11 +42,18 @@ contract PurchaseTest is Test { AuctionHouse internal auctionHouse; Auctioneer.RoutingParams internal routingParams; Auction.AuctionParams internal auctionParams; + Router.PurchaseParams internal purchaseParams; address internal immutable protocol = address(0x2); + address internal immutable alice = address(0x3); + address internal immutable referrer = address(0x4); + address internal immutable auctionOwner = address(0x5); uint256 internal lotId; + uint256 internal constant AMOUNT_IN = 1e18; + uint256 internal AMOUNT_OUT; + function setUp() external { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); @@ -82,7 +89,24 @@ contract PurchaseTest is Test { auctionHouse.installModule(mockAuctionModule); // Create an auction + vm.prank(auctionOwner); lotId = auctionHouse.auction(routingParams, auctionParams); + + // Set the default payout multiplier to 1 + mockAuctionModule.setPayoutMultiplier(lotId, 1); + + // 1:1 exchange rate + AMOUNT_OUT = AMOUNT_IN; + + purchaseParams = Router.PurchaseParams({ + recipient: alice, + referrer: referrer, + lotId: lotId, + amount: AMOUNT_IN, + minAmountOut: AMOUNT_OUT, + auctionData: bytes(""), + approval: bytes("") + }); } modifier whenDerivativeModuleIsInstalled() { @@ -120,21 +144,37 @@ contract PurchaseTest is Test { routingParams.auctionType = toKeycode("BATCH"); // Create the batch auction + vm.prank(auctionOwner); lotId = auctionHouse.auction(routingParams, auctionParams); _; } + modifier whenAccountHasQuoteTokenBalance(uint256 amount_) { + quoteToken.mint(alice, amount_); + _; + } + + modifier whenAccountHasBaseTokenBalance(uint256 amount_) { + baseToken.mint(auctionOwner, amount_); + _; + } + + modifier whenAuctionIsCancelled() { + vm.prank(auctionOwner); + auctionHouse.cancel(lotId); + _; + } + // purchase - // [ ] reverts if the lot id is invalid - // [ ] reverts if the auction is not atomic - // [ ] reverts if the auction is not active - // [ ] reverts if the auction module reverts - // [ ] reverts if the payout amount is less than the minimum - // [ ] reverts if the caller does not have sufficient balance of the quote token - // [ ] reverts if the caller has not approved the Permit2 contract - // [ ] reverts if the auction owner does not have sufficient balance of the payout token - // [ ] reverts if there is a callback that fails - // [ ] reverts if the Permit2 approval is invalid + // [X] reverts if the lot id is invalid + // [X] reverts if the auction is not atomic + // [X] reverts if the auction is not active + // [X] reverts if the auction module reverts + // [X] reverts if the payout amount is less than the minimum + // [ ] quote token transfers + // [ ] reverts if the caller does not have sufficient balance of the quote token + // [ ] reverts if the caller has not approved the Permit2 contract + // [ ] reverts if the Permit2 approval is invalid // [ ] allowlist // [ ] reverts if the caller is not on the allowlist // [ ] derivative @@ -156,5 +196,110 @@ contract PurchaseTest is Test { // [ ] performs post-purchase hook // [ ] performs post-purchase hook with fees // [ ] non-hooks + // [ ] reverts if the auction owner does not have sufficient balance of the payout token // [ ] success - transfers the quote token to the auction owner + + function testReverts_whenLotIdIsInvalid() external { + // Update the lot id to an invalid value + purchaseParams.lotId = 1; + + // Expect revert + bytes memory err = + abi.encodeWithSelector(Auctioneer.InvalidLotId.selector, purchaseParams.lotId); + vm.expectRevert(err); + + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + } + + function testReverts_whenNotAtomicAuction() + external + whenBatchAuctionIsCreated + whenAccountHasQuoteTokenBalance(AMOUNT_IN) + whenAccountHasBaseTokenBalance(AMOUNT_OUT) + { + // Update purchase params + purchaseParams.lotId = lotId; + + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_NotImplemented.selector); + vm.expectRevert(err); + + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + } + + function testReverts_whenAuctionNotActive() + external + whenAuctionIsCancelled + whenAccountHasQuoteTokenBalance(AMOUNT_IN) + whenAccountHasBaseTokenBalance(AMOUNT_OUT) + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + } + + function testReverts_whenAuctionModuleReverts() + external + whenAccountHasQuoteTokenBalance(AMOUNT_IN) + whenAccountHasBaseTokenBalance(AMOUNT_OUT) + { + // Set the auction module to revert + mockAuctionModule.setPurchaseReverts(true); + + // Expect revert + vm.expectRevert("error"); + + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + } + + function testReverts_whenPayoutAmountLessThanMinimum() + external + whenAccountHasQuoteTokenBalance(AMOUNT_IN) + whenAccountHasBaseTokenBalance(AMOUNT_OUT) + { + // Set the payout multiplier so that the payout is less than the minimum + mockAuctionModule.setPayoutMultiplier(lotId, 0); + + // Expect revert + bytes memory err = abi.encodeWithSelector(AuctionHouse.AmountLessThanMinimum.selector); + vm.expectRevert(err); + + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + } + + function testReverts_whenCallerHasInsufficientBalanceOfQuoteToken() + external + whenAccountHasBaseTokenBalance(AMOUNT_OUT) + { + // Expect revert + vm.expectRevert(); + + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + } + + function testReverts_whenOwnerHasInsufficientBalanceOfBaseToken() + external + whenAccountHasQuoteTokenBalance(AMOUNT_IN) + { + // Expect revert + vm.expectRevert(); + + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + } } diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index dee1addf..8665ac7b 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -9,6 +9,7 @@ import {AuctionModule} from "src/modules/Auction.sol"; contract MockAtomicAuctionModule is AuctionModule { mapping(uint256 => uint256) public payoutData; + bool public purchaseReverts; constructor(address _owner) AuctionModule(_owner) { minAuctionDuration = 1 days; @@ -39,6 +40,8 @@ contract MockAtomicAuctionModule is AuctionModule { uint256 amount_, bytes calldata auctionData_ ) external virtual override returns (uint256 payout, bytes memory auctionOutput) { + if (purchaseReverts) revert("error"); + payout = payoutData[id_] * amount_; auctionOutput = auctionData_; } @@ -47,8 +50,12 @@ contract MockAtomicAuctionModule is AuctionModule { payoutData[id_] = multiplier_; } + function setPurchaseReverts(bool reverts_) external virtual { + purchaseReverts = reverts_; + } + function bid(uint256, uint256, uint256, bytes calldata) external virtual override { - revert("unsupported"); + revert Auction_NotImplemented(); } 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 c95bcb77..4a6f837e 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -37,7 +37,7 @@ contract MockBatchAuctionModule is AuctionModule { uint256, bytes calldata ) external virtual override returns (uint256, bytes memory) { - revert("unsupported"); + revert Auction_NotImplemented(); } function bid(uint256, uint256, uint256, bytes calldata) external virtual override {} From f30d5ac34a4578b75fbec7c42644eafce3be72c5 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 11 Jan 2024 17:34:59 +0400 Subject: [PATCH 07/82] Comments --- src/AuctionHouse.sol | 1 + test/AuctionHouse/purchase.t.sol | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 55e395c0..aee536a6 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -74,6 +74,7 @@ abstract contract Router is FeeManager { // ========== ATOMIC AUCTIONS ========== // /// @notice Purchase a lot from an auction + /// @notice Permit2 is utilised to simplify token transfers /// /// @param params_ Purchase parameters /// @return payout Amount of baseToken received by `recipient_` (in native decimals) diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 050dd640..b8ff968e 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -165,6 +165,16 @@ contract PurchaseTest is Test { _; } + modifier whenPermit2IsApproved() { + // TODO + _; + } + + modifier whenPermit2ApprovalIsValid() { + // TODO + _; + } + // purchase // [X] reverts if the lot id is invalid // [X] reverts if the auction is not atomic @@ -172,15 +182,16 @@ contract PurchaseTest is Test { // [X] reverts if the auction module reverts // [X] reverts if the payout amount is less than the minimum // [ ] quote token transfers - // [ ] reverts if the caller does not have sufficient balance of the quote token + // [X] reverts if the caller does not have sufficient balance of the quote token // [ ] reverts if the caller has not approved the Permit2 contract // [ ] reverts if the Permit2 approval is invalid // [ ] allowlist // [ ] reverts if the caller is not on the allowlist - // [ ] derivative + // [ ] derivative payout token // [ ] mints derivative tokens to the recipient // [ ] if specified, uses the condenser - // [ ] non-derivative + // [ ] non-derivative payout token + // [X] reverts if the auction owner does not have sufficient balance of the payout token // [ ] transfers the base token to the recipient // [ ] fees // [ ] protocol fees recorded @@ -196,8 +207,8 @@ contract PurchaseTest is Test { // [ ] performs post-purchase hook // [ ] performs post-purchase hook with fees // [ ] non-hooks - // [ ] reverts if the auction owner does not have sufficient balance of the payout token // [ ] success - transfers the quote token to the auction owner + // permutations: hooks/no hooks, derivative/non-derivative payout token function testReverts_whenLotIdIsInvalid() external { // Update the lot id to an invalid value From b2aadc0718051e543c8e01bdeb119496012eea51 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 11 Jan 2024 17:35:29 +0400 Subject: [PATCH 08/82] forge install: permit2 --- .gitmodules | 3 +++ lib/permit2 | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/permit2 diff --git a/.gitmodules b/.gitmodules index fa36f781..1de46ac8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/prb-math"] path = lib/prb-math url = https://github.com/PaulRBerg/prb-math +[submodule "lib/permit2"] + path = lib/permit2 + url = https://github.com/Uniswap/permit2 diff --git a/lib/permit2 b/lib/permit2 new file mode 160000 index 00000000..cc56ad0f --- /dev/null +++ b/lib/permit2 @@ -0,0 +1 @@ +Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219 From d1687dbdebf73a423f583a5e0e403017f676c66c Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 11 Jan 2024 18:17:44 +0400 Subject: [PATCH 09/82] Remove permit2 library. Add basic interface and clone. --- .gitmodules | 3 -- lib/permit2 | 1 - src/AuctionHouse.sol | 23 ++++++---- src/lib/permit2/interfaces/IPermit2.sol | 40 +++++++++++++++++ test/AuctionHouse/purchase.t.sol | 13 ++++-- test/lib/permit2/Permit2Clone.sol | 30 +++++++++++++ test/lib/permit2/Permit2Helper.sol | 59 +++++++++++++++++++++++++ 7 files changed, 153 insertions(+), 16 deletions(-) delete mode 160000 lib/permit2 create mode 100644 src/lib/permit2/interfaces/IPermit2.sol create mode 100644 test/lib/permit2/Permit2Clone.sol create mode 100644 test/lib/permit2/Permit2Helper.sol diff --git a/.gitmodules b/.gitmodules index 1de46ac8..fa36f781 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,6 +10,3 @@ [submodule "lib/prb-math"] path = lib/prb-math url = https://github.com/PaulRBerg/prb-math -[submodule "lib/permit2"] - path = lib/permit2 - url = https://github.com/Uniswap/permit2 diff --git a/lib/permit2 b/lib/permit2 deleted file mode 160000 index cc56ad0f..00000000 --- a/lib/permit2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219 diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index aee536a6..87838853 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -24,21 +24,26 @@ abstract contract Router is FeeManager { /// @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 lotId Lot ID - /// @param amount Amount of quoteToken to purchase with (in native decimals) - /// @param minAmountOut Minimum amount of baseToken to receive - /// @param auctionData Custom data used by the auction module - /// @param approval Permit approval signature for the quoteToken + /// + /// @param recipient Address to receive payout + /// @param referrer Address of referrer + /// @param approvalDeadline Deadline for 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 auctionData Custom data used by the auction module + /// @param approvalSignature Permit approval signature for the quoteToken struct PurchaseParams { address recipient; address referrer; + uint48 approvalDeadline; uint256 lotId; uint256 amount; uint256 minAmountOut; + uint256 approvalNonce; bytes auctionData; - bytes approval; + bytes approvalSignature; } // ========== STATE VARIABLES ========== // @@ -208,7 +213,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Handle transfers from purchaser and seller _handleTransfers( - params_.lotId, routing, params_.amount, payout, totalFees, params_.approval + params_.lotId, routing, params_.amount, payout, totalFees, params_.approvalSignature ); // Handle payout to user, including creation of derivative tokens diff --git a/src/lib/permit2/interfaces/IPermit2.sol b/src/lib/permit2/interfaces/IPermit2.sol new file mode 100644 index 00000000..be1c7b79 --- /dev/null +++ b/src/lib/permit2/interfaces/IPermit2.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// Minimal Permit2 interface, derived from +// https://github.com/Uniswap/permit2/blob/main/src/interfaces/ISignatureTransfer.sol +interface IPermit2 { + // Token and amount in a permit message. + struct TokenPermissions { + // Token to transfer. + address token; + // Amount to transfer. + uint256 amount; + } + + // The permit2 message. + struct PermitTransferFrom { + // Permitted token and amount. + TokenPermissions permitted; + // Unique identifier for this permit. + uint256 nonce; + // Expiration for this permit. + uint256 deadline; + } + + // Transfer details for permitTransferFrom(). + struct SignatureTransferDetails { + // Recipient of tokens. + address to; + // Amount to transfer. + uint256 requestedAmount; + } + + // Consume a permit2 message and transfer tokens. + function permitTransferFrom( + PermitTransferFrom calldata permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; +} diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index b8ff968e..c0dd100e 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.19; // Libraries import {Test} from "forge-std/Test.sol"; import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; +import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; +import {Permit2Helper} from "test/lib/permit2/Permit2Helper.sol"; // Mocks import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; @@ -30,7 +32,7 @@ import { Module } from "src/modules/Modules.sol"; -contract PurchaseTest is Test { +contract PurchaseTest is Test, Permit2Helper { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAtomicAuctionModule internal mockAuctionModule; @@ -54,6 +56,8 @@ contract PurchaseTest is Test { uint256 internal constant AMOUNT_IN = 1e18; uint256 internal AMOUNT_OUT; + uint256 internal constant APPROVAL_NONCE = 222; + function setUp() external { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); @@ -101,11 +105,13 @@ contract PurchaseTest is Test { purchaseParams = Router.PurchaseParams({ recipient: alice, referrer: referrer, + approvalDeadline: uint48(block.timestamp), lotId: lotId, amount: AMOUNT_IN, minAmountOut: AMOUNT_OUT, + approvalNonce: APPROVAL_NONCE, auctionData: bytes(""), - approval: bytes("") + approvalSignature: bytes("") }); } @@ -184,7 +190,8 @@ contract PurchaseTest is Test { // [ ] quote token transfers // [X] reverts if the caller does not have sufficient balance of the quote token // [ ] reverts if the caller has not approved the Permit2 contract - // [ ] reverts if the Permit2 approval is invalid + // [ ] reverts if the Permit2 approval signature is invalid + // [ ] reverts if the Permit2 approval signature is expired // [ ] allowlist // [ ] reverts if the caller is not on the allowlist // [ ] derivative payout token diff --git a/test/lib/permit2/Permit2Clone.sol b/test/lib/permit2/Permit2Clone.sol new file mode 100644 index 00000000..efa947c9 --- /dev/null +++ b/test/lib/permit2/Permit2Clone.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; + +// Local bytecode clone of the canonical Permit2 contract deployed to mainnet. +contract Permit2Clone is IPermit2 { + error InvalidNonce(); + error InvalidSigner(); + + constructor() { + // Deployed Permit2 bytecode at + // https://etherscan.io/address/0x000000000022D473030F116dDEE9F6B43aC78BA3 + bytes memory bytecode = + hex"6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000000103611b69577f866a5aba21966af95d6c7ab78eb2b2fc913915c28be3b9aa07cc04ff903e3f2890565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a"; + assembly { + return(add(bytecode, 0x20), mload(bytecode)) + } + } + + ///// STUBS ///// + function DOMAIN_SEPARATOR() external view returns (bytes32) {} + + function permitTransferFrom( + PermitTransferFrom calldata permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external {} +} diff --git a/test/lib/permit2/Permit2Helper.sol b/test/lib/permit2/Permit2Helper.sol new file mode 100644 index 00000000..3b789584 --- /dev/null +++ b/test/lib/permit2/Permit2Helper.sol @@ -0,0 +1,59 @@ +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; + +import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; +import {Permit2Clone} from "test/lib/permit2/Permit2Clone.sol"; + +/// @title Permit2Helper +/// @notice Helper functions for Permit2 +/// Largely lifted from https://github.com/dragonfly-xyz/useful-solidity-patterns/blob/main/test/Permit2Vault.t.sol +contract Permit2Helper is Test { + bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = + keccak256("TokenPermissions(address token,uint256 amount)"); + bytes32 constant PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( + "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + + Permit2Clone internal PERMIT2 = new Permit2Clone(); + + // Generate a signature for a permit message. + function _signPermit( + IPermit2.PermitTransferFrom memory permit, + address spender, + uint256 signerKey + ) internal returns (bytes memory sig) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, _getEIP712Hash(permit, spender)); + return abi.encodePacked(r, s, v); + } + + // Compute the EIP712 hash of the permit object. + // Normally this would be implemented off-chain. + function _getEIP712Hash( + IPermit2.PermitTransferFrom memory permit, + address spender + ) internal view returns (bytes32 h) { + return keccak256( + abi.encodePacked( + "\x19\x01", + PERMIT2.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + PERMIT_TRANSFER_FROM_TYPEHASH, + keccak256( + abi.encode( + TOKEN_PERMISSIONS_TYPEHASH, + permit.permitted.token, + permit.permitted.amount + ) + ), + spender, + permit.nonce, + permit.deadline + ) + ) + ) + ); + } +} From 360552f00e35c30be66f53f75c69460401d68420 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 12 Jan 2024 12:47:35 +0400 Subject: [PATCH 10/82] Documentation TODOs --- src/interfaces/IHooks.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/interfaces/IHooks.sol b/src/interfaces/IHooks.sol index 8a57c58b..61e25d43 100644 --- a/src/interfaces/IHooks.sol +++ b/src/interfaces/IHooks.sol @@ -5,11 +5,14 @@ pragma solidity >=0.8.0; /// @notice Interface for hook contracts to be called during auction payment and payout interface IHooks { /// @notice Called before payment and payout + /// TODO define expected state, invariants function pre(uint256 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; /// @notice Called after payment and after payout + /// TODO define expected state, invariants function post(uint256 lotId_, uint256 payout_) external; } From 6fc997c8c934f912dad1f1ef12c9f76008c7e1f1 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 12 Jan 2024 12:47:57 +0400 Subject: [PATCH 11/82] Refactor test TODOs for purchase using branching technique --- test/AuctionHouse/purchase.t.sol | 118 ++++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 35 deletions(-) diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index c0dd100e..eb74e2c1 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -181,41 +181,89 @@ contract PurchaseTest is Test, Permit2Helper { _; } - // purchase - // [X] reverts if the lot id is invalid - // [X] reverts if the auction is not atomic - // [X] reverts if the auction is not active - // [X] reverts if the auction module reverts - // [X] reverts if the payout amount is less than the minimum - // [ ] quote token transfers - // [X] reverts if the caller does not have sufficient balance of the quote token - // [ ] reverts if the caller has not approved the Permit2 contract - // [ ] reverts if the Permit2 approval signature is invalid - // [ ] reverts if the Permit2 approval signature is expired - // [ ] allowlist - // [ ] reverts if the caller is not on the allowlist - // [ ] derivative payout token - // [ ] mints derivative tokens to the recipient - // [ ] if specified, uses the condenser - // [ ] non-derivative payout token - // [X] reverts if the auction owner does not have sufficient balance of the payout token - // [ ] transfers the base token to the recipient - // [ ] fees - // [ ] protocol fees recorded - // [ ] referrer fees recorded - // [ ] hooks - // [ ] reverts if pre-purchase hook reverts - // [ ] reverts if mid-purchase hook reverts - // [ ] reverts if post-purchase hook reverts - // [ ] performs pre-purchase hook - // [ ] performs pre-purchase hook with fees - // [ ] performs mid-purchase hook - // [ ] performs mid-purchase hook with fees - // [ ] performs post-purchase hook - // [ ] performs post-purchase hook with fees - // [ ] non-hooks - // [ ] success - transfers the quote token to the auction owner - // permutations: hooks/no hooks, derivative/non-derivative payout token + // parameter checks + // [ ] when the lot id is invalid + // [ ] it reverts + // [ ] given the auction is not atomic + // [ ] it reverts + // [ ] given the auction is not active + // [ ] it reverts + // [ ] when the auction module reverts + // [ ] it reverts + // [ ] when the calculated payout amount is less than the minimum + // [ ] it reverts + // + // allowlist + // [ ] when the caller is not on the allowlist + // [ ] it reverts + // [ ] when the caller is on the allowlist + // [ ] it succeeds + // + // pre hook + // [ ] given the auction has hooks defined + // [ ] when the pre hook reverts + // [ ] it reverts + // [ ] when the pre hook does not revert + // [ ] given the invariant is not violated - TODO define invariant + // [ ] it succeeds + // [ ] given the invariant is violated + // [ ] it reverts + // + // transfers quote token from caller to auction house + // [ ] when the Permit2 signature is provided + // [ ] when the Permit2 signature is invalid + // [ ] it reverts + // [ ] when the Permit2 signature is expired + // [ ] it reverts + // [ ] when the Permit2 signature is valid + // [ ] given the caller has insufficient balance of the quote token + // [ ] it reverts + // [ ] given the caller has sufficient balance of the quote token + // [ ] given the received amount is less than the transferred amount + // [ ] it reverts + // [ ] given the received amount is the same as the transferred amount + // [ ] quote tokens (including fees) are transferred from the caller to the auction owner + // [ ] when the Permit2 signature is not provided + // [ ] given the caller has insufficient balance of the quote token + // [ ] it reverts + // [ ] given the caller has sufficient balance of the quote token + // [ ] given the caller has not approved the auction house to transfer the quote token + // [ ] it reverts + // [ ] given the caller has approved the auction house to transfer the quote token + // [ ] given the received amount is less than the transferred amount + // [ ] it reverts + // [ ] given the received amount is the same as the transferred amount + // [ ] quote tokens (including fees) are transferred from the caller to the auction owner + // + // exchange of quote and base tokens + // [ ] given the auction has hooks defined + // [ ] when the mid hook reverts + // [ ] it reverts + // [ ] when the mid hook does not transfer enough base tokens to the auction house + // [ ] it reverts + // [ ] when the mid hook transfers enough base tokens to the auction house + // [ ] it succeeds - quote tokens (minus fees) transferred to the auction owner + // [ ] given the auction does not have hooks defined + // [ ] given the received amount is less than the transferred amount + // [ ] it reverts + // [ ] given the received amount is the same as the transferred amount + // [ ] quote tokens (minus fees) are transferred to the auction owner + // + // transfers base token from auction house to recipient + // [ ] given the base token is a derivative + // [ ] given a condenser is set + // [ ] it uses the condenser to determine derivative parameters + // [ ] given a condenser is not set + // [ ] it uses the routing derivative parameters + // [ ] it mints derivative tokens to the recipient using the derivative module + // [ ] given the base token is not a derivative + // [ ] it transfers the base token to the recipient + // + // records fees + // [ ] given that a protocol fee is defined + // [ ] it records the protocol fee + // [ ] given that a referrer fee is defined + // [ ] it records the referrer fee function testReverts_whenLotIdIsInvalid() external { // Update the lot id to an invalid value From f2e07beceea5267dbde3db130785c180c82fca6e Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 12 Jan 2024 14:35:50 +0400 Subject: [PATCH 12/82] TODOs --- src/AuctionHouse.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 87838853..a2b8968a 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -14,11 +14,13 @@ import {Auction, AuctionModule} from "src/modules/Auction.sol"; import {Veecode, fromVeecode, WithModules} from "src/modules/Modules.sol"; +// TODO define purpose abstract contract FeeManager { // TODO write fee logic in separate contract to keep it organized // Router can inherit } +// TODO define purpose abstract contract Router is FeeManager { // ========== STRUCTS ========== // From 9c58c0fae0fdccc519fba407c44ca492d997831e Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 12 Jan 2024 14:37:56 +0400 Subject: [PATCH 13/82] Compile fix --- src/AuctionHouse.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 878b97e5..1cbd0757 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -200,7 +200,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Load routing data for the lot Routing memory routing = lotRouting[params_.lotId]; - uint256 totalFees = _allocateFees(params_.referrer, routing.quoteToken, params_.amount); + uint256 totalFees = allocateFees(params_.referrer, routing.quoteToken, params_.amount); // Send purchase to auction house and get payout plus any extra output bytes memory auctionOutput; From 216e063ef33f4dda10a8a92c70041dae6a1de17c Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 12 Jan 2024 14:56:16 +0400 Subject: [PATCH 14/82] Use solhint-community --- package.json | 6 +- pnpm-lock.yaml | 412 ++++++++++--------------------------------------- 2 files changed, 86 insertions(+), 332 deletions(-) diff --git a/package.json b/package.json index 688407f1..41c3858e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "forge build", "fmt": "forge fmt", "fmt:check": "forge fmt --check", - "solhint": "solhint --fix --noPrompt --config ./.solhint.json 'src/**/*.sol'", + "solhint": "solhint --fix --config ./.solhint.json 'src/**/*.sol'", "solhint:check": "solhint --config ./.solhint.json 'src/**/*.sol'", "lint": "npm run fmt && npm run solhint", "lint:check": "pnpm run fmt:check && pnpm run solhint:check", @@ -20,7 +20,7 @@ "keywords": [], "author": "", "license": "ISC", - "devDependencies": { - "solhint": "^4.1.1" + "dependencies": { + "solhint-community": "^3.7.0" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 704da571..401011e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,10 +4,10 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -devDependencies: - solhint: - specifier: ^4.1.1 - version: 4.1.1 +dependencies: + solhint-community: + specifier: ^3.7.0 + version: 3.7.0 packages: @@ -17,12 +17,12 @@ packages: dependencies: '@babel/highlight': 7.23.4 chalk: 2.4.2 - dev: true + dev: false /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} - dev: true + dev: false /@babel/highlight@7.23.4: resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} @@ -31,50 +31,13 @@ packages: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true - - /@pnpm/config.env-replace@1.1.0: - resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} - engines: {node: '>=12.22.0'} - dev: true - - /@pnpm/network.ca-file@1.0.2: - resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} - engines: {node: '>=12.22.0'} - dependencies: - graceful-fs: 4.2.10 - dev: true - - /@pnpm/npm-conf@2.2.2: - resolution: {integrity: sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==} - engines: {node: '>=12'} - dependencies: - '@pnpm/config.env-replace': 1.1.0 - '@pnpm/network.ca-file': 1.0.2 - config-chain: 1.1.13 - dev: true - - /@sindresorhus/is@5.6.0: - resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} - engines: {node: '>=14.16'} - dev: true + dev: false /@solidity-parser/parser@0.16.2: resolution: {integrity: sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg==} dependencies: antlr4ts: 0.5.0-alpha.4 - dev: true - - /@szmarczak/http-timer@5.0.1: - resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} - engines: {node: '>=14.16'} - dependencies: - defer-to-connect: 2.0.1 - dev: true - - /@types/http-cache-semantics@4.0.4: - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - dev: true + dev: false /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -83,7 +46,7 @@ packages: fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - dev: true + dev: false /ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} @@ -92,81 +55,63 @@ packages: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 uri-js: 4.4.1 - dev: true + dev: false /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - dev: true + dev: false /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} dependencies: color-convert: 1.9.3 - dev: true + dev: false /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} dependencies: color-convert: 2.0.1 - dev: true + dev: false /antlr4@4.13.1: resolution: {integrity: sha512-kiXTspaRYvnIArgE97z5YVVf/cDVQABr3abFRR6mE7yesLMkgu4ujuyV/sgxafQ8wgve0DJQUJ38Z8tkgA2izA==} engines: {node: '>=16'} - dev: true + dev: false /antlr4ts@0.5.0-alpha.4: resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} - dev: true + dev: false /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true + dev: false /ast-parents@0.0.1: resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==} - dev: true + dev: false /astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - dev: true + dev: false /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true + dev: false /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: balanced-match: 1.0.2 - dev: true - - /cacheable-lookup@7.0.0: - resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} - engines: {node: '>=14.16'} - dev: true - - /cacheable-request@10.2.14: - resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} - engines: {node: '>=14.16'} - dependencies: - '@types/http-cache-semantics': 4.0.4 - get-stream: 6.0.1 - http-cache-semantics: 4.1.1 - keyv: 4.5.4 - mimic-response: 4.0.0 - normalize-url: 8.0.0 - responselike: 3.0.0 - dev: true + dev: false /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - dev: true + dev: false /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -175,7 +120,7 @@ packages: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true + dev: false /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -183,40 +128,33 @@ packages: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true + dev: false /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 - dev: true + dev: false /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 - dev: true + dev: false /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true + dev: false /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true + dev: false - /commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} - engines: {node: '>=14'} - dev: true - - /config-chain@1.1.13: - resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} - dependencies: - ini: 1.3.8 - proto-list: 1.2.4 - dev: true + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: false /cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} @@ -231,65 +169,38 @@ packages: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 - dev: true - - /decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - dependencies: - mimic-response: 3.1.0 - dev: true - - /deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - dev: true - - /defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - dev: true + dev: false /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true + dev: false /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: is-arrayish: 0.2.1 - dev: true + dev: false /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - dev: true + dev: false /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true + dev: false /fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - dev: true + dev: false /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - - /form-data-encoder@2.1.4: - resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} - engines: {node: '>= 14.17'} - dev: true + dev: false /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true - - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - dev: true + dev: false /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} @@ -300,55 +211,22 @@ packages: inherits: 2.0.4 minimatch: 5.1.6 once: 1.4.0 - dev: true - - /got@12.6.1: - resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} - engines: {node: '>=14.16'} - dependencies: - '@sindresorhus/is': 5.6.0 - '@szmarczak/http-timer': 5.0.1 - cacheable-lookup: 7.0.0 - cacheable-request: 10.2.14 - decompress-response: 6.0.0 - form-data-encoder: 2.1.4 - get-stream: 6.0.1 - http2-wrapper: 2.2.1 - lowercase-keys: 3.0.0 - p-cancelable: 3.0.0 - responselike: 3.0.0 - dev: true - - /graceful-fs@4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - dev: true + dev: false /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true + dev: false /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - dev: true - - /http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - dev: true - - /http2-wrapper@2.2.1: - resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} - engines: {node: '>=10.19.0'} - dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 - dev: true + dev: false /ignore@5.3.0: resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} engines: {node: '>= 4'} - dev: true + dev: false /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -356,149 +234,82 @@ packages: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - dev: true + dev: false /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true + dev: false /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true - - /ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: true + dev: false /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true + dev: false /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - dev: true + dev: false /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true + dev: false /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true dependencies: argparse: 2.0.1 - dev: true - - /json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: true + dev: false /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true + dev: false /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true + dev: false /json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: true - - /keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - dependencies: - json-buffer: 3.0.1 - dev: true - - /latest-version@7.0.0: - resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} - engines: {node: '>=14.16'} - dependencies: - package-json: 8.1.1 - dev: true + dev: false /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true + dev: false /lodash.truncate@4.4.2: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - dev: true + dev: false /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true - - /lowercase-keys@3.0.0: - resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - - /mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - dev: true - - /mimic-response@4.0.0: - resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + dev: false /minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} dependencies: brace-expansion: 2.0.1 - dev: true - - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true - - /normalize-url@8.0.0: - resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} - engines: {node: '>=14.16'} - dev: true + dev: false /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 - dev: true - - /p-cancelable@3.0.0: - resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} - engines: {node: '>=12.20'} - dev: true - - /package-json@8.1.1: - resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} - engines: {node: '>=14.16'} - dependencies: - got: 12.6.1 - registry-auth-token: 5.0.2 - registry-url: 6.0.1 - semver: 7.5.4 - dev: true + dev: false /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} dependencies: callsites: 3.1.0 - dev: true + dev: false /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} @@ -508,92 +319,45 @@ packages: error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - dev: true + dev: false /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - dev: true + dev: false /pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - dev: true + dev: false /prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true requiresBuild: true - dev: true + dev: false optional: true - /proto-list@1.2.4: - resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - dev: true - /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - dev: true - - /quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - dev: true - - /rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - dev: true - - /registry-auth-token@5.0.2: - resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} - engines: {node: '>=14'} - dependencies: - '@pnpm/npm-conf': 2.2.2 - dev: true - - /registry-url@6.0.1: - resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} - engines: {node: '>=12'} - dependencies: - rc: 1.2.8 - dev: true + dev: false /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - dev: true - - /resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - dev: true + dev: false /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - dev: true - - /responselike@3.0.0: - resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} - engines: {node: '>=14.16'} - dependencies: - lowercase-keys: 3.0.0 - dev: true + dev: false - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true + dev: false /slice-ansi@4.0.0: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} @@ -602,10 +366,10 @@ packages: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - dev: true + dev: false - /solhint@4.1.1: - resolution: {integrity: sha512-7G4iF8H5hKHc0tR+/uyZesSKtfppFIMvPSW+Ku6MSL25oVRuyFeqNhOsXHfkex64wYJyXs4fe+pvhB069I19Tw==} + /solhint-community@3.7.0: + resolution: {integrity: sha512-8nfdaxVll+IIaEBHFz3CzagIZNNTGp4Mrr+6O4m7c9Bs/L8OcgR/xzZJFwROkGAhV8Nbiv4gqJ42nEXZPYl3Qw==} hasBin: true dependencies: '@solidity-parser/parser': 0.16.2 @@ -613,16 +377,15 @@ packages: antlr4: 4.13.1 ast-parents: 0.0.1 chalk: 4.1.2 - commander: 10.0.1 + commander: 11.1.0 cosmiconfig: 8.3.6 fast-diff: 1.3.0 glob: 8.1.0 ignore: 5.3.0 js-yaml: 4.1.0 - latest-version: 7.0.0 lodash: 4.17.21 pluralize: 8.0.0 - semver: 7.5.4 + semver: 6.3.1 strip-ansi: 6.0.1 table: 6.8.1 text-table: 0.2.0 @@ -630,7 +393,7 @@ packages: prettier: 2.8.8 transitivePeerDependencies: - typescript - dev: true + dev: false /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -639,33 +402,28 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true + dev: false /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - dev: true - - /strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - dev: true + dev: false /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true + dev: false /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} dependencies: has-flag: 4.0.0 - dev: true + dev: false /table@6.8.1: resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} @@ -676,22 +434,18 @@ packages: slice-ansi: 4.0.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true + dev: false /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true + dev: false /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.3.1 - dev: true + dev: false /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true + dev: false From e7abe1d7287f14aaf13ac7b23ae792712a9b8a57 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 12 Jan 2024 14:57:07 +0400 Subject: [PATCH 15/82] Add new solhint rules --- .solhint.json | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/.solhint.json b/.solhint.json index ab96b455..649c2263 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,17 +1,33 @@ { - "extends": "solhint:recommended", + "extends": "solhint:recommended", "rules": { - "compiler-version": ["error",">=0.7.0"], + "compiler-version": [ + "error", + ">=0.7.0" + ], "avoid-low-level-calls": "off", - "const-name-snakecase": "off", - "var-name-mixedcase": "off", - "func-name-mixedcase": "off", + "const-name-snakecase": "warn", + "var-name-mixedcase": "warn", + "func-name-mixedcase": "warn", + "immutable-name-snakecase": "warn", + "modifier-name-mixedcase": "warn", + "private-vars-leading-underscore": "warn", "not-rely-on-time": "off", - "func-visibility": [ "warn", { "ignoreConstructors":true }], + "func-visibility": [ + "warn", + { + "ignoreConstructors": true + } + ], "no-inline-assembly": "off", "reason-string": "off", "no-empty-blocks": "off", "no-console": "off", - "no-global-import": "off" + "no-global-import": "warn", + "no-unused-import": "warn", + "no-unused-vars": "warn", + "avoid-tx-origin": "error", + "reentrancy": "error", + "state-visibility": "warn" } -} +} \ No newline at end of file From 6f0b598aa130c54feb7fb7143460f4a6c6a8d265 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 12 Jan 2024 15:07:02 +0400 Subject: [PATCH 16/82] Disable solhint rule --- .solhint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.solhint.json b/.solhint.json index 649c2263..911f8b30 100644 --- a/.solhint.json +++ b/.solhint.json @@ -8,7 +8,7 @@ "avoid-low-level-calls": "off", "const-name-snakecase": "warn", "var-name-mixedcase": "warn", - "func-name-mixedcase": "warn", + "func-name-mixedcase": "off", "immutable-name-snakecase": "warn", "modifier-name-mixedcase": "warn", "private-vars-leading-underscore": "warn", From b909d7f8fa0fceecb3ec12e600ce9b846d0b0d4c Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 12 Jan 2024 15:07:13 +0400 Subject: [PATCH 17/82] chore: linting fixes --- design/ARCHITECTURE.md | 2 +- src/AuctionHouse.sol | 10 +++++----- src/bases/Auctioneer.sol | 2 +- src/bases/Derivatizer.sol | 2 +- src/modules/Auction.sol | 4 ++-- src/modules/Condenser.sol | 2 +- src/modules/Derivative.sol | 2 +- src/modules/Modules.sol | 3 +++ src/modules/auctions/OFDA.sol | 12 ++++++------ src/modules/auctions/OSDA.sol | 18 +++++++++--------- src/modules/auctions/SDA.sol | 22 +++++++++++----------- 11 files changed, 41 insertions(+), 38 deletions(-) diff --git a/design/ARCHITECTURE.md b/design/ARCHITECTURE.md index d2254e12..7ecd7e3b 100644 --- a/design/ARCHITECTURE.md +++ b/design/ARCHITECTURE.md @@ -128,7 +128,7 @@ classDiagram struct AuctionParams +bool allowNewMarkets +uint48 minAuctionDuration - ~uint48 ONE_HUNDRED_PERCENT = 1e5 + ~uint48 _ONE_HUNDRED_PERCENT = 1e5 +mapping[uint256 lotId => Lot] lotData +purchase(uint256 lotId, uint256 amount, bytes auctionData) (uint256, bytes) +bid(uint256 lotId, uint256 amount, uint256 minAmountOut, bytes auctionData) (uint256, bytes) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 1cbd0757..a6a4fdb7 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -70,12 +70,12 @@ abstract contract Router is FeeManager { // Address the protocol receives fees at // TODO make this updatable - address internal immutable PROTOCOL; + address internal immutable _PROTOCOL; // ========== CONSTRUCTOR ========== // constructor(address protocol_) { - PROTOCOL = protocol_; + _PROTOCOL = protocol_; } // ========== ATOMIC AUCTIONS ========== // @@ -131,7 +131,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== AUCTION FUNCTIONS ========== // - function allocateFees( + function _allocateFees( address referrer_, ERC20 quoteToken_, uint256 amount_ @@ -164,7 +164,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; } @@ -200,7 +200,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Load routing data for the lot Routing memory routing = lotRouting[params_.lotId]; - uint256 totalFees = allocateFees(params_.referrer, routing.quoteToken, params_.amount); + uint256 totalFees = _allocateFees(params_.referrer, routing.quoteToken, params_.amount); // Send purchase to auction house and get payout plus any extra output bytes memory auctionOutput; diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index 91b4059e..137c8258 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -71,7 +71,7 @@ abstract contract Auctioneer is WithModules { /// @notice Constant representing 100% /// @dev 1% = 1_000 or 1e3. 100% = 100_000 or 1e5 - uint48 internal constant ONE_HUNDRED_PERCENT = 1e5; + uint48 internal constant _ONE_HUNDRED_PERCENT = 1e5; /// @notice Counter for auction lots uint256 public lotCounter; diff --git a/src/bases/Derivatizer.sol b/src/bases/Derivatizer.sol index 4f6e3350..17a49d78 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 "src/modules/Derivative.sol"; +import {WithModules} from "src/modules/Modules.sol"; abstract contract Derivatizer is WithModules { // // ========== DERIVATIVE MANAGEMENT ========== // diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 8e4ba3e1..0e9f3844 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -1,7 +1,7 @@ /// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.19; -import "src/modules/Modules.sol"; +import {Module} from "src/modules/Modules.sol"; abstract contract Auction { /* ========== ERRORS ========== */ @@ -60,7 +60,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 = 1e5; /// @notice General information pertaining to auction lots mapping(uint256 id => Lot lot) public lotData; diff --git a/src/modules/Condenser.sol b/src/modules/Condenser.sol index e825d8c8..09a85249 100644 --- a/src/modules/Condenser.sol +++ b/src/modules/Condenser.sol @@ -1,7 +1,7 @@ /// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.19; -import "src/modules/Modules.sol"; +import {Module} from "src/modules/Modules.sol"; abstract contract Condenser { function condense( diff --git a/src/modules/Derivative.sol b/src/modules/Derivative.sol index 2a3c3901..4c7fd586 100644 --- a/src/modules/Derivative.sol +++ b/src/modules/Derivative.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.19; import {ERC6909} from "lib/solmate/src/tokens/ERC6909.sol"; -import "src/modules/Modules.sol"; +import {Module, Keycode} from "src/modules/Modules.sol"; abstract contract Derivative { // ========== DATA STRUCTURES ========== // diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 8c5662ba..792679fc 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -375,13 +375,16 @@ abstract contract Module { /// @notice 2 byte identifier for the module type /// @dev This enables the parent contract to check that the module Keycode specified /// @dev is of the correct type + // solhint-disable-next-line func-name-mixedcase function TYPE() public pure virtual returns (Type) {} /// @notice 7 byte, versioned identifier for the module. 2 characters from 0-9 that signify the version and 3-5 characters from A-Z. + // solhint-disable-next-line func-name-mixedcase function VEECODE() public pure virtual returns (Veecode) {} /// @notice Initialization function for the module /// @dev This function is called when the module is installed or upgraded by the module. /// @dev MUST BE GATED BY onlyParent. Used to encompass any initialization or upgrade logic. + // solhint-disable-next-line func-name-mixedcase function INIT() external virtual onlyParent {} } diff --git a/src/modules/auctions/OFDA.sol b/src/modules/auctions/OFDA.sol index 60a3eaff..085d507e 100644 --- a/src/modules/auctions/OFDA.sol +++ b/src/modules/auctions/OFDA.sol @@ -70,8 +70,8 @@ pragma solidity 0.8.19; // // Validate discounts // if ( -// fixedDiscount >= ONE_HUNDRED_PERCENT || -// maxDiscountFromCurrent > ONE_HUNDRED_PERCENT || +// fixedDiscount >= _ONE_HUNDRED_PERCENT || +// maxDiscountFromCurrent > _ONE_HUNDRED_PERCENT || // fixedDiscount > maxDiscountFromCurrent // ) revert Auctioneer_InvalidParams(); @@ -82,8 +82,8 @@ pragma solidity 0.8.19; // auction.conversionMul = conversionMul; // auction.conversionFactor = conversionFactor; // auction.minPrice = oraclePrice.mulDivUp( -// ONE_HUNDRED_PERCENT - maxDiscountFromCurrent, -// ONE_HUNDRED_PERCENT +// _ONE_HUNDRED_PERCENT - maxDiscountFromCurrent, +// _ONE_HUNDRED_PERCENT // ); // } @@ -116,8 +116,8 @@ pragma solidity 0.8.19; // // Apply fixed discount // uint256 price = oraclePrice.mulDivUp( -// uint256(ONE_HUNDRED_PERCENT - auction.fixedDiscount), -// uint256(ONE_HUNDRED_PERCENT) +// uint256(_ONE_HUNDRED_PERCENT - auction.fixedDiscount), +// uint256(_ONE_HUNDRED_PERCENT) // ); // // Compare the current price to the minimum price and return the maximum diff --git a/src/modules/auctions/OSDA.sol b/src/modules/auctions/OSDA.sol index a18fc1df..72a36cc5 100644 --- a/src/modules/auctions/OSDA.sol +++ b/src/modules/auctions/OSDA.sol @@ -91,8 +91,8 @@ pragma solidity 0.8.19; // // Validate discounts // if ( -// baseDiscount >= ONE_HUNDRED_PERCENT || -// maxDiscountFromCurrent > ONE_HUNDRED_PERCENT || +// baseDiscount >= _ONE_HUNDRED_PERCENT || +// maxDiscountFromCurrent > _ONE_HUNDRED_PERCENT || // baseDiscount > maxDiscountFromCurrent // ) revert Auctioneer_InvalidParams(); @@ -103,8 +103,8 @@ pragma solidity 0.8.19; // auction.conversionMul = conversionMul; // auction.conversionFactor = conversionFactor; // auction.minPrice = oraclePrice.mulDivUp( -// ONE_HUNDRED_PERCENT - maxDiscountFromCurrent, -// ONE_HUNDRED_PERCENT +// _ONE_HUNDRED_PERCENT - maxDiscountFromCurrent, +// _ONE_HUNDRED_PERCENT // ); // } @@ -137,8 +137,8 @@ pragma solidity 0.8.19; // // Apply base discount // price = price.mulDivUp( -// uint256(ONE_HUNDRED_PERCENT - auction.baseDiscount), -// uint256(ONE_HUNDRED_PERCENT) +// 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 @@ -174,7 +174,7 @@ pragma solidity 0.8.19; // uint256 decay; // if (expectedCapacity > core.capacity) { // decay = -// ONE_HUNDRED_PERCENT + +// _ONE_HUNDRED_PERCENT + // (auction.decaySpeed * (expectedCapacity - core.capacity)) / // initialCapacity; // } else { @@ -182,11 +182,11 @@ pragma solidity 0.8.19; // // 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; +// 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); +// 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 index 0d2a84b4..1ce2538c 100644 --- a/src/modules/auctions/SDA.sol +++ b/src/modules/auctions/SDA.sol @@ -108,7 +108,7 @@ pragma solidity 0.8.19; // // Validate auction data // if (initialPrice == 0) revert Auctioneer_InvalidParams(); // if (initialPrice < minPrice) revert Auctioneer_InitialPriceLessThanMin(); -// if (targetIntervalDiscount >= ONE_HUNDRED_PERCENT) revert Auctioneer_InvalidParams(); +// if (targetIntervalDiscount >= _ONE_HUNDRED_PERCENT) revert Auctioneer_InvalidParams(); // // Set auction data // uint48 duration = core_.conclusion - core_.start; @@ -226,24 +226,24 @@ pragma solidity 0.8.19; // // Calculate the percent delta expected and current capacity // uint256 delta = capacity > expectedCapacity -// ? ((capacity - expectedCapacity) * ONE_HUNDRED_PERCENT) / initialCapacity -// : ((expectedCapacity - capacity) * ONE_HUNDRED_PERCENT) / initialCapacity; +// ? ((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; +// 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; +// 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.change = auction.equilibriumPrice.mulDiv(delta, _ONE_HUNDRED_PERCENT); // adjustment.lastAdjustment = currentTime; // adjustment.timeToAdjusted = tune.tuneAdjustmentDelay; // } else { @@ -251,8 +251,8 @@ pragma solidity 0.8.19; // // Increase equilibrium price by delta percent // auctionData[id_].equilibriumPrice = auction.equilibriumPrice.mulDiv( -// ONE_HUNDRED_PERCENT + delta, -// ONE_HUNDRED_PERCENT +// _ONE_HUNDRED_PERCENT + delta, +// _ONE_HUNDRED_PERCENT // ); // // Set current adjustment to inactive (e.g. if we are re-tuning early) @@ -348,7 +348,7 @@ pragma solidity 0.8.19; // uint256 decay; // if (expectedCapacity > core.capacity) { // decay = -// ONE_HUNDRED_PERCENT + +// _ONE_HUNDRED_PERCENT + // (auction.decaySpeed * (expectedCapacity - core.capacity)) / // initialCapacity; // } else { @@ -356,11 +356,11 @@ pragma solidity 0.8.19; // // 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; +// 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); +// 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; From 3a6e6a1741f13b1b72d45c537daad30212285b13 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 12 Jan 2024 16:07:13 +0400 Subject: [PATCH 18/82] WIP refactoring of payment collection into Router --- src/AuctionHouse.sol | 36 +++++++++ test/AuctionHouse/purchase.t.sol | 38 +-------- test/Router/ConcreteRouter.sol | 58 +++++++++++++ test/Router/MockFeeOnTransferERC20.sol | 32 ++++++++ test/Router/collectPayment.t.sol | 108 +++++++++++++++++++++++++ test/modules/Auction/MockHook.sol | 6 ++ 6 files changed, 242 insertions(+), 36 deletions(-) create mode 100644 test/Router/ConcreteRouter.sol create mode 100644 test/Router/MockFeeOnTransferERC20.sol create mode 100644 test/Router/collectPayment.t.sol diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index a6a4fdb7..b03e5887 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -14,6 +14,8 @@ import {Auction, AuctionModule} from "src/modules/Auction.sol"; import {Veecode, fromVeecode, WithModules} from "src/modules/Modules.sol"; +import {IHooks} from "src/interfaces/IHooks.sol"; + // TODO define purpose abstract contract FeeManager { // TODO write fee logic in separate contract to keep it organized @@ -107,6 +109,40 @@ abstract contract Router is FeeManager { uint256 id_, Auction.Bid[] memory bids_ ) external virtual returns (uint256[] memory amountsOut); + + // ========== TOKEN TRANSFERS ========== // + + /// @notice Collects payment of the quote token from the user + /// @dev This function handles the following: + /// 1. Calls the pre hook on the hooks contract (if provided) + /// 2. Transfers the quote token from the user + /// 2a. Uses Permit2 to transfer if approval signature is provided + /// 2b. Otherwise uses a standard ERC20 transfer + /// + /// This function reverts if: + /// - The Permit2 approval is invalid + /// - The caller does not have sufficient balance of the quote token + /// - Approval has not been granted to transfer the quote token + /// - The quote token transfer fails + /// - Transferring the quote token would result in a lesser amount being received + /// - The pre-hook reverts + /// + /// @param lotId_ Lot ID + /// @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 + function _collectPayment( + uint256 lotId_, + uint256 amount_, + ERC20 quoteToken_, + IHooks hooks_, + uint48 approvalDeadline_, + uint256 approvalNonce_, + bytes memory approvalSignature_ + ) internal {} } /// @title AuctionHouse diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index eb74e2c1..c6635d3e 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -199,42 +199,6 @@ contract PurchaseTest is Test, Permit2Helper { // [ ] when the caller is on the allowlist // [ ] it succeeds // - // pre hook - // [ ] given the auction has hooks defined - // [ ] when the pre hook reverts - // [ ] it reverts - // [ ] when the pre hook does not revert - // [ ] given the invariant is not violated - TODO define invariant - // [ ] it succeeds - // [ ] given the invariant is violated - // [ ] it reverts - // - // transfers quote token from caller to auction house - // [ ] when the Permit2 signature is provided - // [ ] when the Permit2 signature is invalid - // [ ] it reverts - // [ ] when the Permit2 signature is expired - // [ ] it reverts - // [ ] when the Permit2 signature is valid - // [ ] given the caller has insufficient balance of the quote token - // [ ] it reverts - // [ ] given the caller has sufficient balance of the quote token - // [ ] given the received amount is less than the transferred amount - // [ ] it reverts - // [ ] given the received amount is the same as the transferred amount - // [ ] quote tokens (including fees) are transferred from the caller to the auction owner - // [ ] when the Permit2 signature is not provided - // [ ] given the caller has insufficient balance of the quote token - // [ ] it reverts - // [ ] given the caller has sufficient balance of the quote token - // [ ] given the caller has not approved the auction house to transfer the quote token - // [ ] it reverts - // [ ] given the caller has approved the auction house to transfer the quote token - // [ ] given the received amount is less than the transferred amount - // [ ] it reverts - // [ ] given the received amount is the same as the transferred amount - // [ ] quote tokens (including fees) are transferred from the caller to the auction owner - // // exchange of quote and base tokens // [ ] given the auction has hooks defined // [ ] when the mid hook reverts @@ -244,6 +208,8 @@ contract PurchaseTest is Test, Permit2Helper { // [ ] when the mid hook transfers enough base tokens to the auction house // [ ] it succeeds - quote tokens (minus fees) transferred to the auction owner // [ ] given the auction does not have hooks defined + // [ ] given that approval has not been given to the auction house to transfer base tokens + // [ ] it reverts // [ ] given the received amount is less than the transferred amount // [ ] it reverts // [ ] given the received amount is the same as the transferred amount diff --git a/test/Router/ConcreteRouter.sol b/test/Router/ConcreteRouter.sol new file mode 100644 index 00000000..b029439c --- /dev/null +++ b/test/Router/ConcreteRouter.sol @@ -0,0 +1,58 @@ +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +// Standard libraries +import {ERC20} from "solmate/tokens/ERC20.sol"; + +import {Router} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; +import {IHooks} from "src/interfaces/IHooks.sol"; + +contract ConcreteRouter is Router { + constructor(address protocol_) Router(protocol_) {} + + function purchase(PurchaseParams memory params_) + external + virtual + override + returns (uint256 payout) + {} + + 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) {} + + function settle( + uint256 id_, + Auction.Bid[] memory bids_ + ) external virtual override returns (uint256[] memory amountsOut) {} + + // Expose the _collectPayment function for testing + function collectPayment( + uint256 lotId_, + uint256 amount_, + ERC20 quoteToken_, + IHooks hooks_, + uint48 approvalDeadline_, + uint256 approvalNonce_, + bytes memory approvalSignature_ + ) external { + return _collectPayment( + lotId_, + amount_, + quoteToken_, + hooks_, + approvalDeadline_, + approvalNonce_, + approvalSignature_ + ); + } +} diff --git a/test/Router/MockFeeOnTransferERC20.sol b/test/Router/MockFeeOnTransferERC20.sol new file mode 100644 index 00000000..38f583ea --- /dev/null +++ b/test/Router/MockFeeOnTransferERC20.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + +contract MockFeeOnTransferERC20 is MockERC20 { + uint256 public transferFee; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) MockERC20(name_, symbol_, decimals_) {} + + function setTransferFee(uint256 transferFee_) external { + transferFee = transferFee_; + } + + function transfer(address recipient_, uint256 amount_) public override returns (bool) { + uint256 fee = amount_ * transferFee / 10_000; + return super.transfer(recipient_, amount_ - fee); + } + + function transferFrom( + address sender_, + address recipient_, + uint256 amount_ + ) public override returns (bool) { + uint256 fee = amount_ * transferFee / 10_000; + return super.transferFrom(sender_, recipient_, amount_ - fee); + } +} diff --git a/test/Router/collectPayment.t.sol b/test/Router/collectPayment.t.sol new file mode 100644 index 00000000..820dc22d --- /dev/null +++ b/test/Router/collectPayment.t.sol @@ -0,0 +1,108 @@ +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; + +import {MockHook} from "test/modules/Auction/MockHook.sol"; +import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; +import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; + +import {IHooks} from "src/interfaces/IHooks.sol"; + +contract RouterTest is Test { + ConcreteRouter internal router; + + address internal constant PROTOCOL = address(0x1); + address internal constant USER = address(0x2); + + // Function parameters + uint256 internal lotId = 1; + uint256 internal amount = 10e18; + MockFeeOnTransferERC20 internal quoteToken; + MockHook internal hook; + uint48 internal approvalDeadline = 0; + uint256 internal approvalNonce = 0; + bytes internal approvalSignature = ""; + + function setUp() public { + router = new ConcreteRouter(PROTOCOL); + + quoteToken = new MockFeeOnTransferERC20("QUOTE", "QT", 18); + quoteToken.setTransferFee(0); + } + + modifier givenUserHasBalance(uint256 amount_) { + quoteToken.mint(USER, amount_); + _; + } + + modifier whenPermit2ApprovalIsValid() { + // TODO + _; + } + + modifier whenPermit2ApprovalIsInvalid() { + // TODO + _; + } + + modifier whenPermit2ApprovalIsExpired() { + // TODO + _; + } + + modifier givenUserHasApprovedRouter() { + // As USER, grant approval to transfer quote tokens to the router + vm.prank(USER); + quoteToken.approve(address(router), amount); + _; + } + + modifier givenTokenTakesFeeOnTransfer() { + // Configure the token to take a 1% fee + quoteToken.setTransferFee(100); + _; + } + + // [ ] when the Permit2 signature is provided + // [ ] when the Permit2 signature is invalid + // [ ] it reverts + // [ ] when the Permit2 signature is expired + // [ ] it reverts + // [ ] when the Permit2 signature is valid + // [ ] given the caller has insufficient balance of the quote token + // [ ] it reverts + // [ ] given the received amount is not equal to the transferred amount + // [ ] it reverts + // [ ] given the received amount is the same as the transferred amount + // [ ] quote tokens are transferred from the caller to the auction owner + // [ ] when the Permit2 signature is not provided + // [ ] given the caller has insufficient balance of the quote token + // [ ] it reverts + // [ ] given the caller has sufficient balance of the quote token + // [ ] given the caller has not approved the auction house to transfer the quote token + // [ ] it reverts + // [ ] given the received amount is not equal to the transferred amount + // [ ] it reverts + // [ ] given the received amount is the same as the transferred amount + // [ ] quote tokens are transferred from the caller to the auction owner + + // [ ] given the auction has hooks defined + // [ ] when the pre hook reverts + // [ ] it reverts + // [ ] when the pre hook does not revert + // [ ] given the invariant is violated + // [ ] it reverts + // [ ] given the invariant is not violated - TODO define invariant + // [ ] it succeeds + + modifier whenHooksIsSet() { + hook = new MockHook(); + _; + } + + modifier whenPreHookReverts() { + hook.setPreHookReverts(true); + _; + } +} diff --git a/test/modules/Auction/MockHook.sol b/test/modules/Auction/MockHook.sol index 0a9a361c..c1457821 100644 --- a/test/modules/Auction/MockHook.sol +++ b/test/modules/Auction/MockHook.sol @@ -4,8 +4,14 @@ pragma solidity 0.8.19; import {IHooks} from "src/bases/Auctioneer.sol"; contract MockHook is IHooks { + bool public preHookReverts; + function pre(uint256 lotId_, uint256 amount_) external override {} + function setPreHookReverts(bool preHookReverts_) external { + preHookReverts = preHookReverts_; + } + function mid(uint256 lotId_, uint256 amount_, uint256 payout_) external override {} function post(uint256 lotId_, uint256 payout_) external override {} From 469fc21cfa7e264f1833f53de0475286540f80c3 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 15 Jan 2024 15:03:19 +0400 Subject: [PATCH 19/82] transfer and pre-hook implementation --- src/AuctionHouse.sol | 51 +++++++++++- test/Router/collectPayment.t.sol | 131 ++++++++++++++++++++++++++---- test/modules/Auction/MockHook.sol | 78 ++++++++++++++++-- 3 files changed, 236 insertions(+), 24 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index b03e5887..de170e0c 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -24,6 +24,16 @@ abstract contract FeeManager { // TODO define purpose abstract contract Router is FeeManager { + using SafeTransferLib for ERC20; + + // ========== ERRORS ========== // + + error InsufficientBalance(address token_, uint256 requiredAmount_); + + error InsufficientAllowance(address token_, address router_, uint256 requiredAmount_); + + error UnsupportedToken(address token_); + // ========== STRUCTS ========== // /// @notice Parameters used by the purchase function @@ -142,7 +152,41 @@ abstract contract Router is FeeManager { uint48 approvalDeadline_, uint256 approvalNonce_, bytes memory approvalSignature_ - ) internal {} + ) internal { + // Call pre hook on hooks contract if provided + if (address(hooks_) != address(0)) { + hooks_.pre(lotId_, amount_); + } + + // Check that the user has sufficient balance of the quote token + if (quoteToken_.balanceOf(msg.sender) < amount_) { + revert InsufficientBalance(address(quoteToken_), amount_); + } + + // Check if approval signature has been provided, if so use it to transfer + if (approvalSignature_.length != 0) { + // TODO + } else { + _transfer(amount_, quoteToken_); + } + } + + function _transfer(uint256 amount_, ERC20 token_) internal { + // Check that the user has granted approval to transfer the quote token + if (token_.allowance(msg.sender, address(this)) < amount_) { + revert InsufficientAllowance(address(token_), address(this), amount_); + } + + uint256 balanceBefore = token_.balanceOf(address(this)); + + // Transfer the quote token from the user + token_.safeTransferFrom(msg.sender, address(this), amount_); + + // Check that it is not a fee-on-transfer token + if (token_.balanceOf(address(this)) < balanceBefore + amount_) { + revert UnsupportedToken(address(token_)); + } + } } /// @title AuctionHouse @@ -155,7 +199,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== ERRORS ========== // error AmountLessThanMinimum(); error InvalidHook(); - error UnsupportedToken(ERC20 token_); // ========== EVENTS ========== // event Purchase(uint256 id, address buyer, address referrer, uint256 amount, uint256 payout); @@ -312,7 +355,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 quoteBalance = routing_.quoteToken.balanceOf(address(this)); routing_.quoteToken.safeTransferFrom(msg.sender, address(this), amount_); if (routing_.quoteToken.balanceOf(address(this)) < quoteBalance + amount_) { - revert UnsupportedToken(routing_.quoteToken); + revert UnsupportedToken(address(routing_.quoteToken)); } // If callback address supplied, transfer tokens from teller to callback, then execute callback function, @@ -338,7 +381,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 baseBalance = routing_.baseToken.balanceOf(address(this)); routing_.baseToken.safeTransferFrom(routing_.owner, address(this), payout_); if (routing_.baseToken.balanceOf(address(this)) < (baseBalance + payout_)) { - revert UnsupportedToken(routing_.baseToken); + revert UnsupportedToken(address(routing_.baseToken)); } routing_.quoteToken.safeTransfer(routing_.owner, amountLessFee); diff --git a/test/Router/collectPayment.t.sol b/test/Router/collectPayment.t.sol index 820dc22d..4ef948eb 100644 --- a/test/Router/collectPayment.t.sol +++ b/test/Router/collectPayment.t.sol @@ -7,6 +7,7 @@ import {MockHook} from "test/modules/Auction/MockHook.sol"; import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; +import {Router} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; contract RouterTest is Test { @@ -64,6 +65,8 @@ contract RouterTest is Test { _; } + // ============ Permit2 flow ============ + // [ ] when the Permit2 signature is provided // [ ] when the Permit2 signature is invalid // [ ] it reverts @@ -76,25 +79,89 @@ contract RouterTest is Test { // [ ] it reverts // [ ] given the received amount is the same as the transferred amount // [ ] quote tokens are transferred from the caller to the auction owner - // [ ] when the Permit2 signature is not provided - // [ ] given the caller has insufficient balance of the quote token - // [ ] it reverts - // [ ] given the caller has sufficient balance of the quote token - // [ ] given the caller has not approved the auction house to transfer the quote token - // [ ] it reverts - // [ ] given the received amount is not equal to the transferred amount - // [ ] it reverts - // [ ] given the received amount is the same as the transferred amount - // [ ] quote tokens are transferred from the caller to the auction owner - // [ ] given the auction has hooks defined - // [ ] when the pre hook reverts - // [ ] it reverts + // ============ Transfer flow ============ + + // [X] when the Permit2 signature is not provided + // [X] given the caller has insufficient balance of the quote token + // [X] it reverts + // [X] given the caller has sufficient balance of the quote token + // [X] given the caller has not approved the auction house to transfer the quote token + // [X] it reverts + // [X] given the received amount is not equal to the transferred amount + // [X] it reverts + // [X] given the received amount is the same as the transferred amount + // [X] quote tokens are transferred from the caller to the auction owner + + function test_transfer_insufficientBalance_reverts() public { + // Expect the error + bytes memory err = + abi.encodeWithSelector(Router.InsufficientBalance.selector, address(quoteToken), amount); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + } + + function test_transfer_noApproval_reverts() public givenUserHasBalance(amount) { + // Expect the error + bytes memory err = abi.encodeWithSelector( + Router.InsufficientAllowance.selector, address(quoteToken), address(router), amount + ); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + } + + function test_transfer_feeOnTransfer_reverts() + public + givenUserHasBalance(amount) + givenUserHasApprovedRouter + givenTokenTakesFeeOnTransfer + { + // Expect the error + bytes memory err = + abi.encodeWithSelector(Router.UnsupportedToken.selector, address(quoteToken)); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + } + + function test_transfer() public givenUserHasBalance(amount) givenUserHasApprovedRouter { + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + + // Expect the user to have no balance + assertEq(quoteToken.balanceOf(USER), 0); + + // Expect the router to have the balance + assertEq(quoteToken.balanceOf(address(router)), amount); + } + + // ============ Hooks flow ============ + + // [X] given the auction has hooks defined + // [X] when the pre hook reverts + // [X] it reverts // [ ] when the pre hook does not revert // [ ] given the invariant is violated // [ ] it reverts - // [ ] given the invariant is not violated - TODO define invariant - // [ ] it succeeds + // [X] given the invariant is not violated - TODO define invariant + // [X] it succeeds modifier whenHooksIsSet() { hook = new MockHook(); @@ -105,4 +172,38 @@ contract RouterTest is Test { hook.setPreHookReverts(true); _; } + + modifier whenPreHookBalanceIsRecorded() { + hook.setPreHookValues(address(quoteToken), USER); + _; + } + + function test_preHook_reverts() public whenHooksIsSet whenPreHookReverts { + // Expect the error + vm.expectRevert("revert"); + + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + } + + function test_preHook() + public + givenUserHasBalance(amount) + givenUserHasApprovedRouter + whenHooksIsSet + whenPreHookBalanceIsRecorded + { + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + + // Expect the pre hook to have recorded the balance of USER before the transfer + assertEq(hook.preHookBalance(), amount); + assertEq(quoteToken.balanceOf(USER), 0); + } } diff --git a/test/modules/Auction/MockHook.sol b/test/modules/Auction/MockHook.sol index c1457821..fd43c908 100644 --- a/test/modules/Auction/MockHook.sol +++ b/test/modules/Auction/MockHook.sol @@ -1,18 +1,86 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; +import {ERC20} from "solmate/tokens/ERC20.sol"; + import {IHooks} from "src/bases/Auctioneer.sol"; contract MockHook is IHooks { + address public preHookToken; + address public preHookUser; + uint256 public preHookBalance; bool public preHookReverts; - function pre(uint256 lotId_, uint256 amount_) external override {} + address public midHookToken; + address public midHookUser; + uint256 public midHookBalance; + bool public midHookReverts; + + address public postHookToken; + address public postHookUser; + uint256 public postHookBalance; + bool public postHookReverts; + + function pre(uint256, uint256) external override { + if (preHookReverts) { + revert("revert"); + } + + if (preHookToken != address(0) && preHookUser != address(0)) { + preHookBalance = ERC20(preHookToken).balanceOf(preHookUser); + } else { + preHookBalance = 0; + } + } + + function setPreHookValues(address token_, address user_) external { + preHookToken = token_; + preHookUser = user_; + } - function setPreHookReverts(bool preHookReverts_) external { - preHookReverts = preHookReverts_; + function setPreHookReverts(bool reverts_) external { + preHookReverts = reverts_; } - function mid(uint256 lotId_, uint256 amount_, uint256 payout_) external override {} + function mid(uint256, uint256, uint256) external override { + if (midHookReverts) { + revert("revert"); + } - function post(uint256 lotId_, uint256 payout_) external override {} + if (midHookToken != address(0) && midHookUser != address(0)) { + midHookBalance = ERC20(midHookToken).balanceOf(midHookUser); + } else { + midHookBalance = 0; + } + } + + function setMidHookValues(address token_, address user_) external { + midHookToken = token_; + midHookUser = user_; + } + + function setMidHookReverts(bool reverts_) external { + midHookReverts = reverts_; + } + + function post(uint256, uint256) external override { + if (postHookReverts) { + revert("revert"); + } + + if (postHookToken != address(0) && postHookUser != address(0)) { + postHookBalance = ERC20(postHookToken).balanceOf(postHookUser); + } else { + postHookBalance = 0; + } + } + + function setPostHookValues(address token_, address user_) external { + postHookToken = token_; + postHookUser = user_; + } + + function setPostHookReverts(bool reverts_) external { + postHookReverts = reverts_; + } } From d3a5294aa29db3a2adc48ff21fc9523a519b36ce Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 15 Jan 2024 16:15:08 +0400 Subject: [PATCH 20/82] Tests and implementation for Permit2 transfers --- src/AuctionHouse.sol | 58 +++- test/AuctionHouse/auction.t.sol | 5 +- test/AuctionHouse/cancel.t.sol | 5 +- test/AuctionHouse/purchase.t.sol | 6 +- test/AuctionHouse/setCondenser.t.sol | 5 +- test/Router/ConcreteRouter.sol | 2 +- test/Router/collectPayment.t.sol | 293 ++++++++++++++++-- test/lib/permit2/Permit2Clone.sol | 3 + .../{Permit2Helper.sol => Permit2User.sol} | 27 +- test/modules/Auction/auction.t.sol | 5 +- test/modules/Auction/cancel.t.sol | 5 +- 11 files changed, 360 insertions(+), 54 deletions(-) rename test/lib/permit2/{Permit2Helper.sol => Permit2User.sol} (74%) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index de170e0c..3a104afa 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -4,10 +4,12 @@ pragma solidity 0.8.19; import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; import {SafeTransferLib} from "lib/solmate/src/utils/SafeTransferLib.sol"; -import {Derivatizer} from "src/bases/Derivatizer.sol"; +import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; + import {Auctioneer} from "src/bases/Auctioneer.sol"; import {CondenserModule} from "src/modules/Condenser.sol"; +import {Derivatizer} from "src/bases/Derivatizer.sol"; import {DerivativeModule} from "src/modules/Derivative.sol"; import {Auction, AuctionModule} from "src/modules/Auction.sol"; @@ -30,7 +32,7 @@ abstract contract Router is FeeManager { error InsufficientBalance(address token_, uint256 requiredAmount_); - error InsufficientAllowance(address token_, address router_, uint256 requiredAmount_); + error InsufficientAllowance(address token_, address spender_, uint256 requiredAmount_); error UnsupportedToken(address token_); @@ -84,10 +86,13 @@ abstract contract Router is FeeManager { // TODO make this updatable address internal immutable _PROTOCOL; + IPermit2 public immutable _PERMIT2; + // ========== CONSTRUCTOR ========== // - constructor(address protocol_) { + constructor(address protocol_, address permit2_) { _PROTOCOL = protocol_; + _PERMIT2 = IPermit2(permit2_); } // ========== ATOMIC AUCTIONS ========== // @@ -163,10 +168,14 @@ abstract contract Router is FeeManager { revert InsufficientBalance(address(quoteToken_), amount_); } - // Check if approval signature has been provided, if so use it to transfer + // If a Permit2 approval signature is provided, use it to transfer the quote token if (approvalSignature_.length != 0) { - // TODO - } else { + _permit2Transfer( + amount_, quoteToken_, approvalDeadline_, approvalNonce_, approvalSignature_ + ); + } + // Otherwise fallback to a standard ERC20 transfer + else { _transfer(amount_, quoteToken_); } } @@ -187,6 +196,38 @@ abstract contract Router is FeeManager { revert UnsupportedToken(address(token_)); } } + + function _permit2Transfer( + uint256 amount_, + ERC20 token_, + uint48 approvalDeadline_, + uint256 approvalNonce_, + bytes memory approvalSignature_ + ) internal { + // Check that the user has granted approval to PERMIT2 to transfer the quote token + if (token_.allowance(msg.sender, address(_PERMIT2)) < amount_) { + revert InsufficientAllowance(address(token_), address(_PERMIT2), amount_); + } + + uint256 balanceBefore = token_.balanceOf(address(this)); + + // Use PERMIT2 to transfer the token from the user + _PERMIT2.permitTransferFrom( + IPermit2.PermitTransferFrom( + IPermit2.TokenPermissions(address(token_), amount_), + approvalNonce_, + approvalDeadline_ + ), + IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: amount_}), + msg.sender, // Spender of the tokens + approvalSignature_ + ); + + // Check that it is not a fee-on-transfer token + if (token_.balanceOf(address(this)) < balanceBefore + amount_) { + revert UnsupportedToken(address(token_)); + } + } } /// @title AuctionHouse @@ -204,7 +245,10 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { event Purchase(uint256 id, address buyer, address referrer, uint256 amount, uint256 payout); // ========== CONSTRUCTOR ========== // - constructor(address protocol_) Router(protocol_) WithModules(msg.sender) {} + constructor( + address protocol_, + address permit2_ + ) Router(protocol_, permit2_) WithModules(msg.sender) {} // ========== DIRECT EXECUTION ========== // diff --git a/test/AuctionHouse/auction.t.sol b/test/AuctionHouse/auction.t.sol index 44519bbc..26149a2b 100644 --- a/test/AuctionHouse/auction.t.sol +++ b/test/AuctionHouse/auction.t.sol @@ -12,6 +12,7 @@ import {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule import {MockCondenserModule} from "test/modules/Condenser/MockCondenserModule.sol"; import {MockAllowlist} from "test/modules/Auction/MockAllowlist.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; // Auctions import {AuctionHouse} from "src/AuctionHouse.sol"; @@ -29,7 +30,7 @@ import { Module } from "src/modules/Modules.sol"; -contract AuctionTest is Test { +contract AuctionTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAuctionModule internal mockAuctionModule; @@ -48,7 +49,7 @@ contract AuctionTest is Test { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); - auctionHouse = new AuctionHouse(protocol); + auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); mockAuctionModule = new MockAuctionModule(address(auctionHouse)); mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); mockCondenserModule = new MockCondenserModule(address(auctionHouse)); diff --git a/test/AuctionHouse/cancel.t.sol b/test/AuctionHouse/cancel.t.sol index 9e253b58..9354ef6a 100644 --- a/test/AuctionHouse/cancel.t.sol +++ b/test/AuctionHouse/cancel.t.sol @@ -8,6 +8,7 @@ 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 {Permit2User} from "test/lib/permit2/Permit2User.sol"; // Auctions import {AuctionHouse} from "src/AuctionHouse.sol"; @@ -25,7 +26,7 @@ import { Module } from "src/modules/Modules.sol"; -contract CancelTest is Test { +contract CancelTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAuctionModule internal mockAuctionModule; @@ -44,7 +45,7 @@ contract CancelTest is Test { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); - auctionHouse = new AuctionHouse(auctionOwner); + auctionHouse = new AuctionHouse(auctionOwner, _PERMIT2_ADDRESS); mockAuctionModule = new MockAuctionModule(address(auctionHouse)); auctionHouse.installModule(mockAuctionModule); diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index c6635d3e..e3dcdcf5 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -5,7 +5,6 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; -import {Permit2Helper} from "test/lib/permit2/Permit2Helper.sol"; // Mocks import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; @@ -15,6 +14,7 @@ import {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule import {MockCondenserModule} from "test/modules/Condenser/MockCondenserModule.sol"; import {MockAllowlist} from "test/modules/Auction/MockAllowlist.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; // Auctions import {AuctionHouse, Router} from "src/AuctionHouse.sol"; @@ -32,7 +32,7 @@ import { Module } from "src/modules/Modules.sol"; -contract PurchaseTest is Test, Permit2Helper { +contract PurchaseTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAtomicAuctionModule internal mockAuctionModule; @@ -62,7 +62,7 @@ contract PurchaseTest is Test, Permit2Helper { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); - auctionHouse = new AuctionHouse(protocol); + auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); mockAuctionModule = new MockAtomicAuctionModule(address(auctionHouse)); mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); mockCondenserModule = new MockCondenserModule(address(auctionHouse)); diff --git a/test/AuctionHouse/setCondenser.t.sol b/test/AuctionHouse/setCondenser.t.sol index 915a766c..8b6891e7 100644 --- a/test/AuctionHouse/setCondenser.t.sol +++ b/test/AuctionHouse/setCondenser.t.sol @@ -10,6 +10,7 @@ import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; import {MockAuctionModule} from "test/modules/Auction/MockAuctionModule.sol"; import {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule.sol"; import {MockCondenserModule} from "test/modules/Condenser/MockCondenserModule.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; // Auctions import {AuctionHouse} from "src/AuctionHouse.sol"; @@ -28,7 +29,7 @@ import { Module } from "src/modules/Modules.sol"; -contract SetCondenserTest is Test { +contract SetCondenserTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAuctionModule internal mockAuctionModule; @@ -48,7 +49,7 @@ contract SetCondenserTest is Test { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); - auctionHouse = new AuctionHouse(protocol); + auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); mockAuctionModule = new MockAuctionModule(address(auctionHouse)); mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); mockCondenserModule = new MockCondenserModule(address(auctionHouse)); diff --git a/test/Router/ConcreteRouter.sol b/test/Router/ConcreteRouter.sol index b029439c..53458658 100644 --- a/test/Router/ConcreteRouter.sol +++ b/test/Router/ConcreteRouter.sol @@ -9,7 +9,7 @@ import {Auction} from "src/modules/Auction.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; contract ConcreteRouter is Router { - constructor(address protocol_) Router(protocol_) {} + constructor(address protocol_, address permit2_) Router(protocol_, permit2_) {} function purchase(PurchaseParams memory params_) external diff --git a/test/Router/collectPayment.t.sol b/test/Router/collectPayment.t.sol index 4ef948eb..188d0d3b 100644 --- a/test/Router/collectPayment.t.sol +++ b/test/Router/collectPayment.t.sol @@ -6,15 +6,20 @@ import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; +import {Permit2Clone} from "test/lib/permit2/Permit2Clone.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; +import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; import {Router} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; -contract RouterTest is Test { +contract RouterTest is Test, Permit2User { ConcreteRouter internal router; address internal constant PROTOCOL = address(0x1); - address internal constant USER = address(0x2); + + uint256 internal userKey; + address internal USER; // Function parameters uint256 internal lotId = 1; @@ -26,10 +31,16 @@ contract RouterTest is Test { bytes internal approvalSignature = ""; function setUp() public { - router = new ConcreteRouter(PROTOCOL); + // Set reasonable starting block + vm.warp(1_000_000); + + router = new ConcreteRouter(PROTOCOL, _PERMIT2_ADDRESS); quoteToken = new MockFeeOnTransferERC20("QUOTE", "QT", 18); quoteToken.setTransferFee(0); + + userKey = _getRandomUint256(); + USER = vm.addr(userKey); } modifier givenUserHasBalance(uint256 amount_) { @@ -37,48 +48,255 @@ contract RouterTest is Test { _; } + modifier givenUserHasApprovedRouter() { + // As USER, grant approval to transfer quote tokens to the router + vm.prank(USER); + quoteToken.approve(address(router), amount); + _; + } + + modifier givenTokenTakesFeeOnTransfer() { + // Configure the token to take a 1% fee + quoteToken.setTransferFee(100); + _; + } + + // ============ Permit2 flow ============ + + // [X] when the Permit2 signature is provided + // [X] when the Permit2 signature is invalid + // [X] it reverts + // [X] when the Permit2 signature is expired + // [X] it reverts + // [X] when the Permit2 signature is valid + // [X] given the caller has insufficient balance of the quote token + // [X] it reverts + // [X] given the received amount is not equal to the transferred amount + // [X] it reverts + // [X] given the received amount is the same as the transferred amount + // [X] quote tokens are transferred from the caller to the auction owner + + modifier givenPermit2Approved() { + // Approve the Permit2 contract to spend the quote token + vm.prank(USER); + quoteToken.approve(_PERMIT2_ADDRESS, type(uint256).max); + _; + } + modifier whenPermit2ApprovalIsValid() { - // TODO + // Assumes approval has been given + + approvalNonce = _getRandomUint256(); + approvalDeadline = uint48(block.timestamp + 1 days); + approvalSignature = _signPermit( + IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({token: address(quoteToken), amount: amount}), + nonce: approvalNonce, + deadline: approvalDeadline + }), + address(router), + userKey + ); + _; + } + + modifier whenPermit2ApprovalNonceIsUsed() { + // Assumes that whenPermit2ApprovalIsValid precedes this modifier + require(approvalNonce != 0, "approval nonce is 0"); + + // Mint tokens + quoteToken.mint(USER, amount); + + // Consume the nonce + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + _; + } + + modifier whenPermit2ApprovalIsOtherSigner() { + // Sign as another user + uint256 anotherUserKey = _getRandomUint256(); + + approvalNonce = _getRandomUint256(); + approvalDeadline = uint48(block.timestamp + 1 days); + approvalSignature = _signPermit( + IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({token: address(quoteToken), amount: amount}), + nonce: approvalNonce, + deadline: approvalDeadline + }), + address(router), + anotherUserKey + ); _; } modifier whenPermit2ApprovalIsInvalid() { - // TODO + approvalNonce = _getRandomUint256(); + approvalDeadline = uint48(block.timestamp + 1 days); + approvalSignature = "JUNK"; _; } modifier whenPermit2ApprovalIsExpired() { - // TODO + approvalNonce = _getRandomUint256(); + approvalDeadline = uint48(block.timestamp - 1 days); + approvalSignature = _signPermit( + IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({token: address(quoteToken), amount: amount}), + nonce: approvalNonce, + deadline: approvalDeadline + }), + address(router), + userKey + ); _; } - modifier givenUserHasApprovedRouter() { - // As USER, grant approval to transfer quote tokens to the router + function test_permit2_noApproval_reverts() + public + givenUserHasBalance(amount) + whenPermit2ApprovalIsValid + { + // Expect the error + bytes memory err = abi.encodeWithSelector( + Router.InsufficientAllowance.selector, address(quoteToken), _PERMIT2_ADDRESS, amount + ); + vm.expectRevert(err); + + // Call vm.prank(USER); - quoteToken.approve(address(router), amount); - _; + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); } - modifier givenTokenTakesFeeOnTransfer() { - // Configure the token to take a 1% fee - quoteToken.setTransferFee(100); - _; + function test_permit2_reusedSignature_reverts() + public + givenUserHasBalance(amount) + givenPermit2Approved + whenPermit2ApprovalIsValid + whenPermit2ApprovalNonceIsUsed + { + // Expect the error + bytes memory err = abi.encodeWithSelector(Permit2Clone.InvalidNonce.selector); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); } - // ============ Permit2 flow ============ + function test_permit2_invalidSignature_reverts() + public + givenUserHasBalance(amount) + givenPermit2Approved + whenPermit2ApprovalIsInvalid + { + // Expect the error + bytes memory err = abi.encodeWithSelector(Permit2Clone.InvalidSignatureLength.selector); + vm.expectRevert(err); - // [ ] when the Permit2 signature is provided - // [ ] when the Permit2 signature is invalid - // [ ] it reverts - // [ ] when the Permit2 signature is expired - // [ ] it reverts - // [ ] when the Permit2 signature is valid - // [ ] given the caller has insufficient balance of the quote token - // [ ] it reverts - // [ ] given the received amount is not equal to the transferred amount - // [ ] it reverts - // [ ] given the received amount is the same as the transferred amount - // [ ] quote tokens are transferred from the caller to the auction owner + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + } + + function test_permit2_expiredSignature_reverts() + public + givenUserHasBalance(amount) + givenPermit2Approved + whenPermit2ApprovalIsExpired + { + // Expect the error + bytes memory err = + abi.encodeWithSelector(Permit2Clone.SignatureExpired.selector, approvalDeadline); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + } + + function test_permit2_otherSigner_reverts() + public + givenUserHasBalance(amount) + givenPermit2Approved + whenPermit2ApprovalIsOtherSigner + { + // Expect the error + bytes memory err = abi.encodeWithSelector(Permit2Clone.InvalidSigner.selector); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + } + + function test_permit2_insufficientBalance_reverts() + public + givenPermit2Approved + whenPermit2ApprovalIsValid + { + // Expect the error + bytes memory err = + abi.encodeWithSelector(Router.InsufficientBalance.selector, address(quoteToken), amount); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + } + + function test_permit2_feeOnTransfer_reverts() + public + givenUserHasBalance(amount) + givenTokenTakesFeeOnTransfer + givenPermit2Approved + whenPermit2ApprovalIsValid + { + // Expect the error + bytes memory err = + abi.encodeWithSelector(Router.UnsupportedToken.selector, address(quoteToken)); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + } + + function test_permit2() + public + givenUserHasBalance(amount) + givenPermit2Approved + whenPermit2ApprovalIsValid + { + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + + // Expect the user to have no balance + assertEq(quoteToken.balanceOf(USER), 0); + + // Expect the router to have the balance + assertEq(quoteToken.balanceOf(address(router)), amount); + } // ============ Transfer flow ============ @@ -189,7 +407,7 @@ contract RouterTest is Test { ); } - function test_preHook() + function test_preHook_transfer() public givenUserHasBalance(amount) givenUserHasApprovedRouter @@ -206,4 +424,23 @@ contract RouterTest is Test { assertEq(hook.preHookBalance(), amount); assertEq(quoteToken.balanceOf(USER), 0); } + + function test_preHook_permit2() + public + givenUserHasBalance(amount) + givenPermit2Approved + whenPermit2ApprovalIsValid + whenHooksIsSet + whenPreHookBalanceIsRecorded + { + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + + // Expect the pre hook to have recorded the balance of USER before the transfer + assertEq(hook.preHookBalance(), amount); + assertEq(quoteToken.balanceOf(USER), 0); + } } diff --git a/test/lib/permit2/Permit2Clone.sol b/test/lib/permit2/Permit2Clone.sol index efa947c9..2bb5b0d0 100644 --- a/test/lib/permit2/Permit2Clone.sol +++ b/test/lib/permit2/Permit2Clone.sol @@ -7,6 +7,9 @@ import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; contract Permit2Clone is IPermit2 { error InvalidNonce(); error InvalidSigner(); + error SignatureExpired(uint256 deadline); + error InvalidAmount(uint256 amount); + error InvalidSignatureLength(); constructor() { // Deployed Permit2 bytecode at diff --git a/test/lib/permit2/Permit2Helper.sol b/test/lib/permit2/Permit2User.sol similarity index 74% rename from test/lib/permit2/Permit2Helper.sol rename to test/lib/permit2/Permit2User.sol index 3b789584..0b7b8989 100644 --- a/test/lib/permit2/Permit2Helper.sol +++ b/test/lib/permit2/Permit2User.sol @@ -6,24 +6,41 @@ import {Test} from "forge-std/Test.sol"; import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; import {Permit2Clone} from "test/lib/permit2/Permit2Clone.sol"; -/// @title Permit2Helper +/// @title Permit2User /// @notice Helper functions for Permit2 /// Largely lifted from https://github.com/dragonfly-xyz/useful-solidity-patterns/blob/main/test/Permit2Vault.t.sol -contract Permit2Helper is Test { +contract Permit2User is Test { bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); bytes32 constant PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" ); - Permit2Clone internal PERMIT2 = new Permit2Clone(); + Permit2Clone internal _PERMIT2 = new Permit2Clone(); + address internal _PERMIT2_ADDRESS = address(_PERMIT2); + + // Generate a random uint256 + function _getRandomUint256() internal view returns (uint256) { + return uint256( + keccak256( + abi.encode( + tx.origin, + block.number, + block.timestamp, + block.coinbase, + address(this).codehash, + gasleft() + ) + ) + ); + } // Generate a signature for a permit message. function _signPermit( IPermit2.PermitTransferFrom memory permit, address spender, uint256 signerKey - ) internal returns (bytes memory sig) { + ) internal view returns (bytes memory sig) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, _getEIP712Hash(permit, spender)); return abi.encodePacked(r, s, v); } @@ -37,7 +54,7 @@ contract Permit2Helper is Test { return keccak256( abi.encodePacked( "\x19\x01", - PERMIT2.DOMAIN_SEPARATOR(), + _PERMIT2.DOMAIN_SEPARATOR(), keccak256( abi.encode( PERMIT_TRANSFER_FROM_TYPEHASH, diff --git a/test/modules/Auction/auction.t.sol b/test/modules/Auction/auction.t.sol index 021c74f8..9443c0d9 100644 --- a/test/modules/Auction/auction.t.sol +++ b/test/modules/Auction/auction.t.sol @@ -8,6 +8,7 @@ import {console2} from "forge-std/console2.sol"; // Mocks import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; import {MockAuctionModule} from "test/modules/Auction/MockAuctionModule.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; // Auctions import {AuctionHouse} from "src/AuctionHouse.sol"; @@ -25,7 +26,7 @@ import { Module } from "src/modules/Modules.sol"; -contract AuctionTest is Test { +contract AuctionTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAuctionModule internal mockAuctionModule; @@ -43,7 +44,7 @@ contract AuctionTest is Test { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); - auctionHouse = new AuctionHouse(protocol); + auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); mockAuctionModule = new MockAuctionModule(address(auctionHouse)); auctionHouse.installModule(mockAuctionModule); diff --git a/test/modules/Auction/cancel.t.sol b/test/modules/Auction/cancel.t.sol index a2d1c547..49fe53cd 100644 --- a/test/modules/Auction/cancel.t.sol +++ b/test/modules/Auction/cancel.t.sol @@ -8,6 +8,7 @@ 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 {Permit2User} from "test/lib/permit2/Permit2User.sol"; // Auctions import {AuctionHouse} from "src/AuctionHouse.sol"; @@ -25,7 +26,7 @@ import { Module } from "src/modules/Modules.sol"; -contract CancelTest is Test { +contract CancelTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAuctionModule internal mockAuctionModule; @@ -44,7 +45,7 @@ contract CancelTest is Test { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); - auctionHouse = new AuctionHouse(protocol); + auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); mockAuctionModule = new MockAuctionModule(address(auctionHouse)); auctionHouse.installModule(mockAuctionModule); From db34c900fd0d929f225879f7b70b120fcb3c91f6 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 15 Jan 2024 16:30:28 +0400 Subject: [PATCH 21/82] Documentation --- src/AuctionHouse.sol | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 3a104afa..9f7ec60b 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -180,6 +180,19 @@ abstract contract Router is FeeManager { } } + /// @notice Performs an ERC20 transfer of `token_` from the caller + /// @dev This function handles the following: + /// 1. Checks that the user has granted approval to transfer the token + /// 2. Transfers the token from the user + /// 3. Checks that the transferred amount was received + /// + /// This function reverts if: + /// - Approval has not been granted to this contract to transfer the token + /// - The token transfer fails + /// - The transferred amount is less than the requested amount + /// + /// @param amount_ Amount of tokens to transfer (in native decimals) + /// @param token_ Token to transfer function _transfer(uint256 amount_, ERC20 token_) internal { // Check that the user has granted approval to transfer the quote token if (token_.allowance(msg.sender, address(this)) < amount_) { @@ -189,6 +202,7 @@ abstract contract Router is FeeManager { uint256 balanceBefore = token_.balanceOf(address(this)); // Transfer the quote token from the user + // `safeTransferFrom()` will revert upon failure token_.safeTransferFrom(msg.sender, address(this), amount_); // Check that it is not a fee-on-transfer token @@ -197,6 +211,22 @@ abstract contract Router is FeeManager { } } + /// @notice Performs a Permit2 transfer of `token_` from the caller + /// @dev This function handles the following: + /// 1. Checks that the user has granted approval to transfer the token + /// 2. Uses Permit2 to transfer the token from the user + /// 3. Checks that the transferred amount was received + /// + /// This function reverts if: + /// - Approval has not been granted to Permit2 to transfer the token + /// - The Permit2 transfer (or signature validation) fails + /// - The transferred amount is less than the requested amount + /// + /// @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 function _permit2Transfer( uint256 amount_, ERC20 token_, From 19a2541cb6649024d44ca50a2730f1a9f9e8c778 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 15 Jan 2024 16:35:25 +0400 Subject: [PATCH 22/82] Fix cancelled test --- test/AuctionHouse/purchase.t.sol | 10 ---------- test/modules/Auction/MockAtomicAuctionModule.sol | 6 +++++- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index e3dcdcf5..b692ba0f 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -171,16 +171,6 @@ contract PurchaseTest is Test, Permit2User { _; } - modifier whenPermit2IsApproved() { - // TODO - _; - } - - modifier whenPermit2ApprovalIsValid() { - // TODO - _; - } - // parameter checks // [ ] when the lot id is invalid // [ ] it reverts diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 8665ac7b..d5806930 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -11,6 +11,8 @@ contract MockAtomicAuctionModule is AuctionModule { mapping(uint256 => uint256) public payoutData; bool public purchaseReverts; + mapping(uint256 lotId => bool isCancelled) public cancelled; + constructor(address _owner) AuctionModule(_owner) { minAuctionDuration = 1 days; } @@ -32,7 +34,7 @@ contract MockAtomicAuctionModule is AuctionModule { } function _cancel(uint256 id_) internal override { - // + cancelled[id_] = true; } function purchase( @@ -42,6 +44,8 @@ contract MockAtomicAuctionModule is AuctionModule { ) external virtual override returns (uint256 payout, bytes memory auctionOutput) { if (purchaseReverts) revert("error"); + if (cancelled[id_]) revert Auction_MarketNotActive(id_); + payout = payoutData[id_] * amount_; auctionOutput = auctionData_; } From 5f5ba743651ad60243962a3638b317db0632b2fa Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 15 Jan 2024 16:48:11 +0400 Subject: [PATCH 23/82] Fix test function names --- test/AuctionHouse/purchase.t.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index b692ba0f..dce63f6d 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -221,7 +221,7 @@ contract PurchaseTest is Test, Permit2User { // [ ] given that a referrer fee is defined // [ ] it records the referrer fee - function testReverts_whenLotIdIsInvalid() external { + function test_whenLotIdIsInvalid_reverts() external { // Update the lot id to an invalid value purchaseParams.lotId = 1; @@ -235,7 +235,7 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); } - function testReverts_whenNotAtomicAuction() + function test_whenNotAtomicAuction_reverts() external whenBatchAuctionIsCreated whenAccountHasQuoteTokenBalance(AMOUNT_IN) @@ -253,7 +253,7 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); } - function testReverts_whenAuctionNotActive() + function test_whenAuctionNotActive_reverts() external whenAuctionIsCancelled whenAccountHasQuoteTokenBalance(AMOUNT_IN) @@ -268,7 +268,7 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); } - function testReverts_whenAuctionModuleReverts() + function test_whenAuctionModuleReverts_reverts() external whenAccountHasQuoteTokenBalance(AMOUNT_IN) whenAccountHasBaseTokenBalance(AMOUNT_OUT) @@ -284,7 +284,7 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); } - function testReverts_whenPayoutAmountLessThanMinimum() + function test_whenPayoutAmountLessThanMinimum_reverts() external whenAccountHasQuoteTokenBalance(AMOUNT_IN) whenAccountHasBaseTokenBalance(AMOUNT_OUT) @@ -301,7 +301,7 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); } - function testReverts_whenCallerHasInsufficientBalanceOfQuoteToken() + function test_whenCallerHasInsufficientBalanceOfQuoteToken_reverts() external whenAccountHasBaseTokenBalance(AMOUNT_OUT) { @@ -313,7 +313,7 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); } - function testReverts_whenOwnerHasInsufficientBalanceOfBaseToken() + function test_whenOwnerHasInsufficientBalanceOfBaseToken_reverts() external whenAccountHasQuoteTokenBalance(AMOUNT_IN) { From 777b78bc87b2312cac7f95927d1ccd00d373a8a0 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 15 Jan 2024 17:17:31 +0400 Subject: [PATCH 24/82] Simplify Permit2 signing. Add tests for allowlist. --- src/AuctionHouse.sol | 2 + test/AuctionHouse/purchase.t.sol | 230 ++++++++++++++++++------- test/Router/collectPayment.t.sol | 75 ++++---- test/lib/permit2/Permit2User.sol | 10 +- test/modules/Auction/MockAllowlist.sol | 18 +- 5 files changed, 236 insertions(+), 99 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 9f7ec60b..bff4c423 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -271,6 +271,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { error AmountLessThanMinimum(); error InvalidHook(); + error NotAuthorized(); + // ========== EVENTS ========== // event Purchase(uint256 id, address buyer, address referrer, uint256 amount, uint256 payout); diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index dce63f6d..ffee1478 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -42,23 +42,32 @@ contract PurchaseTest is Test, Permit2User { MockHook internal mockHook; AuctionHouse internal auctionHouse; - Auctioneer.RoutingParams internal routingParams; - Auction.AuctionParams internal auctionParams; - Router.PurchaseParams internal purchaseParams; address internal immutable protocol = address(0x2); - address internal immutable alice = address(0x3); address internal immutable referrer = address(0x4); address internal immutable auctionOwner = address(0x5); + uint256 internal aliceKey; + address internal alice; + uint256 internal lotId; uint256 internal constant AMOUNT_IN = 1e18; uint256 internal AMOUNT_OUT; - uint256 internal constant APPROVAL_NONCE = 222; + uint256 internal approvalNonce; + bytes internal approvalSignature; + uint48 internal approvalDeadline; + + // Function parameters (can be modified) + Auctioneer.RoutingParams internal routingParams; + Auction.AuctionParams internal auctionParams; + Router.PurchaseParams internal purchaseParams; function setUp() external { + aliceKey = _getRandomUint256(); + alice = vm.addr(aliceKey); + baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); @@ -102,16 +111,19 @@ contract PurchaseTest is Test, Permit2User { // 1:1 exchange rate AMOUNT_OUT = AMOUNT_IN; + approvalNonce = _getRandomUint256(); + approvalDeadline = uint48(block.timestamp) + 1 days; + purchaseParams = Router.PurchaseParams({ recipient: alice, referrer: referrer, - approvalDeadline: uint48(block.timestamp), + approvalDeadline: approvalDeadline, lotId: lotId, amount: AMOUNT_IN, minAmountOut: AMOUNT_OUT, - approvalNonce: APPROVAL_NONCE, + approvalNonce: approvalNonce, auctionData: bytes(""), - approvalSignature: bytes("") + approvalSignature: approvalSignature }); } @@ -172,54 +184,14 @@ contract PurchaseTest is Test, Permit2User { } // parameter checks - // [ ] when the lot id is invalid - // [ ] it reverts - // [ ] given the auction is not atomic - // [ ] it reverts - // [ ] given the auction is not active - // [ ] it reverts - // [ ] when the auction module reverts - // [ ] it reverts - // [ ] when the calculated payout amount is less than the minimum - // [ ] it reverts - // - // allowlist - // [ ] when the caller is not on the allowlist - // [ ] it reverts - // [ ] when the caller is on the allowlist - // [ ] it succeeds - // - // exchange of quote and base tokens - // [ ] given the auction has hooks defined - // [ ] when the mid hook reverts - // [ ] it reverts - // [ ] when the mid hook does not transfer enough base tokens to the auction house - // [ ] it reverts - // [ ] when the mid hook transfers enough base tokens to the auction house - // [ ] it succeeds - quote tokens (minus fees) transferred to the auction owner - // [ ] given the auction does not have hooks defined - // [ ] given that approval has not been given to the auction house to transfer base tokens - // [ ] it reverts - // [ ] given the received amount is less than the transferred amount - // [ ] it reverts - // [ ] given the received amount is the same as the transferred amount - // [ ] quote tokens (minus fees) are transferred to the auction owner - // - // transfers base token from auction house to recipient - // [ ] given the base token is a derivative - // [ ] given a condenser is set - // [ ] it uses the condenser to determine derivative parameters - // [ ] given a condenser is not set - // [ ] it uses the routing derivative parameters - // [ ] it mints derivative tokens to the recipient using the derivative module - // [ ] given the base token is not a derivative - // [ ] it transfers the base token to the recipient - // - // records fees - // [ ] given that a protocol fee is defined - // [ ] it records the protocol fee - // [ ] given that a referrer fee is defined - // [ ] it records the referrer fee + // [X] when the lot id is invalid + // [X] it reverts + // [X] given the auction is not atomic + // [X] it reverts + // [X] given the auction is not active + // [X] it reverts + // [X] when the auction module reverts + // [X] it reverts function test_whenLotIdIsInvalid_reverts() external { // Update the lot id to an invalid value @@ -284,26 +256,133 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); } - function test_whenPayoutAmountLessThanMinimum_reverts() + // allowlist + // [X] given an allowlist is set + // [X] when the caller is not on the allowlist + // [X] it reverts + // [X] when the caller is on the allowlist + // [X] it succeeds + + modifier givenAuctionHasAllowlist() { + // Register a new auction with an allowlist + routingParams.allowlist = mockAllowlist; + lotId = auctionHouse.auction(routingParams, auctionParams); + _; + } + + modifier givenCallerIsOnAllowlist() { + // Assumes the allowlist is set + require(address(routingParams.allowlist) != address(0), "allowlist not set"); + + // Set the caller to be on the allowlist + mockAllowlist.setAllowed(alice, true); + _; + } + + function test_givenCallerNotOnAllowlist() external givenAuctionHasAllowlist { + // Expect revert + bytes memory err = abi.encodeWithSelector(AuctionHouse.NotAuthorized.selector); + vm.expectRevert(err); + + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + } + + function test_givenCallerOnAllowlist() external + givenAuctionHasAllowlist + givenCallerIsOnAllowlist whenAccountHasQuoteTokenBalance(AMOUNT_IN) whenAccountHasBaseTokenBalance(AMOUNT_OUT) { - // Set the payout multiplier so that the payout is less than the minimum - mockAuctionModule.setPayoutMultiplier(lotId, 0); + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); - // Expect revert - bytes memory err = abi.encodeWithSelector(AuctionHouse.AmountLessThanMinimum.selector); - vm.expectRevert(err); + // Caller has no quote tokens + assertEq(quoteToken.balanceOf(alice), 0); + + // Caller has base tokens + assertEq(baseToken.balanceOf(alice), AMOUNT_OUT); + } + + // transfer quote token to auction house + // [X] when the permit2 signature is provided + // [X] it succeeds using Permit2 + // [X] when the permit2 signature is not provided + // [X] it succeeds using ERC20 transfer + + modifier givenQuoteTokenSpendingIsApproved() { + quoteToken.approve(address(auctionHouse), AMOUNT_IN); + _; + } + + function test_whenPermit2Signature() + external + whenAccountHasQuoteTokenBalance(AMOUNT_IN) + whenAccountHasBaseTokenBalance(AMOUNT_OUT) + { + // Set the permit2 signature + purchaseParams.approvalSignature = _signPermit( + address(quoteToken), + AMOUNT_IN, + approvalNonce, + approvalDeadline, + address(auctionHouse), + aliceKey + ); // Purchase vm.prank(alice); auctionHouse.purchase(purchaseParams); + + // Check balances + assertEq(quoteToken.balanceOf(address(auctionHouse)), AMOUNT_IN); + assertEq(quoteToken.balanceOf(alice), 0); + + // Ignore the rest } - function test_whenCallerHasInsufficientBalanceOfQuoteToken_reverts() + function test_whenNoPermit2Signature() external + givenQuoteTokenSpendingIsApproved + whenAccountHasQuoteTokenBalance(AMOUNT_IN) whenAccountHasBaseTokenBalance(AMOUNT_OUT) + { + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + + // Check balances + assertEq(quoteToken.balanceOf(address(auctionHouse)), AMOUNT_IN); + assertEq(quoteToken.balanceOf(alice), 0); + + // Ignore the rest + } + + // exchange of quote and base tokens + // [ ] given the auction has hooks defined + // [ ] when the mid hook reverts + // [ ] it reverts + // [ ] when the mid hook does not transfer enough base tokens to the auction house + // [ ] it reverts + // [ ] when the mid hook transfers enough base tokens to the auction house + // [ ] it succeeds - quote tokens (minus fees) transferred to the auction owner + // [ ] given the auction does not have hooks defined + // [ ] given that approval has not been given to the auction house to transfer base tokens + // [ ] it reverts + // [ ] given the received amount is less than the transferred amount + // [ ] it reverts + // [ ] given the received amount is the same as the transferred amount + // [ ] quote tokens (minus fees) are transferred to the auction owner + + // [ ] when the calculated payout amount is less than the minimum + // [ ] it reverts + + function test_whenOwnerHasInsufficientBalanceOfBaseToken_reverts() + external + whenAccountHasQuoteTokenBalance(AMOUNT_IN) { // Expect revert vm.expectRevert(); @@ -313,15 +392,36 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); } - function test_whenOwnerHasInsufficientBalanceOfBaseToken_reverts() + function test_whenPayoutAmountLessThanMinimum_reverts() external whenAccountHasQuoteTokenBalance(AMOUNT_IN) + whenAccountHasBaseTokenBalance(AMOUNT_OUT) { + // Set the payout multiplier so that the payout is less than the minimum + mockAuctionModule.setPayoutMultiplier(lotId, 0); + // Expect revert - vm.expectRevert(); + bytes memory err = abi.encodeWithSelector(AuctionHouse.AmountLessThanMinimum.selector); + vm.expectRevert(err); // Purchase vm.prank(alice); auctionHouse.purchase(purchaseParams); } + + // transfers base token from auction house to recipient + // [ ] given the base token is a derivative + // [ ] given a condenser is set + // [ ] it uses the condenser to determine derivative parameters + // [ ] given a condenser is not set + // [ ] it uses the routing derivative parameters + // [ ] it mints derivative tokens to the recipient using the derivative module + // [ ] given the base token is not a derivative + // [ ] it transfers the base token to the recipient + // + // records fees + // [ ] given that a protocol fee is defined + // [ ] it records the protocol fee + // [ ] given that a referrer fee is defined + // [ ] it records the referrer fee } diff --git a/test/Router/collectPayment.t.sol b/test/Router/collectPayment.t.sol index 188d0d3b..0a7dc2f4 100644 --- a/test/Router/collectPayment.t.sol +++ b/test/Router/collectPayment.t.sol @@ -89,13 +89,7 @@ contract RouterTest is Test, Permit2User { approvalNonce = _getRandomUint256(); approvalDeadline = uint48(block.timestamp + 1 days); approvalSignature = _signPermit( - IPermit2.PermitTransferFrom({ - permitted: IPermit2.TokenPermissions({token: address(quoteToken), amount: amount}), - nonce: approvalNonce, - deadline: approvalDeadline - }), - address(router), - userKey + address(quoteToken), amount, approvalNonce, approvalDeadline, address(router), userKey ); _; } @@ -122,17 +116,25 @@ contract RouterTest is Test, Permit2User { approvalNonce = _getRandomUint256(); approvalDeadline = uint48(block.timestamp + 1 days); approvalSignature = _signPermit( - IPermit2.PermitTransferFrom({ - permitted: IPermit2.TokenPermissions({token: address(quoteToken), amount: amount}), - nonce: approvalNonce, - deadline: approvalDeadline - }), + address(quoteToken), + amount, + approvalNonce, + approvalDeadline, address(router), anotherUserKey ); _; } + modifier whenPermit2ApprovalIsOtherSpender() { + approvalNonce = _getRandomUint256(); + approvalDeadline = uint48(block.timestamp + 1 days); + approvalSignature = _signPermit( + address(quoteToken), amount, approvalNonce, approvalDeadline, address(PROTOCOL), userKey + ); + _; + } + modifier whenPermit2ApprovalIsInvalid() { approvalNonce = _getRandomUint256(); approvalDeadline = uint48(block.timestamp + 1 days); @@ -144,18 +146,12 @@ contract RouterTest is Test, Permit2User { approvalNonce = _getRandomUint256(); approvalDeadline = uint48(block.timestamp - 1 days); approvalSignature = _signPermit( - IPermit2.PermitTransferFrom({ - permitted: IPermit2.TokenPermissions({token: address(quoteToken), amount: amount}), - nonce: approvalNonce, - deadline: approvalDeadline - }), - address(router), - userKey + address(quoteToken), amount, approvalNonce, approvalDeadline, address(router), userKey ); _; } - function test_permit2_noApproval_reverts() + function test_permit2_givenNoTokenApproval_reverts() public givenUserHasBalance(amount) whenPermit2ApprovalIsValid @@ -173,7 +169,7 @@ contract RouterTest is Test, Permit2User { ); } - function test_permit2_reusedSignature_reverts() + function test_permit2_whenApprovalSignatureIsReused_reverts() public givenUserHasBalance(amount) givenPermit2Approved @@ -191,7 +187,7 @@ contract RouterTest is Test, Permit2User { ); } - function test_permit2_invalidSignature_reverts() + function test_permit2_whenApprovalSignatureIsInvalid_reverts() public givenUserHasBalance(amount) givenPermit2Approved @@ -208,7 +204,7 @@ contract RouterTest is Test, Permit2User { ); } - function test_permit2_expiredSignature_reverts() + function test_permit2_whenApprovalSignatureIsExpired_reverts() public givenUserHasBalance(amount) givenPermit2Approved @@ -226,7 +222,7 @@ contract RouterTest is Test, Permit2User { ); } - function test_permit2_otherSigner_reverts() + function test_permit2_whenApprovalSignatureBelongsToOtherSigner_reverts() public givenUserHasBalance(amount) givenPermit2Approved @@ -243,7 +239,24 @@ contract RouterTest is Test, Permit2User { ); } - function test_permit2_insufficientBalance_reverts() + function test_permit2_whenApprovalSignatureBelongsToOtherSpender_reverts() + public + givenUserHasBalance(amount) + givenPermit2Approved + whenPermit2ApprovalIsOtherSpender + { + // Expect the error + bytes memory err = abi.encodeWithSelector(Permit2Clone.InvalidSigner.selector); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayment( + lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature + ); + } + + function test_permit2_whenUserHasInsufficientBalance_reverts() public givenPermit2Approved whenPermit2ApprovalIsValid @@ -260,7 +273,7 @@ contract RouterTest is Test, Permit2User { ); } - function test_permit2_feeOnTransfer_reverts() + function test_permit2_givenTokenTakesFeeOnTransfer_reverts() public givenUserHasBalance(amount) givenTokenTakesFeeOnTransfer @@ -311,7 +324,7 @@ contract RouterTest is Test, Permit2User { // [X] given the received amount is the same as the transferred amount // [X] quote tokens are transferred from the caller to the auction owner - function test_transfer_insufficientBalance_reverts() public { + function test_transfer_whenUserHasInsufficientBalance_reverts() public { // Expect the error bytes memory err = abi.encodeWithSelector(Router.InsufficientBalance.selector, address(quoteToken), amount); @@ -324,7 +337,7 @@ contract RouterTest is Test, Permit2User { ); } - function test_transfer_noApproval_reverts() public givenUserHasBalance(amount) { + function test_transfer_givenNoTokenApproval_reverts() public givenUserHasBalance(amount) { // Expect the error bytes memory err = abi.encodeWithSelector( Router.InsufficientAllowance.selector, address(quoteToken), address(router), amount @@ -338,7 +351,7 @@ contract RouterTest is Test, Permit2User { ); } - function test_transfer_feeOnTransfer_reverts() + function test_transfer_givenTokenTakesFeeOnTransfer_reverts() public givenUserHasBalance(amount) givenUserHasApprovedRouter @@ -407,7 +420,7 @@ contract RouterTest is Test, Permit2User { ); } - function test_preHook_transfer() + function test_preHook_withTransfer() public givenUserHasBalance(amount) givenUserHasApprovedRouter @@ -425,7 +438,7 @@ contract RouterTest is Test, Permit2User { assertEq(quoteToken.balanceOf(USER), 0); } - function test_preHook_permit2() + function test_preHook_withPermit2() public givenUserHasBalance(amount) givenPermit2Approved diff --git a/test/lib/permit2/Permit2User.sol b/test/lib/permit2/Permit2User.sol index 0b7b8989..776959e8 100644 --- a/test/lib/permit2/Permit2User.sol +++ b/test/lib/permit2/Permit2User.sol @@ -37,10 +37,18 @@ contract Permit2User is Test { // Generate a signature for a permit message. function _signPermit( - IPermit2.PermitTransferFrom memory permit, + address token_, + uint256 amount_, + uint256 nonce_, + uint256 deadline_, address spender, uint256 signerKey ) internal view returns (bytes memory sig) { + IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({token: token_, amount: amount_}), + nonce: nonce_, + deadline: deadline_ + }); (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, _getEIP712Hash(permit, spender)); return abi.encodePacked(r, s, v); } diff --git a/test/modules/Auction/MockAllowlist.sol b/test/modules/Auction/MockAllowlist.sol index 44a7a1d6..431a2d7a 100644 --- a/test/modules/Auction/MockAllowlist.sol +++ b/test/modules/Auction/MockAllowlist.sol @@ -8,9 +8,19 @@ contract MockAllowlist is IAllowlist { uint256[] public registeredIds; - function isAllowed(address, bytes calldata) external view override returns (bool) {} + mapping(address => bool) public allowed; - function isAllowed(uint256, address, bytes calldata) external view override returns (bool) {} + function isAllowed(address address_, bytes calldata) external view override returns (bool) { + return allowed[address_]; + } + + function isAllowed( + uint256, + address address_, + bytes calldata + ) external view override returns (bool) { + return allowed[address_]; + } function register(bytes calldata) external override {} @@ -29,4 +39,8 @@ contract MockAllowlist is IAllowlist { function getRegisteredIds() external view returns (uint256[] memory) { return registeredIds; } + + function setAllowed(address account, bool allowed_) external { + allowed[account] = allowed_; + } } From 601180ab7d87505c6612d5f1b2d1823bd1162ed2 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 15 Jan 2024 17:56:44 +0400 Subject: [PATCH 25/82] WIP tests for _collectPayout --- test/Router/ConcreteRouter.sol | 9 +++ test/Router/collectPayment.t.sol | 12 ++- test/Router/collectPayout.t.sol | 119 ++++++++++++++++++++++++++++++ test/modules/Auction/MockHook.sol | 19 +++++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 test/Router/collectPayout.t.sol diff --git a/test/Router/ConcreteRouter.sol b/test/Router/ConcreteRouter.sol index 53458658..d2902bb8 100644 --- a/test/Router/ConcreteRouter.sol +++ b/test/Router/ConcreteRouter.sol @@ -55,4 +55,13 @@ contract ConcreteRouter is Router { approvalSignature_ ); } + + function collectPayout( + uint256 lotId_, + uint256 amount_, + ERC20 payoutToken_, + IHooks hooks_ + ) external { + return _collectPayout(lotId_, amount_, payoutToken_, hooks_); + } } diff --git a/test/Router/collectPayment.t.sol b/test/Router/collectPayment.t.sol index 0a7dc2f4..091d101f 100644 --- a/test/Router/collectPayment.t.sol +++ b/test/Router/collectPayment.t.sol @@ -13,7 +13,7 @@ import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; import {Router} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; -contract RouterTest is Test, Permit2User { +contract CollectPaymentTest is Test, Permit2User { ConcreteRouter internal router; address internal constant PROTOCOL = address(0x1); @@ -434,8 +434,13 @@ contract RouterTest is Test, Permit2User { ); // Expect the pre hook to have recorded the balance of USER before the transfer + assertEq(hook.preHookCalled(), true); assertEq(hook.preHookBalance(), amount); assertEq(quoteToken.balanceOf(USER), 0); + + // Ensure that the mid and post hooks were not called + assertEq(hook.midHookCalled(), false); + assertEq(hook.postHookCalled(), false); } function test_preHook_withPermit2() @@ -453,7 +458,12 @@ contract RouterTest is Test, Permit2User { ); // Expect the pre hook to have recorded the balance of USER before the transfer + assertEq(hook.preHookCalled(), true); assertEq(hook.preHookBalance(), amount); assertEq(quoteToken.balanceOf(USER), 0); + + // Ensure that the mid and post hooks were not called + assertEq(hook.midHookCalled(), false); + assertEq(hook.postHookCalled(), false); } } diff --git a/test/Router/collectPayout.t.sol b/test/Router/collectPayout.t.sol new file mode 100644 index 00000000..2790d82f --- /dev/null +++ b/test/Router/collectPayout.t.sol @@ -0,0 +1,119 @@ +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; + +import {MockHook} from "test/modules/Auction/MockHook.sol"; +import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; +import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; +import {Permit2Clone} from "test/lib/permit2/Permit2Clone.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; +import {Router} from "src/AuctionHouse.sol"; +import {IHooks} from "src/interfaces/IHooks.sol"; + +contract CollectPayoutTest is Test, Permit2User { + ConcreteRouter internal router; + + address internal constant PROTOCOL = address(0x1); + + address internal USER = address(0x2); + address internal OWNER = address(0x3); + + // Function parameters + uint256 internal lotId = 1; + uint256 internal amount = 10e18; + MockFeeOnTransferERC20 internal payoutToken; + MockHook internal hook; + + function setUp() public { + // Set reasonable starting block + vm.warp(1_000_000); + + router = new ConcreteRouter(PROTOCOL, _PERMIT2_ADDRESS); + + payoutToken = new MockFeeOnTransferERC20("Payout Token", "PAYOUT", 18); + payoutToken.setTransferFee(0); + } + + modifier givenOwnerHasBalance(uint256 amount_) { + payoutToken.mint(OWNER, amount_); + _; + } + + modifier givenOwnerHasApprovedRouter() { + vm.prank(OWNER); + payoutToken.approve(address(router), type(uint256).max); + _; + } + + modifier givenTokenTakesFeeOnTransfer() { + payoutToken.setTransferFee(1e18); + _; + } + + // ========== Hooks flow ========== // + + // [ ] given the auction has hooks defined + // [X] when the mid hook reverts + // [X] it reverts + // [ ] when the mid hook does not revert + // [ ] given the invariant is violated + // [ ] it reverts + // [X] given the invariant is not violated - TODO define invariant + // [X] it succeeds + + modifier givenAuctionHasHook() { + hook = new MockHook(); + _; + } + + modifier givenMidHookReverts() { + hook.setMidHookReverts(true); + _; + } + + modifier whenMidHookBalanceIsRecorded() { + hook.setMidHookValues(address(payoutToken), OWNER); + _; + } + + function test_givenAuctionHasHook_whenMidHookReverts_reverts() + public + givenAuctionHasHook + givenMidHookReverts + { + // Expect revert + vm.expectRevert("revert"); + + // Call + vm.prank(USER); + router.collectPayout(lotId, amount, payoutToken, hook); + } + + function test_givenAuctionHasHook() + public + givenAuctionHasHook + givenOwnerHasBalance(amount) + givenOwnerHasApprovedRouter + whenMidHookBalanceIsRecorded + { + // Call + vm.prank(USER); + router.collectPayout(lotId, amount, payoutToken, hook); + + // Expect payout token balance to be transferred to the router + assertEq(payoutToken.balanceOf(address(router)), amount); + assertEq(payoutToken.balanceOf(OWNER), 0); + assertEq(payoutToken.balanceOf(address(hook)), 0); + + // Expect the hook to be called + assertEq(hook.midHookCalled(), true); + assertEq(hook.midHookBalance(), amount); + + // Expect the other hooks not to be called + assertEq(hook.preHookCalled(), false); + assertEq(hook.postHookCalled(), false); + } +} diff --git a/test/modules/Auction/MockHook.sol b/test/modules/Auction/MockHook.sol index fd43c908..3129c6f5 100644 --- a/test/modules/Auction/MockHook.sol +++ b/test/modules/Auction/MockHook.sol @@ -8,17 +8,30 @@ import {IHooks} from "src/bases/Auctioneer.sol"; contract MockHook is IHooks { address public preHookToken; address public preHookUser; + + /// @notice Use this to determine if the hook was called at the right time uint256 public preHookBalance; + + /// @notice Use this to determine if the hook was called + bool public preHookCalled; bool public preHookReverts; address public midHookToken; address public midHookUser; + /// @notice Use this to determine if the hook was called at the right time uint256 public midHookBalance; + + /// @notice Use this to determine if the hook was called + bool public midHookCalled; bool public midHookReverts; address public postHookToken; address public postHookUser; + /// @notice Use this to determine if the hook was called at the right time uint256 public postHookBalance; + + /// @notice Use this to determine if the hook was called + bool public postHookCalled; bool public postHookReverts; function pre(uint256, uint256) external override { @@ -26,6 +39,8 @@ contract MockHook is IHooks { revert("revert"); } + preHookCalled = true; + if (preHookToken != address(0) && preHookUser != address(0)) { preHookBalance = ERC20(preHookToken).balanceOf(preHookUser); } else { @@ -47,6 +62,8 @@ contract MockHook is IHooks { revert("revert"); } + midHookCalled = true; + if (midHookToken != address(0) && midHookUser != address(0)) { midHookBalance = ERC20(midHookToken).balanceOf(midHookUser); } else { @@ -68,6 +85,8 @@ contract MockHook is IHooks { revert("revert"); } + postHookCalled = true; + if (postHookToken != address(0) && postHookUser != address(0)) { postHookBalance = ERC20(postHookToken).balanceOf(postHookUser); } else { From 30f757d5ed8578ae3c1635cac3ae492366a3746e Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 16 Jan 2024 15:24:42 +0400 Subject: [PATCH 26/82] More test TODOs. Implement _collectPayout(). --- src/AuctionHouse.sol | 71 +++++++++++- test/AuctionHouse/auction.t.sol | 2 +- test/AuctionHouse/purchase.t.sol | 38 +++--- test/Router/ConcreteRouter.sol | 7 +- test/Router/collectPayment.t.sol | 21 ++-- test/Router/collectPayout.t.sol | 184 ++++++++++++++++++++++++++---- test/modules/Auction/MockHook.sol | 113 ++++++++++++------ 7 files changed, 349 insertions(+), 87 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index bff4c423..81a16a9c 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -36,6 +36,8 @@ abstract contract Router is FeeManager { error UnsupportedToken(address token_); + error InvalidHook(); + // ========== STRUCTS ========== // /// @notice Parameters used by the purchase function @@ -141,6 +143,7 @@ abstract contract Router is FeeManager { /// - The quote token transfer fails /// - Transferring the quote token would result in a lesser amount being received /// - The pre-hook reverts + /// - TODO: The pre-hook invariant is violated /// /// @param lotId_ Lot ID /// @param amount_ Amount of quoteToken to collect (in native decimals) @@ -180,6 +183,73 @@ abstract contract Router is FeeManager { } } + /// @notice Collects the payout token from the auction owner + /// @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 + /// + /// This function reverts if: + /// - Approval has not been granted to transfer the payout token + /// - The auction owner does not have sufficient balance of the payout token + /// - The payout token transfer fails + /// - Transferring the payout token would result in a lesser amount being received + /// - The mid-hook reverts + /// - The mid-hook invariant is violated + /// + /// @param lotId_ Lot ID + /// @param lotOwner_ Owner of the lot + /// @param paymentAmount_ Amount of quoteToken collected (in native decimals) + /// @param payoutAmount_ Amount of payoutToken to collect (in native decimals) + /// @param payoutToken_ Payout token to collect + /// @param hooks_ Hooks contract to call (optional) + function _collectPayout( + uint256 lotId_, + address lotOwner_, + uint256 paymentAmount_, + uint256 payoutAmount_, + ERC20 payoutToken_, + IHooks hooks_ + ) internal { + // Get the balance of the payout token before the transfer + uint256 balanceBefore = payoutToken_.balanceOf(address(this)); + + // Call mid hook on hooks contract if provided + if (address(hooks_) != address(0)) { + // The mid hook is expected to transfer the payout token to this contract + hooks_.mid(lotId_, paymentAmount_, payoutAmount_); + + // Check that the mid hook transferred the expected amount of payout tokens + if (payoutToken_.balanceOf(address(this)) < balanceBefore + payoutAmount_) { + revert InvalidHook(); + } + } + // Otherwise fallback to a standard ERC20 transfer + else { + // Check that the auction owner has sufficient balance of the payout token + if (payoutToken_.balanceOf(lotOwner_) < payoutAmount_) { + revert InsufficientBalance(address(payoutToken_), payoutAmount_); + } + + // Check that the auction owner has granted approval to transfer the payout token + if (payoutToken_.allowance(lotOwner_, address(this)) < payoutAmount_) { + revert InsufficientAllowance(address(payoutToken_), address(this), payoutAmount_); + } + + // Transfer the payout token from the auction owner + // `safeTransferFrom()` will revert upon failure + payoutToken_.safeTransferFrom(lotOwner_, address(this), payoutAmount_); + + // Check that it is not a fee-on-transfer token + if (payoutToken_.balanceOf(address(this)) < balanceBefore + payoutAmount_) { + revert UnsupportedToken(address(payoutToken_)); + } + } + } + + // TODO sendPayout + + // TODO sendPayment + /// @notice Performs an ERC20 transfer of `token_` from the caller /// @dev This function handles the following: /// 1. Checks that the user has granted approval to transfer the token @@ -269,7 +339,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== ERRORS ========== // error AmountLessThanMinimum(); - error InvalidHook(); error NotAuthorized(); diff --git a/test/AuctionHouse/auction.t.sol b/test/AuctionHouse/auction.t.sol index 26149a2b..501b0bc4 100644 --- a/test/AuctionHouse/auction.t.sol +++ b/test/AuctionHouse/auction.t.sol @@ -54,7 +54,7 @@ contract AuctionTest is Test, Permit2User { mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); mockCondenserModule = new MockCondenserModule(address(auctionHouse)); mockAllowlist = new MockAllowlist(); - mockHook = new MockHook(); + mockHook = new MockHook(address(quoteToken), address(baseToken)); auctionParams = Auction.AuctionParams({ start: uint48(block.timestamp), diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index ffee1478..87429c74 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -76,7 +76,7 @@ contract PurchaseTest is Test, Permit2User { mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); mockCondenserModule = new MockCondenserModule(address(auctionHouse)); mockAllowlist = new MockAllowlist(); - mockHook = new MockHook(); + mockHook = new MockHook(address(quoteToken), address(baseToken)); auctionParams = Auction.AuctionParams({ start: uint48(block.timestamp), @@ -361,24 +361,28 @@ contract PurchaseTest is Test, Permit2User { // Ignore the rest } - // exchange of quote and base tokens + // transfer quote token to auction owner // [ ] given the auction has hooks defined - // [ ] when the mid hook reverts - // [ ] it reverts - // [ ] when the mid hook does not transfer enough base tokens to the auction house - // [ ] it reverts - // [ ] when the mid hook transfers enough base tokens to the auction house - // [ ] it succeeds - quote tokens (minus fees) transferred to the auction owner + // [ ] the quote token is transferred to the hook before the hook is called // [ ] given the auction does not have hooks defined - // [ ] given that approval has not been given to the auction house to transfer base tokens - // [ ] it reverts - // [ ] given the received amount is less than the transferred amount - // [ ] it reverts - // [ ] given the received amount is the same as the transferred amount - // [ ] quote tokens (minus fees) are transferred to the auction owner - - // [ ] when the calculated payout amount is less than the minimum - // [ ] it reverts + // [ ] the quote token is transferred to the auction owner + + // transfer payout token to router + // [ ] given the payout token is not a derivative + // [ ] given the auction has hooks defined + // [ ] the payout token is tranferred by the hook to the router + // [ ] given the auction does not have hooks defined + // [ ] the payout token is transferred to the router + + // transfer payout token to recipient + // [ ] given the payout token is not a derivative + // [ ] when the recipient is different to the caller + // [ ] the payout token is transferred from the router to the recipient + // [ ] given the payout token is a derivative + // [ ] when the recipient is different to the caller + // [ ] it mints a derivative to the recipient + + // TODO check invariants for entire flow function test_whenOwnerHasInsufficientBalanceOfBaseToken_reverts() external diff --git a/test/Router/ConcreteRouter.sol b/test/Router/ConcreteRouter.sol index d2902bb8..39db202b 100644 --- a/test/Router/ConcreteRouter.sol +++ b/test/Router/ConcreteRouter.sol @@ -58,10 +58,13 @@ contract ConcreteRouter is Router { function collectPayout( uint256 lotId_, - uint256 amount_, + address lotOwner_, + uint256 paymentAmount_, + uint256 payoutAmount_, ERC20 payoutToken_, IHooks hooks_ ) external { - return _collectPayout(lotId_, amount_, payoutToken_, hooks_); + return + _collectPayout(lotId_, lotOwner_, paymentAmount_, payoutAmount_, payoutToken_, hooks_); } } diff --git a/test/Router/collectPayment.t.sol b/test/Router/collectPayment.t.sol index 091d101f..631a5eec 100644 --- a/test/Router/collectPayment.t.sol +++ b/test/Router/collectPayment.t.sol @@ -395,7 +395,15 @@ contract CollectPaymentTest is Test, Permit2User { // [X] it succeeds modifier whenHooksIsSet() { - hook = new MockHook(); + hook = new MockHook(address(quoteToken), address(0)); + + // Set the addresses to track + address[] memory addresses = new address[](3); + addresses[0] = USER; + addresses[1] = address(router); + addresses[2] = address(hook); + + hook.setBalanceAddresses(addresses); _; } @@ -404,11 +412,6 @@ contract CollectPaymentTest is Test, Permit2User { _; } - modifier whenPreHookBalanceIsRecorded() { - hook.setPreHookValues(address(quoteToken), USER); - _; - } - function test_preHook_reverts() public whenHooksIsSet whenPreHookReverts { // Expect the error vm.expectRevert("revert"); @@ -425,7 +428,6 @@ contract CollectPaymentTest is Test, Permit2User { givenUserHasBalance(amount) givenUserHasApprovedRouter whenHooksIsSet - whenPreHookBalanceIsRecorded { // Call vm.prank(USER); @@ -435,7 +437,7 @@ contract CollectPaymentTest is Test, Permit2User { // Expect the pre hook to have recorded the balance of USER before the transfer assertEq(hook.preHookCalled(), true); - assertEq(hook.preHookBalance(), amount); + assertEq(hook.preHookBalances(quoteToken, USER), amount); assertEq(quoteToken.balanceOf(USER), 0); // Ensure that the mid and post hooks were not called @@ -449,7 +451,6 @@ contract CollectPaymentTest is Test, Permit2User { givenPermit2Approved whenPermit2ApprovalIsValid whenHooksIsSet - whenPreHookBalanceIsRecorded { // Call vm.prank(USER); @@ -459,7 +460,7 @@ contract CollectPaymentTest is Test, Permit2User { // Expect the pre hook to have recorded the balance of USER before the transfer assertEq(hook.preHookCalled(), true); - assertEq(hook.preHookBalance(), amount); + assertEq(hook.preHookBalances(quoteToken, USER), amount); assertEq(quoteToken.balanceOf(USER), 0); // Ensure that the mid and post hooks were not called diff --git a/test/Router/collectPayout.t.sol b/test/Router/collectPayout.t.sol index 2790d82f..ae6fe9cf 100644 --- a/test/Router/collectPayout.t.sol +++ b/test/Router/collectPayout.t.sol @@ -23,7 +23,8 @@ contract CollectPayoutTest is Test, Permit2User { // Function parameters uint256 internal lotId = 1; - uint256 internal amount = 10e18; + uint256 internal paymentAmount = 1e18; + uint256 internal payoutAmount = 10e18; MockFeeOnTransferERC20 internal payoutToken; MockHook internal hook; @@ -49,23 +50,32 @@ contract CollectPayoutTest is Test, Permit2User { } modifier givenTokenTakesFeeOnTransfer() { - payoutToken.setTransferFee(1e18); + payoutToken.setTransferFee(1000); _; } // ========== Hooks flow ========== // - // [ ] given the auction has hooks defined + // [X] given the auction has hooks defined // [X] when the mid hook reverts // [X] it reverts - // [ ] when the mid hook does not revert - // [ ] given the invariant is violated - // [ ] it reverts - // [X] given the invariant is not violated - TODO define invariant + // [X] when the mid hook does not revert + // [X] given the invariant is violated + // [X] it reverts + // [X] given the invariant is not violated // [X] it succeeds modifier givenAuctionHasHook() { - hook = new MockHook(); + hook = new MockHook(address(0), address(payoutToken)); + + // Set the addresses to track + address[] memory addresses = new address[](4); + addresses[0] = USER; + addresses[1] = OWNER; + addresses[2] = address(router); + addresses[3] = address(hook); + + hook.setBalanceAddresses(addresses); _; } @@ -74,8 +84,19 @@ contract CollectPayoutTest is Test, Permit2User { _; } - modifier whenMidHookBalanceIsRecorded() { - hook.setMidHookValues(address(payoutToken), OWNER); + modifier whenMidHookBreaksInvariant() { + hook.setMidHookMultiplier(9000); + _; + } + + modifier givenHookHasBalance(uint256 amount_) { + payoutToken.mint(address(hook), amount_); + _; + } + + modifier givenHookHasApprovedRouter() { + vm.prank(address(hook)); + payoutToken.approve(address(router), type(uint256).max); _; } @@ -89,31 +110,150 @@ contract CollectPayoutTest is Test, Permit2User { // Call vm.prank(USER); - router.collectPayout(lotId, amount, payoutToken, hook); + router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); } - function test_givenAuctionHasHook() + function test_givenAuctionHasHook_whenMidHookBreaksInvariant_reverts() public givenAuctionHasHook - givenOwnerHasBalance(amount) - givenOwnerHasApprovedRouter - whenMidHookBalanceIsRecorded + givenHookHasBalance(payoutAmount) + givenHookHasApprovedRouter + whenMidHookBreaksInvariant { + // Expect revert + bytes memory err = abi.encodeWithSelector(Router.InvalidHook.selector); + vm.expectRevert(err); + // Call vm.prank(USER); - router.collectPayout(lotId, amount, payoutToken, hook); + router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + } - // Expect payout token balance to be transferred to the router - assertEq(payoutToken.balanceOf(address(router)), amount); - assertEq(payoutToken.balanceOf(OWNER), 0); - assertEq(payoutToken.balanceOf(address(hook)), 0); + function test_givenAuctionHasHook_feeOnTransfer_reverts() + public + givenAuctionHasHook + givenHookHasBalance(payoutAmount) + givenHookHasApprovedRouter + givenTokenTakesFeeOnTransfer + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Router.InvalidHook.selector); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + } + + function test_givenAuctionHasHook() + public + givenAuctionHasHook + givenHookHasBalance(payoutAmount) + givenHookHasApprovedRouter + { + // Call + vm.prank(USER); + router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); - // Expect the hook to be called + // Expect the hook to be called prior to any transfer of the payout token assertEq(hook.midHookCalled(), true); - assertEq(hook.midHookBalance(), amount); + assertEq(hook.midHookBalances(payoutToken, OWNER), 0, "mid-hook: owner balance mismatch"); + assertEq(hook.midHookBalances(payoutToken, USER), 0, "mid-hook: user balance mismatch"); + assertEq( + hook.midHookBalances(payoutToken, address(router)), + 0, + "mid-hook: router balance mismatch" + ); + assertEq( + hook.midHookBalances(payoutToken, address(hook)), + payoutAmount, + "mid-hook: hook balance mismatch" + ); // Expect the other hooks not to be called assertEq(hook.preHookCalled(), false); assertEq(hook.postHookCalled(), false); + + // Expect payout token balance to be transferred to the router + assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); + assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); + assertEq(payoutToken.balanceOf(address(router)), payoutAmount, "router balance mismatch"); + assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); + } + + // ========== Non-hooks flow ========== // + + // [X] given the auction does not have hooks defined + // [X] given the auction owner has insufficient balance of the payout token + // [X] it reverts + // [X] given the auction owner has not approved the router to transfer the payout token + // [X] it reverts + // [X] given transferring the payout token would result in a lesser amount being received + // [X] it reverts + // [X] it succeeds + + function test_insufficientBalance_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector( + Router.InsufficientBalance.selector, address(payoutToken), payoutAmount + ); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + } + + function test_insufficientAllowance_reverts() public givenOwnerHasBalance(payoutAmount) { + // Expect revert + bytes memory err = abi.encodeWithSelector( + Router.InsufficientAllowance.selector, + address(payoutToken), + address(router), + payoutAmount + ); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + } + + function test_feeOnTransfer_reverts() + public + givenOwnerHasBalance(payoutAmount) + givenOwnerHasApprovedRouter + givenTokenTakesFeeOnTransfer + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(Router.UnsupportedToken.selector, address(payoutToken)); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + } + + function test_success() public givenOwnerHasBalance(payoutAmount) givenOwnerHasApprovedRouter { + // Call + vm.prank(USER); + router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + + // Expect payout token balance to be transferred to the router + assertEq(payoutToken.balanceOf(OWNER), 0); + assertEq(payoutToken.balanceOf(USER), 0); + assertEq(payoutToken.balanceOf(address(router)), payoutAmount); + assertEq(payoutToken.balanceOf(address(hook)), 0); } + + // ========== Derivative flow ========== // + + // [ ] given the auction has a derivative defined + // [ ] given the auction has hooks defined + // [ ] given the hook breaks the invariant + // [ ] it reverts + // [ ] it succeeds - derivative is minted to the router, mid hook is called before minting + // [ ] given the auction does not have hooks defined + // [ ] it succeeds - derivative is minted to the router } diff --git a/test/modules/Auction/MockHook.sol b/test/modules/Auction/MockHook.sol index 3129c6f5..6608f46a 100644 --- a/test/modules/Auction/MockHook.sol +++ b/test/modules/Auction/MockHook.sol @@ -5,35 +5,50 @@ import {ERC20} from "solmate/tokens/ERC20.sol"; import {IHooks} from "src/bases/Auctioneer.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; + contract MockHook is IHooks { - address public preHookToken; - address public preHookUser; + using SafeTransferLib for ERC20; + + ERC20 public quoteToken; + ERC20 public payoutToken; + ERC20[] public tokens; + + address[] public balanceAddresses; /// @notice Use this to determine if the hook was called at the right time - uint256 public preHookBalance; + mapping(ERC20 token_ => mapping(address user_ => uint256 balance_)) public preHookBalances; /// @notice Use this to determine if the hook was called bool public preHookCalled; bool public preHookReverts; - address public midHookToken; - address public midHookUser; /// @notice Use this to determine if the hook was called at the right time - uint256 public midHookBalance; + mapping(ERC20 token_ => mapping(address user_ => uint256 balance_)) public midHookBalances; /// @notice Use this to determine if the hook was called bool public midHookCalled; bool public midHookReverts; + uint256 public midHookMultiplier; - address public postHookToken; - address public postHookUser; /// @notice Use this to determine if the hook was called at the right time - uint256 public postHookBalance; + mapping(ERC20 token_ => mapping(address user_ => uint256 balance_)) public postHookBalances; /// @notice Use this to determine if the hook was called bool public postHookCalled; bool public postHookReverts; + constructor(address quoteToken_, address payoutToken_) { + quoteToken = ERC20(quoteToken_); + payoutToken = ERC20(payoutToken_); + + tokens = new ERC20[](2); + tokens[0] = quoteToken; + tokens[1] = payoutToken; + + midHookMultiplier = 10_000; + } + function pre(uint256, uint256) external override { if (preHookReverts) { revert("revert"); @@ -41,45 +56,58 @@ contract MockHook is IHooks { preHookCalled = true; - if (preHookToken != address(0) && preHookUser != address(0)) { - preHookBalance = ERC20(preHookToken).balanceOf(preHookUser); - } else { - preHookBalance = 0; + // Iterate over tokens and balance addresses + for (uint256 i = 0; i < tokens.length; i++) { + ERC20 token = tokens[i]; + if (address(token) == address(0)) { + continue; + } + + for (uint256 j = 0; j < balanceAddresses.length; j++) { + preHookBalances[token][balanceAddresses[j]] = token.balanceOf(balanceAddresses[j]); + } } - } - function setPreHookValues(address token_, address user_) external { - preHookToken = token_; - preHookUser = user_; + // Does nothing at the moment } function setPreHookReverts(bool reverts_) external { preHookReverts = reverts_; } - function mid(uint256, uint256, uint256) external override { + function mid(uint256, uint256, uint256 payout_) external override { if (midHookReverts) { revert("revert"); } midHookCalled = true; - if (midHookToken != address(0) && midHookUser != address(0)) { - midHookBalance = ERC20(midHookToken).balanceOf(midHookUser); - } else { - midHookBalance = 0; + // Iterate over tokens and balance addresses + for (uint256 i = 0; i < tokens.length; i++) { + ERC20 token = tokens[i]; + if (address(token) == address(0)) { + continue; + } + + for (uint256 j = 0; j < balanceAddresses.length; j++) { + midHookBalances[token][balanceAddresses[j]] = token.balanceOf(balanceAddresses[j]); + } } - } - function setMidHookValues(address token_, address user_) external { - midHookToken = token_; - midHookUser = user_; + uint256 actualPayout = payout_ * midHookMultiplier / 10_000; + + // Has to transfer the payout token to the router + ERC20(payoutToken).safeTransfer(msg.sender, actualPayout); } function setMidHookReverts(bool reverts_) external { midHookReverts = reverts_; } + function setMidHookMultiplier(uint256 multiplier_) external { + midHookMultiplier = multiplier_; + } + function post(uint256, uint256) external override { if (postHookReverts) { revert("revert"); @@ -87,19 +115,36 @@ contract MockHook is IHooks { postHookCalled = true; - if (postHookToken != address(0) && postHookUser != address(0)) { - postHookBalance = ERC20(postHookToken).balanceOf(postHookUser); - } else { - postHookBalance = 0; + // Iterate over tokens and balance addresses + for (uint256 i = 0; i < tokens.length; i++) { + ERC20 token = tokens[i]; + if (address(token) == address(0)) { + continue; + } + + for (uint256 j = 0; j < balanceAddresses.length; j++) { + midHookBalances[token][balanceAddresses[j]] = token.balanceOf(balanceAddresses[j]); + } } - } - function setPostHookValues(address token_, address user_) external { - postHookToken = token_; - postHookUser = user_; + // Does nothing at the moment } function setPostHookReverts(bool reverts_) external { postHookReverts = reverts_; } + + function setBalanceAddresses(address[] memory addresses_) external { + for (uint256 i = 0; i < addresses_.length; i++) { + balanceAddresses.push(addresses_[i]); + } + } + + function setQuoteToken(address quoteToken_) external { + quoteToken = ERC20(quoteToken_); + } + + function setPayoutToken(address payoutToken_) external { + payoutToken = ERC20(payoutToken_); + } } From 9cd7ec103e6536bb17ad4d526fc8aef257b4db5d Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 16 Jan 2024 15:47:08 +0400 Subject: [PATCH 27/82] Cleanup --- src/AuctionHouse.sol | 2 ++ test/Router/collectPayout.t.sol | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 81a16a9c..79c4081b 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -244,6 +244,8 @@ abstract contract Router is FeeManager { revert UnsupportedToken(address(payoutToken_)); } } + + // TODO handle derivative } // TODO sendPayout diff --git a/test/Router/collectPayout.t.sol b/test/Router/collectPayout.t.sol index ae6fe9cf..776133d0 100644 --- a/test/Router/collectPayout.t.sol +++ b/test/Router/collectPayout.t.sol @@ -6,10 +6,8 @@ import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; -import {Permit2Clone} from "test/lib/permit2/Permit2Clone.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; -import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; import {Router} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; From 32e6cb7cf4d5255fe2af695aaf31d59e2292a3ad Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 16 Jan 2024 15:47:23 +0400 Subject: [PATCH 28/82] sendPayment() --- src/AuctionHouse.sol | 29 +++++++++++ test/Router/ConcreteRouter.sol | 9 ++++ test/Router/sendPayment.t.sol | 93 ++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 test/Router/sendPayment.t.sol diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 79c4081b..8f94fd8e 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -183,6 +183,35 @@ abstract contract Router is FeeManager { } } + /// @notice Sends payment of the quote token to the auction owner + /// @dev This function handles the following: + /// 1. Sends the payment amount to the auction owner or hook (if provided) + /// This function assumes: + /// - The quote token has already been transferred to this contract + /// - The quote token is supported (e.g. not fee-on-transfer) + /// + /// This function reverts if: + /// - The transfer fails + /// + /// @param lotOwner_ Owner of the lot + /// @param amount_ Amount of quoteToken to send (in native decimals) + /// @param quoteToken_ Quote token to send + /// @param hooks_ Hooks contract to call (optional) + function _sendPayment( + address lotOwner_, + uint256 amount_, + ERC20 quoteToken_, + IHooks hooks_ + ) internal { + if (address(hooks_) != address(0)) { + // Send quote token to hooks contract + quoteToken_.safeTransfer(address(hooks_), amount_); + } else { + // Send quote token to auction owner + quoteToken_.safeTransfer(lotOwner_, amount_); + } + } + /// @notice Collects the payout token from the auction owner /// @dev This function handles the following: /// 1. Calls the mid hook on the hooks contract (if provided) diff --git a/test/Router/ConcreteRouter.sol b/test/Router/ConcreteRouter.sol index 39db202b..906c2572 100644 --- a/test/Router/ConcreteRouter.sol +++ b/test/Router/ConcreteRouter.sol @@ -67,4 +67,13 @@ contract ConcreteRouter is Router { return _collectPayout(lotId_, lotOwner_, paymentAmount_, payoutAmount_, payoutToken_, hooks_); } + + function sendPayment( + address lotOwner_, + uint256 paymentAmount_, + ERC20 quoteToken_, + IHooks hooks_ + ) external { + return _sendPayment(lotOwner_, paymentAmount_, quoteToken_, hooks_); + } } diff --git a/test/Router/sendPayment.t.sol b/test/Router/sendPayment.t.sol new file mode 100644 index 00000000..b5851627 --- /dev/null +++ b/test/Router/sendPayment.t.sol @@ -0,0 +1,93 @@ +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; + +import {MockHook} from "test/modules/Auction/MockHook.sol"; +import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; +import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +import {Router} from "src/AuctionHouse.sol"; +import {IHooks} from "src/interfaces/IHooks.sol"; + +contract SendPaymentTest is Test, Permit2User { + ConcreteRouter internal router; + + address internal constant PROTOCOL = address(0x1); + + address internal USER = address(0x2); + address internal OWNER = address(0x3); + + // Function parameters + uint256 internal lotId = 1; + uint256 internal paymentAmount = 1e18; + MockFeeOnTransferERC20 internal quoteToken; + MockHook internal hook; + + function setUp() public { + // Set reasonable starting block + vm.warp(1_000_000); + + router = new ConcreteRouter(PROTOCOL, _PERMIT2_ADDRESS); + + quoteToken = new MockFeeOnTransferERC20("Quote Token", "QUOTE", 18); + quoteToken.setTransferFee(0); + } + + // [X] given the auction has hooks defined + // [X] it transfers the payment amount to the hook + // [X] given the auction does not have hooks defined + // [X] it transfers the payment amount to the owner + + modifier givenAuctionHasHook() { + hook = new MockHook(address(quoteToken), address(0)); + + // Set the addresses to track + address[] memory addresses = new address[](4); + addresses[0] = address(USER); + addresses[1] = address(OWNER); + addresses[2] = address(router); + addresses[3] = address(hook); + + hook.setBalanceAddresses(addresses); + _; + } + + modifier givenRouterHasBalance(uint256 amount_) { + quoteToken.mint(address(router), amount_); + _; + } + + function test_givenAuctionHasHook() + public + givenAuctionHasHook + givenRouterHasBalance(paymentAmount) + { + // Call + vm.prank(USER); + router.sendPayment(OWNER, paymentAmount, quoteToken, hook); + + // Check balances + assertEq(quoteToken.balanceOf(USER), 0, "user balance mismatch"); + assertEq(quoteToken.balanceOf(OWNER), 0, "owner balance mismatch"); + assertEq(quoteToken.balanceOf(address(router)), 0, "router balance mismatch"); + assertEq(quoteToken.balanceOf(address(hook)), paymentAmount, "hook balance mismatch"); + + // Hooks not called + assertEq(hook.preHookCalled(), false, "pre hook called"); + assertEq(hook.midHookCalled(), false, "mid hook called"); + assertEq(hook.postHookCalled(), false, "post hook called"); + } + + function test_givenAuctionHasNoHook() public givenRouterHasBalance(paymentAmount) { + // Call + vm.prank(USER); + router.sendPayment(OWNER, paymentAmount, quoteToken, hook); + + // Check balances + assertEq(quoteToken.balanceOf(USER), 0, "user balance mismatch"); + assertEq(quoteToken.balanceOf(OWNER), paymentAmount, "owner balance mismatch"); + assertEq(quoteToken.balanceOf(address(router)), 0, "router balance mismatch"); + } +} From 7edc9f152973fd3b897b187e20f4497acd4f814e Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 16 Jan 2024 16:19:47 +0400 Subject: [PATCH 29/82] Add overall purchase check with invariants --- src/AuctionHouse.sol | 14 ++ test/AuctionHouse/purchase.t.sol | 250 ++++++++++++++++++++----------- test/Router/collectPayout.t.sol | 10 ++ 3 files changed, 186 insertions(+), 88 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 8f94fd8e..a0ed9dda 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -22,6 +22,8 @@ import {IHooks} from "src/interfaces/IHooks.sol"; abstract contract FeeManager { // TODO write fee logic in separate contract to keep it organized // Router can inherit + +// TODO disbursing fees } // TODO define purpose @@ -359,6 +361,18 @@ abstract contract Router is FeeManager { revert UnsupportedToken(address(token_)); } } + + // ========== FEE MANAGEMENT ========== // + + function setProtocolFee(uint48 protocolFee_) external { + // TOOD make this permissioned + protocolFee = protocolFee_; + } + + function setReferrerFee(address referrer_, uint48 referrerFee_) external { + // TOOD make this permissioned + referrerFees[referrer_] = referrerFee_; + } } /// @title AuctionHouse diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 87429c74..b58caee0 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -46,6 +46,7 @@ contract PurchaseTest is Test, Permit2User { 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; @@ -55,14 +56,20 @@ contract PurchaseTest is Test, Permit2User { uint256 internal constant AMOUNT_IN = 1e18; uint256 internal AMOUNT_OUT; - uint256 internal approvalNonce; - bytes internal approvalSignature; - uint48 internal approvalDeadline; + uint48 internal referrerFee; + uint48 internal protocolFee; + + uint256 internal amountInLessFee; + uint256 internal amountInReferrerFee; + uint256 internal amountInProtocolFee; // Function parameters (can be modified) Auctioneer.RoutingParams internal routingParams; Auction.AuctionParams internal auctionParams; Router.PurchaseParams internal purchaseParams; + uint256 internal approvalNonce; + bytes internal approvalSignature; + uint48 internal approvalDeadline; function setUp() external { aliceKey = _getRandomUint256(); @@ -105,17 +112,28 @@ 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; + auctionHouse.setProtocolFee(protocolFee); + auctionHouse.setReferrerFee(referrer, referrerFee); + + amountInReferrerFee = (AMOUNT_IN * referrerFee) / 10_000; + amountInProtocolFee = (AMOUNT_IN * protocolFee) / 10_000; + amountInLessFee = AMOUNT_IN - amountInReferrerFee - amountInProtocolFee; + // Set the default payout multiplier to 1 mockAuctionModule.setPayoutMultiplier(lotId, 1); // 1:1 exchange rate - AMOUNT_OUT = AMOUNT_IN; - - approvalNonce = _getRandomUint256(); - approvalDeadline = uint48(block.timestamp) + 1 days; + AMOUNT_OUT = amountInLessFee; + // Purchase parameters purchaseParams = Router.PurchaseParams({ - recipient: alice, + recipient: recipient, referrer: referrer, approvalDeadline: approvalDeadline, lotId: lotId, @@ -167,22 +185,38 @@ contract PurchaseTest is Test, Permit2User { _; } - modifier whenAccountHasQuoteTokenBalance(uint256 amount_) { + modifier givenUserHasQuoteTokenBalance(uint256 amount_) { quoteToken.mint(alice, amount_); _; } - modifier whenAccountHasBaseTokenBalance(uint256 amount_) { + modifier givenOwnerHasBaseTokenBalance(uint256 amount_) { baseToken.mint(auctionOwner, amount_); _; } - modifier whenAuctionIsCancelled() { + modifier givenHookHasBaseTokenBalance(uint256 amount_) { + baseToken.mint(address(mockHook), amount_); + _; + } + + modifier givenAuctionIsCancelled() { vm.prank(auctionOwner); auctionHouse.cancel(lotId); _; } + modifier givenQuoteTokenSpendingIsApproved() { + vm.prank(alice); + quoteToken.approve(address(auctionHouse), AMOUNT_IN); + _; + } + + modifier givenAuctionHasHooks() { + routingParams.hooks = IHooks(address(mockHook)); + _; + } + // parameter checks // [X] when the lot id is invalid // [X] it reverts @@ -210,8 +244,8 @@ contract PurchaseTest is Test, Permit2User { function test_whenNotAtomicAuction_reverts() external whenBatchAuctionIsCreated - whenAccountHasQuoteTokenBalance(AMOUNT_IN) - whenAccountHasBaseTokenBalance(AMOUNT_OUT) + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenOwnerHasBaseTokenBalance(AMOUNT_OUT) { // Update purchase params purchaseParams.lotId = lotId; @@ -227,9 +261,9 @@ contract PurchaseTest is Test, Permit2User { function test_whenAuctionNotActive_reverts() external - whenAuctionIsCancelled - whenAccountHasQuoteTokenBalance(AMOUNT_IN) - whenAccountHasBaseTokenBalance(AMOUNT_OUT) + givenAuctionIsCancelled + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenOwnerHasBaseTokenBalance(AMOUNT_OUT) { // Expect revert bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); @@ -242,8 +276,8 @@ contract PurchaseTest is Test, Permit2User { function test_whenAuctionModuleReverts_reverts() external - whenAccountHasQuoteTokenBalance(AMOUNT_IN) - whenAccountHasBaseTokenBalance(AMOUNT_OUT) + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenOwnerHasBaseTokenBalance(AMOUNT_OUT) { // Set the auction module to revert mockAuctionModule.setPurchaseReverts(true); @@ -256,6 +290,23 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); } + function test_whenPayoutAmountLessThanMinimum_reverts() + external + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenOwnerHasBaseTokenBalance(AMOUNT_OUT) + { + // Set the payout multiplier so that the payout is less than the minimum + mockAuctionModule.setPayoutMultiplier(lotId, 0); + + // Expect revert + bytes memory err = abi.encodeWithSelector(AuctionHouse.AmountLessThanMinimum.selector); + vm.expectRevert(err); + + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + } + // allowlist // [X] given an allowlist is set // [X] when the caller is not on the allowlist @@ -293,8 +344,9 @@ contract PurchaseTest is Test, Permit2User { external givenAuctionHasAllowlist givenCallerIsOnAllowlist - whenAccountHasQuoteTokenBalance(AMOUNT_IN) - whenAccountHasBaseTokenBalance(AMOUNT_OUT) + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenOwnerHasBaseTokenBalance(AMOUNT_OUT) + givenQuoteTokenSpendingIsApproved { // Purchase vm.prank(alice); @@ -303,8 +355,9 @@ contract PurchaseTest is Test, Permit2User { // Caller has no quote tokens assertEq(quoteToken.balanceOf(alice), 0); - // Caller has base tokens - assertEq(baseToken.balanceOf(alice), AMOUNT_OUT); + // Recipient has base tokens + assertEq(baseToken.balanceOf(alice), 0); + assertEq(baseToken.balanceOf(recipient), amountInLessFee); } // transfer quote token to auction house @@ -313,15 +366,10 @@ contract PurchaseTest is Test, Permit2User { // [X] when the permit2 signature is not provided // [X] it succeeds using ERC20 transfer - modifier givenQuoteTokenSpendingIsApproved() { - quoteToken.approve(address(auctionHouse), AMOUNT_IN); - _; - } - function test_whenPermit2Signature() external - whenAccountHasQuoteTokenBalance(AMOUNT_IN) - whenAccountHasBaseTokenBalance(AMOUNT_OUT) + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenOwnerHasBaseTokenBalance(AMOUNT_OUT) { // Set the permit2 signature purchaseParams.approvalSignature = _signPermit( @@ -338,7 +386,7 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); // Check balances - assertEq(quoteToken.balanceOf(address(auctionHouse)), AMOUNT_IN); + assertEq(quoteToken.balanceOf(address(auctionHouse)), amountInLessFee); assertEq(quoteToken.balanceOf(alice), 0); // Ignore the rest @@ -346,86 +394,112 @@ contract PurchaseTest is Test, Permit2User { function test_whenNoPermit2Signature() external + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenOwnerHasBaseTokenBalance(AMOUNT_OUT) givenQuoteTokenSpendingIsApproved - whenAccountHasQuoteTokenBalance(AMOUNT_IN) - whenAccountHasBaseTokenBalance(AMOUNT_OUT) { // Purchase vm.prank(alice); auctionHouse.purchase(purchaseParams); // Check balances - assertEq(quoteToken.balanceOf(address(auctionHouse)), AMOUNT_IN); + assertEq(quoteToken.balanceOf(address(auctionHouse)), amountInLessFee); assertEq(quoteToken.balanceOf(alice), 0); // Ignore the rest } - // transfer quote token to auction owner - // [ ] given the auction has hooks defined - // [ ] the quote token is transferred to the hook before the hook is called - // [ ] given the auction does not have hooks defined - // [ ] the quote token is transferred to the auction owner - - // transfer payout token to router - // [ ] given the payout token is not a derivative - // [ ] given the auction has hooks defined - // [ ] the payout token is tranferred by the hook to the router - // [ ] given the auction does not have hooks defined - // [ ] the payout token is transferred to the router - - // transfer payout token to recipient - // [ ] given the payout token is not a derivative - // [ ] when the recipient is different to the caller - // [ ] the payout token is transferred from the router to the recipient - // [ ] given the payout token is a derivative - // [ ] when the recipient is different to the caller - // [ ] it mints a derivative to the recipient - - // TODO check invariants for entire flow - - function test_whenOwnerHasInsufficientBalanceOfBaseToken_reverts() - external - whenAccountHasQuoteTokenBalance(AMOUNT_IN) - { - // Expect revert - vm.expectRevert(); + // [X] given the auction has hooks defined + // [X] it succeeds - quote token transferred to hook, payout token (minus fees) transferred to recipient + // [X] given the auction does not have hooks defined + // [X] it succeeds - quote token transferred to auction owner, payout token (minus fees) transferred to recipient + function test_hooks() + public + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenHookHasBaseTokenBalance(AMOUNT_OUT) + givenQuoteTokenSpendingIsApproved + givenAuctionHasHooks + { // Purchase vm.prank(alice); auctionHouse.purchase(purchaseParams); + + // Check balances + assertEq(quoteToken.balanceOf(alice), 0); + assertEq(quoteToken.balanceOf(recipient), 0); + assertEq(quoteToken.balanceOf(address(mockHook)), amountInLessFee); + assertEq( + quoteToken.balanceOf(address(auctionHouse)), amountInProtocolFee + amountInReferrerFee + ); + assertEq(quoteToken.balanceOf(auctionOwner), 0); + + assertEq(baseToken.balanceOf(alice), 0); + assertEq(baseToken.balanceOf(recipient), AMOUNT_OUT); + assertEq(baseToken.balanceOf(address(mockHook)), 0); + assertEq(baseToken.balanceOf(address(auctionHouse)), 0); + assertEq(baseToken.balanceOf(auctionOwner), 0); + + // Check accrued fees + assertEq(auctionHouse.rewards(alice, quoteToken), 0); + assertEq(auctionHouse.rewards(recipient, quoteToken), 0); + assertEq(auctionHouse.rewards(referrer, quoteToken), amountInReferrerFee); + assertEq(auctionHouse.rewards(protocol, quoteToken), amountInProtocolFee); + assertEq(auctionHouse.rewards(address(mockHook), quoteToken), 0); + assertEq(auctionHouse.rewards(address(auctionHouse), quoteToken), 0); + assertEq(auctionHouse.rewards(auctionOwner, quoteToken), 0); + + assertEq(auctionHouse.rewards(alice, baseToken), 0); + assertEq(auctionHouse.rewards(recipient, baseToken), 0); + assertEq(auctionHouse.rewards(referrer, baseToken), 0); + assertEq(auctionHouse.rewards(protocol, baseToken), 0); + assertEq(auctionHouse.rewards(address(mockHook), baseToken), 0); + assertEq(auctionHouse.rewards(address(auctionHouse), baseToken), 0); + assertEq(auctionHouse.rewards(auctionOwner, baseToken), 0); } - function test_whenPayoutAmountLessThanMinimum_reverts() - external - whenAccountHasQuoteTokenBalance(AMOUNT_IN) - whenAccountHasBaseTokenBalance(AMOUNT_OUT) + function test_noHooks() + public + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenOwnerHasBaseTokenBalance(AMOUNT_OUT) + givenQuoteTokenSpendingIsApproved { - // Set the payout multiplier so that the payout is less than the minimum - mockAuctionModule.setPayoutMultiplier(lotId, 0); - - // Expect revert - bytes memory err = abi.encodeWithSelector(AuctionHouse.AmountLessThanMinimum.selector); - vm.expectRevert(err); - // Purchase vm.prank(alice); auctionHouse.purchase(purchaseParams); + + // Check balances + assertEq(quoteToken.balanceOf(alice), 0); + assertEq(quoteToken.balanceOf(recipient), 0); + assertEq(quoteToken.balanceOf(address(mockHook)), 0); + assertEq( + quoteToken.balanceOf(address(auctionHouse)), amountInProtocolFee + amountInReferrerFee + ); + assertEq(quoteToken.balanceOf(auctionOwner), amountInLessFee); + + assertEq(baseToken.balanceOf(alice), 0); + assertEq(baseToken.balanceOf(recipient), AMOUNT_OUT); + assertEq(baseToken.balanceOf(address(mockHook)), 0); + assertEq(baseToken.balanceOf(address(auctionHouse)), 0); + assertEq(baseToken.balanceOf(auctionOwner), 0); + + // Check accrued fees + assertEq(auctionHouse.rewards(alice, quoteToken), 0); + assertEq(auctionHouse.rewards(recipient, quoteToken), 0); + assertEq(auctionHouse.rewards(referrer, quoteToken), amountInReferrerFee); + assertEq(auctionHouse.rewards(protocol, quoteToken), amountInProtocolFee); + assertEq(auctionHouse.rewards(address(mockHook), quoteToken), 0); + assertEq(auctionHouse.rewards(address(auctionHouse), quoteToken), 0); + assertEq(auctionHouse.rewards(auctionOwner, quoteToken), 0); + + assertEq(auctionHouse.rewards(alice, baseToken), 0); + assertEq(auctionHouse.rewards(recipient, baseToken), 0); + assertEq(auctionHouse.rewards(referrer, baseToken), 0); + assertEq(auctionHouse.rewards(protocol, baseToken), 0); + assertEq(auctionHouse.rewards(address(mockHook), baseToken), 0); + assertEq(auctionHouse.rewards(address(auctionHouse), baseToken), 0); + assertEq(auctionHouse.rewards(auctionOwner, baseToken), 0); } - // transfers base token from auction house to recipient - // [ ] given the base token is a derivative - // [ ] given a condenser is set - // [ ] it uses the condenser to determine derivative parameters - // [ ] given a condenser is not set - // [ ] it uses the routing derivative parameters - // [ ] it mints derivative tokens to the recipient using the derivative module - // [ ] given the base token is not a derivative - // [ ] it transfers the base token to the recipient - // - // records fees - // [ ] given that a protocol fee is defined - // [ ] it records the protocol fee - // [ ] given that a referrer fee is defined - // [ ] it records the referrer fee + // TODO derivative module } diff --git a/test/Router/collectPayout.t.sol b/test/Router/collectPayout.t.sol index 776133d0..c1d8f194 100644 --- a/test/Router/collectPayout.t.sol +++ b/test/Router/collectPayout.t.sol @@ -254,4 +254,14 @@ contract CollectPayoutTest is Test, Permit2User { // [ ] it succeeds - derivative is minted to the router, mid hook is called before minting // [ ] given the auction does not have hooks defined // [ ] it succeeds - derivative is minted to the router + + // transfers base token from auction house to recipient + // [ ] given the base token is a derivative + // [ ] given a condenser is set + // [ ] it uses the condenser to determine derivative parameters + // [ ] given a condenser is not set + // [ ] it uses the routing derivative parameters + // [ ] it mints derivative tokens to the recipient using the derivative module + // [ ] given the base token is not a derivative + // [ ] it transfers the base token to the recipient } From 47e511592ce661e891cf4747e571dfe20d59f7a1 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 16 Jan 2024 16:44:42 +0400 Subject: [PATCH 30/82] Tests and implementation for sendPayout() --- src/AuctionHouse.sol | 44 +++++++- test/Router/ConcreteRouter.sol | 10 ++ test/Router/sendPayout.t.sol | 177 ++++++++++++++++++++++++++++++ test/modules/Auction/MockHook.sol | 2 +- 4 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 test/Router/sendPayout.t.sol diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index a0ed9dda..0dfd4af8 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -279,9 +279,49 @@ abstract contract Router is FeeManager { // TODO handle derivative } - // TODO sendPayout + /// @notice Sends the payout token to the recipient + /// @dev This function handles the following: + /// 1. Sends the payout token from the router to the recipient + /// 2. Calls the post hook on the hooks contract (if provided) + /// + /// This function assumes that: + /// - The payout token has already been transferred to this contract + /// - The payout token is supported (e.g. not fee-on-transfer) + /// + /// This function reverts if: + /// - The payout token transfer fails + /// - The payout token transfer would result in a lesser amount being received + /// - The post-hook reverts + /// - The post-hook invariant is violated + /// + /// @param lotId_ Lot ID + /// @param recipient_ Address to receive payout + /// @param payoutAmount_ Amount of payoutToken to send (in native decimals) + /// @param payoutToken_ Payout token to send + /// @param hooks_ Hooks contract to call (optional) + function _sendPayout( + uint256 lotId_, + address recipient_, + uint256 payoutAmount_, + ERC20 payoutToken_, + IHooks hooks_ + ) internal { + // Get the pre-transfer balance + uint256 balanceBefore = payoutToken_.balanceOf(recipient_); + + // Send payout token to recipient + payoutToken_.safeTransfer(recipient_, payoutAmount_); - // TODO sendPayment + // Check that the recipient received the expected amount of payout tokens + if (payoutToken_.balanceOf(recipient_) < balanceBefore + payoutAmount_) { + revert UnsupportedToken(address(payoutToken_)); + } + + // Call post hook on hooks contract if provided + if (address(hooks_) != address(0)) { + hooks_.post(lotId_, payoutAmount_); + } + } /// @notice Performs an ERC20 transfer of `token_` from the caller /// @dev This function handles the following: diff --git a/test/Router/ConcreteRouter.sol b/test/Router/ConcreteRouter.sol index 906c2572..0aff59a0 100644 --- a/test/Router/ConcreteRouter.sol +++ b/test/Router/ConcreteRouter.sol @@ -76,4 +76,14 @@ contract ConcreteRouter is Router { ) external { return _sendPayment(lotOwner_, paymentAmount_, quoteToken_, hooks_); } + + function sendPayout( + uint256 lotId_, + address recipient_, + uint256 payoutAmount_, + ERC20 payoutToken_, + IHooks hooks_ + ) external { + return _sendPayout(lotId_, recipient_, payoutAmount_, payoutToken_, hooks_); + } } diff --git a/test/Router/sendPayout.t.sol b/test/Router/sendPayout.t.sol new file mode 100644 index 00000000..8f1b3d98 --- /dev/null +++ b/test/Router/sendPayout.t.sol @@ -0,0 +1,177 @@ +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; + +import {MockHook} from "test/modules/Auction/MockHook.sol"; +import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; +import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +import {Router} from "src/AuctionHouse.sol"; +import {IHooks} from "src/interfaces/IHooks.sol"; + +contract SendPayoutTest is Test, Permit2User { + ConcreteRouter internal router; + + address internal constant PROTOCOL = address(0x1); + + address internal USER = address(0x2); + address internal OWNER = address(0x3); + address internal RECIPIENT = address(0x4); + + // Function parameters + uint256 internal lotId = 1; + uint256 internal payoutAmount = 10e18; + MockFeeOnTransferERC20 internal payoutToken; + MockHook internal hook; + + function setUp() public { + // Set reasonable starting block + vm.warp(1_000_000); + + router = new ConcreteRouter(PROTOCOL, _PERMIT2_ADDRESS); + + payoutToken = new MockFeeOnTransferERC20("Payout Token", "PAYOUT", 18); + payoutToken.setTransferFee(0); + } + + modifier givenTokenTakesFeeOnTransfer() { + payoutToken.setTransferFee(1000); + _; + } + + modifier givenRouterHasBalance(uint256 amount_) { + payoutToken.mint(address(router), amount_); + _; + } + + // ========== Hooks flow ========== // + + // [ ] given the auction has hooks defined + // [X] when the token is unsupported + // [X] it reverts + // [X] when the post hook reverts + // [X] it reverts + // [ ] when the post hook invariant is broken + // [ ] it reverts + // [X] it succeeds - transfers the payout from the router to the recipient + + modifier givenAuctionHasHook() { + hook = new MockHook(address(0), address(payoutToken)); + + // Set the addresses to track + address[] memory addresses = new address[](5); + addresses[0] = USER; + addresses[1] = OWNER; + addresses[2] = address(router); + addresses[3] = address(hook); + addresses[4] = RECIPIENT; + + hook.setBalanceAddresses(addresses); + _; + } + + modifier givenPostHookReverts() { + hook.setPostHookReverts(true); + _; + } + + function test_hooks_whenPostHookReverts_reverts() + public + givenAuctionHasHook + givenPostHookReverts + givenRouterHasBalance(payoutAmount) + { + // Expect revert + vm.expectRevert("revert"); + + // Call + vm.prank(USER); + router.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + } + + function test_hooks_feeOnTransfer_reverts() + public + givenAuctionHasHook + givenRouterHasBalance(payoutAmount) + givenTokenTakesFeeOnTransfer + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(Router.UnsupportedToken.selector, address(payoutToken)); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + } + + function test_hooks() public givenAuctionHasHook givenRouterHasBalance(payoutAmount) { + // Call + vm.prank(USER); + router.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + + // Check balances + assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); + assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); + assertEq(payoutToken.balanceOf(address(router)), 0, "router balance mismatch"); + assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); + assertEq(payoutToken.balanceOf(RECIPIENT), payoutAmount, "recipient balance mismatch"); + + // Check the hook was called at the right time + assertEq(hook.preHookCalled(), false, "pre hook mismatch"); + assertEq(hook.midHookCalled(), false, "mid hook mismatch"); + assertEq(hook.postHookCalled(), true, "post hook mismatch"); + assertEq(hook.postHookBalances(payoutToken, USER), 0, "post hook user balance mismatch"); + assertEq(hook.postHookBalances(payoutToken, OWNER), 0, "post hook owner balance mismatch"); + assertEq( + hook.postHookBalances(payoutToken, address(router)), + 0, + "post hook router balance mismatch" + ); + assertEq( + hook.postHookBalances(payoutToken, address(hook)), 0, "post hook hook balance mismatch" + ); + assertEq( + hook.postHookBalances(payoutToken, RECIPIENT), + payoutAmount, + "post hook recipient balance mismatch" + ); + } + + // ========== Non-hooks flow ========== // + + // [X] given the auction does not have hooks defined + // [X] given transferring the payout token would result in a lesser amount being received + // [X] it reverts + // [X] it succeeds - transfers the payout from the router to the recipient + + function test_noHooks_feeOnTransfer_reverts() + public + givenRouterHasBalance(payoutAmount) + givenTokenTakesFeeOnTransfer + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(Router.UnsupportedToken.selector, address(payoutToken)); + vm.expectRevert(err); + + // Call + vm.prank(USER); + router.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + } + + function test_noHooks() public givenRouterHasBalance(payoutAmount) { + // Call + vm.prank(USER); + router.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + + // Check balances + assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); + assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); + assertEq(payoutToken.balanceOf(address(router)), 0, "router balance mismatch"); + assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); + assertEq(payoutToken.balanceOf(RECIPIENT), payoutAmount, "recipient balance mismatch"); + } +} diff --git a/test/modules/Auction/MockHook.sol b/test/modules/Auction/MockHook.sol index 6608f46a..95ed04dc 100644 --- a/test/modules/Auction/MockHook.sol +++ b/test/modules/Auction/MockHook.sol @@ -123,7 +123,7 @@ contract MockHook is IHooks { } for (uint256 j = 0; j < balanceAddresses.length; j++) { - midHookBalances[token][balanceAddresses[j]] = token.balanceOf(balanceAddresses[j]); + postHookBalances[token][balanceAddresses[j]] = token.balanceOf(balanceAddresses[j]); } } From bb65a371865c15b62a92fd3ff3347243b0706e03 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 16 Jan 2024 17:23:40 +0400 Subject: [PATCH 31/82] Put the purchase() components together --- src/AuctionHouse.sol | 138 ++++-------------- test/AuctionHouse/purchase.t.sol | 56 +++++-- .../Auction/MockAtomicAuctionModule.sol | 7 +- 3 files changed, 84 insertions(+), 117 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 0dfd4af8..19aa3497 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -486,8 +486,9 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// 2. Sends the purchase amount to the auction module /// 3. Records the purchase on the auction module /// 4. Transfers the quote token from the caller - /// 5. Transfers the quote token to the auction owner or executes the callback - /// 6. Transfers the payout token to the recipient + /// 5. Transfers the quote token to the auction owner + /// 5. Transfers the base token from the auction owner or executes the callback + /// 6. Transfers the base token to the recipient /// /// This function reverts if: /// - `lotId_` is invalid @@ -503,33 +504,52 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { isValidLot(params_.lotId) returns (uint256 payout) { - // TODO should this not check if the auction is atomic? - // Response: No, my thought was that the module will just revert on `purchase` if it's not atomic. Vice versa - // Load routing data for the lot 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, bytes(""))) { + revert NotAuthorized(); + } + } + uint256 totalFees = _allocateFees(params_.referrer, routing.quoteToken, params_.amount); + uint256 amountLessFees = params_.amount - totalFees; // Send purchase to auction house and get payout plus any extra output bytes memory auctionOutput; { AuctionModule module = _getModuleForId(params_.lotId); (payout, auctionOutput) = - module.purchase(params_.lotId, params_.amount - totalFees, params_.auctionData); + module.purchase(params_.lotId, amountLessFees, params_.auctionData); } // Check that payout is at least minimum amount out // @dev Moved the slippage check from the auction to the AuctionHouse to allow different routing and purchase logic if (payout < params_.minAmountOut) revert AmountLessThanMinimum(); - // Handle transfers from purchaser and seller - _handleTransfers( - params_.lotId, routing, params_.amount, payout, totalFees, params_.approvalSignature + // Collect payment from the purchaser + _collectPayment( + params_.lotId, + params_.amount, + routing.quoteToken, + routing.hooks, + params_.approvalDeadline, + params_.approvalNonce, + params_.approvalSignature ); - // Handle payout to user, including creation of derivative tokens - _handlePayout(routing, params_.recipient, payout, auctionOutput); + // Send payment to auction owner + _sendPayment(routing.owner, amountLessFees, routing.quoteToken, routing.hooks); + + // Collect payout from auction owner + _collectPayout( + params_.lotId, routing.owner, amountLessFees, payout, routing.baseToken, routing.hooks + ); + + // Send payout to recipient + _sendPayout(params_.lotId, params_.recipient, payout, routing.baseToken, routing.hooks); // Emit event emit Purchase(params_.lotId, msg.sender, params_.referrer, params_.amount, payout); @@ -560,100 +580,4 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { ) external override returns (uint256[] memory amountsOut) { // TODO } - - // ============ INTERNAL EXECUTION FUNCTIONS ========== // - - /// @notice Handles transfer of funds from user and market owner/callback - function _handleTransfers( - uint256 id_, - Routing memory routing_, - uint256 amount_, - uint256 payout_, - uint256 feePaid_, - bytes memory approval_ - ) internal { - // Calculate amount net of fees - uint256 amountLessFee = amount_ - feePaid_; - - // Check if approval signature has been provided, if so use it increase allowance - // TODO a bunch of extra data has to be provided for Permit. - if (approval_.length != 0) {} - - // Have to transfer to teller 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) - uint256 quoteBalance = routing_.quoteToken.balanceOf(address(this)); - routing_.quoteToken.safeTransferFrom(msg.sender, address(this), amount_); - if (routing_.quoteToken.balanceOf(address(this)) < quoteBalance + amount_) { - revert UnsupportedToken(address(routing_.quoteToken)); - } - - // If callback address supplied, transfer tokens from teller to callback, then execute callback function, - // and ensure proper amount of tokens transferred in. - // TODO substitute callback for hooks (and implement in more places)? - 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), amountLessFee); - - // Call the callback function to receive payout tokens for payout - uint256 baseBalance = routing_.baseToken.balanceOf(address(this)); - routing_.hooks.mid(id_, amountLessFee, payout_); - - // Check to ensure that the callback sent the requested amount of payout tokens back to the teller - if (routing_.baseToken.balanceOf(address(this)) < (baseBalance + payout_)) { - revert InvalidHook(); - } - } else { - // If no callback is provided, transfer tokens from market 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), payout_); - if (routing_.baseToken.balanceOf(address(this)) < (baseBalance + payout_)) { - revert UnsupportedToken(address(routing_.baseToken)); - } - - routing_.quoteToken.safeTransfer(routing_.owner, amountLessFee); - } - } - - function _handlePayout( - Routing memory routing_, - address recipient_, - uint256 payout_, - bytes memory auctionOutput_ - ) internal { - // If no derivative, then the payout is sent directly to the recipient - // Otherwise, send parameters and payout to the derivative to mint to recipient - if (fromVeecode(routing_.derivativeReference) == bytes7("")) { - // No derivative, send payout to recipient - routing_.baseToken.safeTransfer(recipient_, payout_); - } else { - // Get the module for the derivative type - // We assume that the module type has been checked when the lot was created - DerivativeModule module = - DerivativeModule(_getModuleIfInstalled(routing_.derivativeReference)); - - bytes memory derivativeParams = routing_.derivativeParams; - - // Lookup condensor module from combination of auction and derivative types - // If condenser specified, condense auction output and derivative params before sending to derivative module - Veecode condenserRef = - condensers[routing_.auctionReference][routing_.derivativeReference]; - if (fromVeecode(condenserRef) != bytes7("")) { - // Get condenser module - CondenserModule condenser = CondenserModule(_getModuleIfInstalled(condenserRef)); - - // Condense auction output and derivative params - derivativeParams = condenser.condense(auctionOutput_, derivativeParams); - } - - // Approve the module to transfer payout tokens - routing_.baseToken.safeApprove(address(module), payout_); - - // Call the module to mint derivative tokens to the recipient - module.mint(recipient_, derivativeParams, payout_, routing_.wrapDerivative); - } - } } diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index b58caee0..e7551249 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -121,13 +121,10 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.setProtocolFee(protocolFee); auctionHouse.setReferrerFee(referrer, referrerFee); - amountInReferrerFee = (AMOUNT_IN * referrerFee) / 10_000; - amountInProtocolFee = (AMOUNT_IN * protocolFee) / 10_000; + amountInReferrerFee = (AMOUNT_IN * referrerFee) / 1e5; + amountInProtocolFee = (AMOUNT_IN * protocolFee) / 1e5; amountInLessFee = AMOUNT_IN - amountInReferrerFee - amountInProtocolFee; - // Set the default payout multiplier to 1 - mockAuctionModule.setPayoutMultiplier(lotId, 1); - // 1:1 exchange rate AMOUNT_OUT = amountInLessFee; @@ -212,8 +209,26 @@ contract PurchaseTest is Test, Permit2User { _; } + modifier givenQuoteTokenPermit2IsApproved() { + vm.prank(alice); + quoteToken.approve(address(_PERMIT2_ADDRESS), type(uint256).max); + _; + } + + modifier givenBaseTokenSpendingIsApproved() { + vm.prank(auctionOwner); + baseToken.approve(address(auctionHouse), AMOUNT_OUT); + _; + } + modifier givenAuctionHasHooks() { routingParams.hooks = IHooks(address(mockHook)); + + // Create a new auction with the hooks + lotId = auctionHouse.auction(routingParams, auctionParams); + + // Update the purchase params + purchaseParams.lotId = lotId; _; } @@ -296,7 +311,7 @@ contract PurchaseTest is Test, Permit2User { givenOwnerHasBaseTokenBalance(AMOUNT_OUT) { // Set the payout multiplier so that the payout is less than the minimum - mockAuctionModule.setPayoutMultiplier(lotId, 0); + mockAuctionModule.setPayoutMultiplier(lotId, 90_000); // Expect revert bytes memory err = abi.encodeWithSelector(AuctionHouse.AmountLessThanMinimum.selector); @@ -314,10 +329,17 @@ contract PurchaseTest is Test, Permit2User { // [X] when the caller is on the allowlist // [X] it succeeds + // TODO add support for allowlist proof + modifier givenAuctionHasAllowlist() { // Register a new auction with an allowlist routingParams.allowlist = mockAllowlist; + + vm.prank(auctionOwner); lotId = auctionHouse.auction(routingParams, auctionParams); + + // Update the purchase params + purchaseParams.lotId = lotId; _; } @@ -347,6 +369,7 @@ contract PurchaseTest is Test, Permit2User { givenUserHasQuoteTokenBalance(AMOUNT_IN) givenOwnerHasBaseTokenBalance(AMOUNT_OUT) givenQuoteTokenSpendingIsApproved + givenBaseTokenSpendingIsApproved { // Purchase vm.prank(alice); @@ -370,6 +393,8 @@ contract PurchaseTest is Test, Permit2User { external givenUserHasQuoteTokenBalance(AMOUNT_IN) givenOwnerHasBaseTokenBalance(AMOUNT_OUT) + givenBaseTokenSpendingIsApproved + givenQuoteTokenPermit2IsApproved { // Set the permit2 signature purchaseParams.approvalSignature = _signPermit( @@ -386,8 +411,13 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.purchase(purchaseParams); // Check balances - assertEq(quoteToken.balanceOf(address(auctionHouse)), amountInLessFee); assertEq(quoteToken.balanceOf(alice), 0); + assertEq(quoteToken.balanceOf(recipient), 0); + assertEq(quoteToken.balanceOf(address(mockHook)), 0); + assertEq( + quoteToken.balanceOf(address(auctionHouse)), amountInProtocolFee + amountInReferrerFee + ); + assertEq(quoteToken.balanceOf(auctionOwner), amountInLessFee); // Ignore the rest } @@ -397,14 +427,20 @@ contract PurchaseTest is Test, Permit2User { givenUserHasQuoteTokenBalance(AMOUNT_IN) givenOwnerHasBaseTokenBalance(AMOUNT_OUT) givenQuoteTokenSpendingIsApproved + givenBaseTokenSpendingIsApproved { // Purchase vm.prank(alice); auctionHouse.purchase(purchaseParams); // Check balances - assertEq(quoteToken.balanceOf(address(auctionHouse)), amountInLessFee); assertEq(quoteToken.balanceOf(alice), 0); + assertEq(quoteToken.balanceOf(recipient), 0); + assertEq(quoteToken.balanceOf(address(mockHook)), 0); + assertEq( + quoteToken.balanceOf(address(auctionHouse)), amountInProtocolFee + amountInReferrerFee + ); + assertEq(quoteToken.balanceOf(auctionOwner), amountInLessFee); // Ignore the rest } @@ -416,10 +452,11 @@ contract PurchaseTest is Test, Permit2User { function test_hooks() public + givenAuctionHasHooks givenUserHasQuoteTokenBalance(AMOUNT_IN) givenHookHasBaseTokenBalance(AMOUNT_OUT) givenQuoteTokenSpendingIsApproved - givenAuctionHasHooks + givenBaseTokenSpendingIsApproved { // Purchase vm.prank(alice); @@ -463,6 +500,7 @@ contract PurchaseTest is Test, Permit2User { givenUserHasQuoteTokenBalance(AMOUNT_IN) givenOwnerHasBaseTokenBalance(AMOUNT_OUT) givenQuoteTokenSpendingIsApproved + givenBaseTokenSpendingIsApproved { // Purchase vm.prank(alice); diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index d5806930..b5407fb7 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -46,7 +46,12 @@ contract MockAtomicAuctionModule is AuctionModule { if (cancelled[id_]) revert Auction_MarketNotActive(id_); - payout = payoutData[id_] * amount_; + if (payoutData[id_] == 0) { + payout = amount_; + } else { + payout = payoutData[id_] * amount_ / 1e5; + } + auctionOutput = auctionData_; } From e3d140f87aa54943bbd4ef6415178e692f286c7a Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 16 Jan 2024 20:53:47 +0400 Subject: [PATCH 32/82] Rename variable --- src/AuctionHouse.sol | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 19aa3497..b1ef42ef 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -502,7 +502,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { external override isValidLot(params_.lotId) - returns (uint256 payout) + returns (uint256 payoutAmount) { // Load routing data for the lot Routing memory routing = lotRouting[params_.lotId]; @@ -521,13 +521,13 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { bytes memory auctionOutput; { AuctionModule module = _getModuleForId(params_.lotId); - (payout, auctionOutput) = + (payoutAmount, auctionOutput) = module.purchase(params_.lotId, amountLessFees, params_.auctionData); } // Check that payout is at least minimum amount out // @dev Moved the slippage check from the auction to the AuctionHouse to allow different routing and purchase logic - if (payout < params_.minAmountOut) revert AmountLessThanMinimum(); + if (payoutAmount < params_.minAmountOut) revert AmountLessThanMinimum(); // Collect payment from the purchaser _collectPayment( @@ -545,14 +545,21 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Collect payout from auction owner _collectPayout( - params_.lotId, routing.owner, amountLessFees, payout, routing.baseToken, routing.hooks + params_.lotId, + routing.owner, + amountLessFees, + payoutAmount, + routing.baseToken, + routing.hooks ); // Send payout to recipient - _sendPayout(params_.lotId, params_.recipient, payout, routing.baseToken, routing.hooks); + _sendPayout( + params_.lotId, params_.recipient, payoutAmount, routing.baseToken, routing.hooks + ); // Emit event - emit Purchase(params_.lotId, msg.sender, params_.referrer, params_.amount, payout); + emit Purchase(params_.lotId, msg.sender, params_.referrer, params_.amount, payoutAmount); } // ========== BATCH AUCTIONS ========== // From a76a2d752962ef40dfae027f48d6c2e75cd558b2 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 16 Jan 2024 20:54:02 +0400 Subject: [PATCH 33/82] Add test for derivative module in purchase() --- test/AuctionHouse/purchase.t.sol | 61 ++++++++++++++++--- .../Derivative/MockDerivativeModule.sol | 11 +++- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index e7551249..12179f82 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -85,6 +85,8 @@ contract PurchaseTest is Test, Permit2User { mockAllowlist = new MockAllowlist(); mockHook = new MockHook(address(quoteToken), address(baseToken)); + mockDerivativeModule.setDerivativeToken(baseToken); + auctionParams = Auction.AuctionParams({ start: uint48(block.timestamp), duration: uint48(1 days), @@ -142,16 +144,11 @@ contract PurchaseTest is Test, Permit2User { }); } - modifier whenDerivativeModuleIsInstalled() { + modifier givenDerivativeModuleIsInstalled() { auctionHouse.installModule(mockDerivativeModule); _; } - modifier whenDerivativeTypeIsSet() { - routingParams.derivativeType = toKeycode("DERV"); - _; - } - modifier whenCondenserModuleIsInstalled() { auctionHouse.installModule(mockCondenserModule); _; @@ -179,6 +176,9 @@ contract PurchaseTest is Test, Permit2User { // Create the batch auction vm.prank(auctionOwner); lotId = auctionHouse.auction(routingParams, auctionParams); + + // Update purchase parameters + purchaseParams.lotId = lotId; _; } @@ -262,9 +262,6 @@ contract PurchaseTest is Test, Permit2User { givenUserHasQuoteTokenBalance(AMOUNT_IN) givenOwnerHasBaseTokenBalance(AMOUNT_OUT) { - // Update purchase params - purchaseParams.lotId = lotId; - // Expect revert bytes memory err = abi.encodeWithSelector(Auction.Auction_NotImplemented.selector); vm.expectRevert(err); @@ -539,5 +536,49 @@ contract PurchaseTest is Test, Permit2User { assertEq(auctionHouse.rewards(auctionOwner, baseToken), 0); } - // TODO derivative module + // ======== Derivative flow ======== // + + modifier givenAuctionHasDerivative() { + // Assumes the derivative module is already installed + + // Set up a new auction with a derivative + routingParams.derivativeType = toKeycode("DERV"); + routingParams.derivativeParams = abi.encode(""); + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + + // Set purchase parameters + purchaseParams.lotId = lotId; + _; + } + + // [X] given the auction has a derivative defined + // [X] it succeeds - derivative is minted + + function test_derivative() + public + givenDerivativeModuleIsInstalled + givenAuctionHasDerivative + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenQuoteTokenSpendingIsApproved + { + // Call + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + + // Check balances + assertEq(quoteToken.balanceOf(alice), 0); + assertEq(quoteToken.balanceOf(recipient), 0); + assertEq(quoteToken.balanceOf(address(mockHook)), 0); + assertEq( + quoteToken.balanceOf(address(auctionHouse)), amountInProtocolFee + amountInReferrerFee + ); + assertEq(quoteToken.balanceOf(auctionOwner), amountInLessFee); + + assertEq(baseToken.balanceOf(alice), 0); + assertEq(baseToken.balanceOf(recipient), AMOUNT_OUT); + assertEq(baseToken.balanceOf(address(mockHook)), 0); + assertEq(baseToken.balanceOf(address(auctionHouse)), 0); + assertEq(baseToken.balanceOf(auctionOwner), 0); + } } diff --git a/test/modules/Derivative/MockDerivativeModule.sol b/test/modules/Derivative/MockDerivativeModule.sol index 17e8c09f..4b5d61a4 100644 --- a/test/modules/Derivative/MockDerivativeModule.sol +++ b/test/modules/Derivative/MockDerivativeModule.sol @@ -7,8 +7,11 @@ import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; // Auctions import {DerivativeModule} from "src/modules/Derivative.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + contract MockDerivativeModule is DerivativeModule { bool internal validateFails; + MockERC20 internal derivativeToken; constructor(address _owner) Module(_owner) {} @@ -37,7 +40,9 @@ contract MockDerivativeModule is DerivativeModule { uint256 tokenId_, uint256 amount_, bool wrapped_ - ) external virtual override returns (uint256, address, uint256) {} + ) external virtual override returns (uint256, address, uint256) { + derivativeToken.mint(to_, amount_); + } function redeem(uint256 tokenId_, uint256 amount_, bool wrapped_) external virtual override {} @@ -77,4 +82,8 @@ contract MockDerivativeModule is DerivativeModule { function setValidateFails(bool validateFails_) external { validateFails = validateFails_; } + + function setDerivativeToken(MockERC20 token_) external { + derivativeToken = token_; + } } From 52f66d4524cd71204a04b3fa8ace3532f43cd2e7 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 11:31:00 +0400 Subject: [PATCH 34/82] WIP tests for derivative payout --- src/AuctionHouse.sol | 12 ++++++++++-- test/Router/ConcreteRouter.sol | 15 +++++++++++++-- test/Router/collectPayout.t.sol | 22 +++++++++++++++++++++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index b1ef42ef..8b713d83 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -131,6 +131,8 @@ abstract contract Router is FeeManager { // ========== TOKEN TRANSFERS ========== // + // TODO shift functions to AuctionHouse + /// @notice Collects payment of the quote token from the user /// @dev This function handles the following: /// 1. Calls the pre hook on the hooks contract (if provided) @@ -239,7 +241,10 @@ abstract contract Router is FeeManager { uint256 paymentAmount_, uint256 payoutAmount_, ERC20 payoutToken_, - IHooks hooks_ + IHooks hooks_, + Veecode derivativeReference, + bytes memory derivativeParams, + bool wrapDerivative ) internal { // Get the balance of the payout token before the transfer uint256 balanceBefore = payoutToken_.balanceOf(address(this)); @@ -550,7 +555,10 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { amountLessFees, payoutAmount, routing.baseToken, - routing.hooks + routing.hooks, + routing.derivativeReference, + routing.derivativeParams, + routing.wrapDerivative ); // Send payout to recipient diff --git a/test/Router/ConcreteRouter.sol b/test/Router/ConcreteRouter.sol index 0aff59a0..b0c48cb4 100644 --- a/test/Router/ConcreteRouter.sol +++ b/test/Router/ConcreteRouter.sol @@ -8,6 +8,8 @@ import {Router} from "src/AuctionHouse.sol"; import {Auction} from "src/modules/Auction.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; +import {wrapVeecode, toKeycode} from "src/modules/Modules.sol"; + contract ConcreteRouter is Router { constructor(address protocol_, address permit2_) Router(protocol_, permit2_) {} @@ -64,8 +66,17 @@ contract ConcreteRouter is Router { ERC20 payoutToken_, IHooks hooks_ ) external { - return - _collectPayout(lotId_, lotOwner_, paymentAmount_, payoutAmount_, payoutToken_, hooks_); + return _collectPayout( + lotId_, + lotOwner_, + paymentAmount_, + payoutAmount_, + payoutToken_, + hooks_, + wrapVeecode(toKeycode("MOCK"), 1), + bytes(""), + false + ); } function sendPayment( diff --git a/test/Router/collectPayout.t.sol b/test/Router/collectPayout.t.sol index c1d8f194..bd29d326 100644 --- a/test/Router/collectPayout.t.sol +++ b/test/Router/collectPayout.t.sol @@ -10,6 +10,10 @@ import {Permit2User} from "test/lib/permit2/Permit2User.sol"; import {Router} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; +import {IAllowlist} from "src/interfaces/IAllowlist.sol"; +import {Auctioneer} from "src/bases/Auctioneer.sol"; + +import {Veecode, wrapVeecode, toKeycode} from "src/modules/Modules.sol"; contract CollectPayoutTest is Test, Permit2User { ConcreteRouter internal router; @@ -23,8 +27,12 @@ contract CollectPayoutTest is Test, Permit2User { uint256 internal lotId = 1; uint256 internal paymentAmount = 1e18; uint256 internal payoutAmount = 10e18; + MockFeeOnTransferERC20 internal quoteToken; MockFeeOnTransferERC20 internal payoutToken; MockHook internal hook; + Veecode internal derivativeReference; + bytes internal derivativeParams; + bool internal wrapDerivative; function setUp() public { // Set reasonable starting block @@ -32,8 +40,15 @@ contract CollectPayoutTest is Test, Permit2User { router = new ConcreteRouter(PROTOCOL, _PERMIT2_ADDRESS); + quoteToken = new MockFeeOnTransferERC20("Quote Token", "QUOTE", 18); + quoteToken.setTransferFee(0); + payoutToken = new MockFeeOnTransferERC20("Payout Token", "PAYOUT", 18); payoutToken.setTransferFee(0); + + derivativeReference = wrapVeecode(toKeycode(""), 0); + derivativeParams = bytes(""); + wrapDerivative = false; } modifier givenOwnerHasBalance(uint256 amount_) { @@ -64,7 +79,7 @@ contract CollectPayoutTest is Test, Permit2User { // [X] it succeeds modifier givenAuctionHasHook() { - hook = new MockHook(address(0), address(payoutToken)); + hook = new MockHook(address(quoteToken), address(payoutToken)); // Set the addresses to track address[] memory addresses = new address[](4); @@ -264,4 +279,9 @@ contract CollectPayoutTest is Test, Permit2User { // [ ] it mints derivative tokens to the recipient using the derivative module // [ ] given the base token is not a derivative // [ ] it transfers the base token to the recipient + + modifier givenAuctionHasDerivative() { + derivativeReference = wrapVeecode(toKeycode("DERV"), 1); + _; + } } From 4dd14e5a9c9c0d88b082bab0a4352ea84caad8fd Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 11:32:40 +0400 Subject: [PATCH 35/82] Move file --- test/Router/collectPayment.t.sol | 2 +- test/Router/collectPayout.t.sol | 2 +- test/Router/sendPayment.t.sol | 2 +- test/Router/sendPayout.t.sol | 2 +- test/{Router => lib/mocks}/MockFeeOnTransferERC20.sol | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename test/{Router => lib/mocks}/MockFeeOnTransferERC20.sol (100%) diff --git a/test/Router/collectPayment.t.sol b/test/Router/collectPayment.t.sol index 631a5eec..2c17baad 100644 --- a/test/Router/collectPayment.t.sol +++ b/test/Router/collectPayment.t.sol @@ -5,7 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; -import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; +import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; import {Permit2Clone} from "test/lib/permit2/Permit2Clone.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; diff --git a/test/Router/collectPayout.t.sol b/test/Router/collectPayout.t.sol index bd29d326..dbb49216 100644 --- a/test/Router/collectPayout.t.sol +++ b/test/Router/collectPayout.t.sol @@ -5,7 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; -import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; +import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; import {Router} from "src/AuctionHouse.sol"; diff --git a/test/Router/sendPayment.t.sol b/test/Router/sendPayment.t.sol index b5851627..14aadd2b 100644 --- a/test/Router/sendPayment.t.sol +++ b/test/Router/sendPayment.t.sol @@ -5,7 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; -import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; +import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; import {Router} from "src/AuctionHouse.sol"; diff --git a/test/Router/sendPayout.t.sol b/test/Router/sendPayout.t.sol index 8f1b3d98..27c40460 100644 --- a/test/Router/sendPayout.t.sol +++ b/test/Router/sendPayout.t.sol @@ -5,7 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; -import {MockFeeOnTransferERC20} from "test/Router/MockFeeOnTransferERC20.sol"; +import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; import {Router} from "src/AuctionHouse.sol"; diff --git a/test/Router/MockFeeOnTransferERC20.sol b/test/lib/mocks/MockFeeOnTransferERC20.sol similarity index 100% rename from test/Router/MockFeeOnTransferERC20.sol rename to test/lib/mocks/MockFeeOnTransferERC20.sol From 87a0138f84790348060f001b70db750bf5dd31d4 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 11:52:30 +0400 Subject: [PATCH 36/82] Shift transfer functions to AuctionHouse --- src/AuctionHouse.sol | 411 +++++++++--------- .../MockAuctionHouse.sol} | 34 +- .../collectPayment.t.sol | 96 ++-- .../collectPayout.t.sol | 64 +-- .../sendPayment.t.sol | 18 +- .../{Router => AuctionHouse}/sendPayout.t.sol | 38 +- 6 files changed, 328 insertions(+), 333 deletions(-) rename test/{Router/ConcreteRouter.sol => AuctionHouse/MockAuctionHouse.sol} (66%) rename test/{Router => AuctionHouse}/collectPayment.t.sol (84%) rename test/{Router => AuctionHouse}/collectPayout.t.sol (76%) rename test/{Router => AuctionHouse}/sendPayment.t.sol (80%) rename test/{Router => AuctionHouse}/sendPayout.t.sol (76%) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 8b713d83..a759166d 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -28,18 +28,6 @@ abstract contract FeeManager { // TODO define purpose abstract contract Router is FeeManager { - using SafeTransferLib for ERC20; - - // ========== ERRORS ========== // - - error InsufficientBalance(address token_, uint256 requiredAmount_); - - error InsufficientAllowance(address token_, address spender_, uint256 requiredAmount_); - - error UnsupportedToken(address token_); - - error InvalidHook(); - // ========== STRUCTS ========== // /// @notice Parameters used by the purchase function @@ -90,13 +78,10 @@ abstract contract Router is FeeManager { // TODO make this updatable address internal immutable _PROTOCOL; - IPermit2 public immutable _PERMIT2; - // ========== CONSTRUCTOR ========== // - constructor(address protocol_, address permit2_) { + constructor(address protocol_) { _PROTOCOL = protocol_; - _PERMIT2 = IPermit2(permit2_); } // ========== ATOMIC AUCTIONS ========== // @@ -129,9 +114,210 @@ abstract contract Router is FeeManager { Auction.Bid[] memory bids_ ) external virtual returns (uint256[] memory amountsOut); - // ========== TOKEN TRANSFERS ========== // + // ========== FEE MANAGEMENT ========== // + + function setProtocolFee(uint48 protocolFee_) external { + // TOOD make this permissioned + protocolFee = protocolFee_; + } + + function setReferrerFee(address referrer_, uint48 referrerFee_) external { + // TOOD make this permissioned + referrerFees[referrer_] = referrerFee_; + } +} + +/// @title AuctionHouse +/// @notice As its name implies, the AuctionHouse is where auctions take place and the core of the protocol. +contract AuctionHouse is Derivatizer, Auctioneer, Router { + using SafeTransferLib for ERC20; + + /// Implement the router functionality here since it combines all of the base functionality + + // ========== ERRORS ========== // + + error AmountLessThanMinimum(); + + error NotAuthorized(); + + error InsufficientBalance(address token_, uint256 requiredAmount_); + + error InsufficientAllowance(address token_, address spender_, uint256 requiredAmount_); + + error UnsupportedToken(address token_); + + error InvalidHook(); + + // ========== EVENTS ========== // + + event Purchase(uint256 id, address buyer, address referrer, uint256 amount, uint256 payout); + + // ========== STATE VARIABLES ========== // + + IPermit2 public immutable _PERMIT2; + + // ========== CONSTRUCTOR ========== // + + constructor(address protocol_, address permit2_) Router(protocol_) WithModules(msg.sender) { + _PERMIT2 = IPermit2(permit2_); + } + + // ========== DIRECT EXECUTION ========== // + + // ========== AUCTION FUNCTIONS ========== // + + function _allocateFees( + address referrer_, + ERC20 quoteToken_, + uint256 amount_ + ) internal returns (uint256 totalFees) { + // 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; + } else { + 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 + toReferrer = (amount_ * referrerFee) / FEE_DECIMALS; + 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; - // TODO shift functions to AuctionHouse + return toReferrer + toProtocol; + } + + // ========== ATOMIC AUCTIONS ========== // + + /// @inheritdoc Router + /// @dev This fuction handles the following: + /// 1. Calculates the fees for the purchase + /// 2. Sends the purchase amount to the auction module + /// 3. Records the purchase on the auction module + /// 4. Transfers the quote token from the caller + /// 5. Transfers the quote token to the auction owner + /// 5. Transfers the base token from the auction owner or executes the callback + /// 6. Transfers the base token to the recipient + /// + /// This function reverts if: + /// - `lotId_` is invalid + /// - The respective auction module reverts + /// - `payout` is less than `minAmountOut_` + /// - The caller does not have sufficient balance of the quote token + /// - The auction owner does not have sufficient balance of the payout token + /// - Any of the callbacks fail + /// - Any of the token transfers fail + function purchase(PurchaseParams memory params_) + external + override + isValidLot(params_.lotId) + returns (uint256 payoutAmount) + { + // Load routing data for the lot + 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, bytes(""))) { + revert NotAuthorized(); + } + } + + uint256 totalFees = _allocateFees(params_.referrer, routing.quoteToken, params_.amount); + uint256 amountLessFees = params_.amount - totalFees; + + // Send purchase to auction house and get payout plus any extra output + bytes memory auctionOutput; + { + AuctionModule module = _getModuleForId(params_.lotId); + (payoutAmount, auctionOutput) = + module.purchase(params_.lotId, amountLessFees, params_.auctionData); + } + + // Check that payout is at least minimum amount out + // @dev Moved the slippage check from the auction to the AuctionHouse to allow different routing and purchase logic + 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 + ); + + // Send payment to auction owner + _sendPayment(routing.owner, amountLessFees, routing.quoteToken, routing.hooks); + + // Collect payout from auction owner + _collectPayout( + params_.lotId, + routing.owner, + amountLessFees, + payoutAmount, + routing.baseToken, + routing.hooks, + routing.derivativeReference, + routing.derivativeParams, + routing.wrapDerivative + ); + + // Send payout to recipient + _sendPayout( + params_.lotId, params_.recipient, payoutAmount, routing.baseToken, routing.hooks + ); + + // Emit event + emit Purchase(params_.lotId, msg.sender, params_.referrer, params_.amount, payoutAmount); + } + + // ========== BATCH AUCTIONS ========== // + + function bid( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_, + bytes calldata auctionData_, + bytes calldata approval_ + ) external override { + // TODO + } + + function settle(uint256 id_) external override returns (uint256[] memory amountsOut) { + // TODO + } + + // Off-chain auction variant + function settle( + uint256 id_, + Auction.Bid[] memory bids_ + ) external override returns (uint256[] memory amountsOut) { + // TODO + } + + // ========== TOKEN TRANSFERS ========== // /// @notice Collects payment of the quote token from the user /// @dev This function handles the following: @@ -406,193 +592,4 @@ abstract contract Router is FeeManager { revert UnsupportedToken(address(token_)); } } - - // ========== FEE MANAGEMENT ========== // - - function setProtocolFee(uint48 protocolFee_) external { - // TOOD make this permissioned - protocolFee = protocolFee_; - } - - function setReferrerFee(address referrer_, uint48 referrerFee_) external { - // TOOD make this permissioned - referrerFees[referrer_] = referrerFee_; - } -} - -/// @title AuctionHouse -/// @notice As its name implies, the AuctionHouse is where auctions take place and the core of the protocol. -contract AuctionHouse is Derivatizer, Auctioneer, Router { - using SafeTransferLib for ERC20; - - /// Implement the router functionality here since it combines all of the base functionality - - // ========== ERRORS ========== // - error AmountLessThanMinimum(); - - error NotAuthorized(); - - // ========== EVENTS ========== // - event Purchase(uint256 id, address buyer, address referrer, uint256 amount, uint256 payout); - - // ========== CONSTRUCTOR ========== // - constructor( - address protocol_, - address permit2_ - ) Router(protocol_, permit2_) WithModules(msg.sender) {} - - // ========== DIRECT EXECUTION ========== // - - // ========== AUCTION FUNCTIONS ========== // - - function _allocateFees( - address referrer_, - ERC20 quoteToken_, - uint256 amount_ - ) internal returns (uint256 totalFees) { - // 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; - } else { - 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 - toReferrer = (amount_ * referrerFee) / FEE_DECIMALS; - 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; - } - - // ========== ATOMIC AUCTIONS ========== // - - /// @inheritdoc Router - /// @dev This fuction handles the following: - /// 1. Calculates the fees for the purchase - /// 2. Sends the purchase amount to the auction module - /// 3. Records the purchase on the auction module - /// 4. Transfers the quote token from the caller - /// 5. Transfers the quote token to the auction owner - /// 5. Transfers the base token from the auction owner or executes the callback - /// 6. Transfers the base token to the recipient - /// - /// This function reverts if: - /// - `lotId_` is invalid - /// - The respective auction module reverts - /// - `payout` is less than `minAmountOut_` - /// - The caller does not have sufficient balance of the quote token - /// - The auction owner does not have sufficient balance of the payout token - /// - Any of the callbacks fail - /// - Any of the token transfers fail - function purchase(PurchaseParams memory params_) - external - override - isValidLot(params_.lotId) - returns (uint256 payoutAmount) - { - // Load routing data for the lot - 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, bytes(""))) { - revert NotAuthorized(); - } - } - - uint256 totalFees = _allocateFees(params_.referrer, routing.quoteToken, params_.amount); - uint256 amountLessFees = params_.amount - totalFees; - - // Send purchase to auction house and get payout plus any extra output - bytes memory auctionOutput; - { - AuctionModule module = _getModuleForId(params_.lotId); - (payoutAmount, auctionOutput) = - module.purchase(params_.lotId, amountLessFees, params_.auctionData); - } - - // Check that payout is at least minimum amount out - // @dev Moved the slippage check from the auction to the AuctionHouse to allow different routing and purchase logic - 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 - ); - - // Send payment to auction owner - _sendPayment(routing.owner, amountLessFees, routing.quoteToken, routing.hooks); - - // Collect payout from auction owner - _collectPayout( - params_.lotId, - routing.owner, - amountLessFees, - payoutAmount, - routing.baseToken, - routing.hooks, - routing.derivativeReference, - routing.derivativeParams, - routing.wrapDerivative - ); - - // Send payout to recipient - _sendPayout( - params_.lotId, params_.recipient, payoutAmount, routing.baseToken, routing.hooks - ); - - // Emit event - emit Purchase(params_.lotId, msg.sender, params_.referrer, params_.amount, payoutAmount); - } - - // ========== BATCH AUCTIONS ========== // - - function bid( - address recipient_, - address referrer_, - uint256 id_, - uint256 amount_, - uint256 minAmountOut_, - bytes calldata auctionData_, - bytes calldata approval_ - ) external override { - // TODO - } - - function settle(uint256 id_) external override returns (uint256[] memory amountsOut) { - // TODO - } - - // Off-chain auction variant - function settle( - uint256 id_, - Auction.Bid[] memory bids_ - ) external override returns (uint256[] memory amountsOut) { - // TODO - } } diff --git a/test/Router/ConcreteRouter.sol b/test/AuctionHouse/MockAuctionHouse.sol similarity index 66% rename from test/Router/ConcreteRouter.sol rename to test/AuctionHouse/MockAuctionHouse.sol index b0c48cb4..3578068b 100644 --- a/test/Router/ConcreteRouter.sol +++ b/test/AuctionHouse/MockAuctionHouse.sol @@ -1,41 +1,17 @@ /// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.19; -// Standard libraries import {ERC20} from "solmate/tokens/ERC20.sol"; -import {Router} from "src/AuctionHouse.sol"; -import {Auction} from "src/modules/Auction.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; import {wrapVeecode, toKeycode} from "src/modules/Modules.sol"; -contract ConcreteRouter is Router { - constructor(address protocol_, address permit2_) Router(protocol_, permit2_) {} - - function purchase(PurchaseParams memory params_) - external - virtual - override - returns (uint256 payout) - {} - - 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) {} - - function settle( - uint256 id_, - Auction.Bid[] memory bids_ - ) external virtual override returns (uint256[] memory amountsOut) {} +/// @notice Mock AuctionHouse contract for testing +/// @dev It currently exposes some internal functions so that they can be tested in isolation +contract MockAuctionHouse is AuctionHouse { + constructor(address protocol_, address permit2_) AuctionHouse(protocol_, permit2_) {} // Expose the _collectPayment function for testing function collectPayment( diff --git a/test/Router/collectPayment.t.sol b/test/AuctionHouse/collectPayment.t.sol similarity index 84% rename from test/Router/collectPayment.t.sol rename to test/AuctionHouse/collectPayment.t.sol index 2c17baad..ce264081 100644 --- a/test/Router/collectPayment.t.sol +++ b/test/AuctionHouse/collectPayment.t.sol @@ -4,17 +4,17 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; -import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; +import {MockAuctionHouse} from "test/AuctionHouse/MockAuctionHouse.sol"; import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; import {Permit2Clone} from "test/lib/permit2/Permit2Clone.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; -import {Router} from "src/AuctionHouse.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; contract CollectPaymentTest is Test, Permit2User { - ConcreteRouter internal router; + MockAuctionHouse internal auctionHouse; address internal constant PROTOCOL = address(0x1); @@ -34,7 +34,7 @@ contract CollectPaymentTest is Test, Permit2User { // Set reasonable starting block vm.warp(1_000_000); - router = new ConcreteRouter(PROTOCOL, _PERMIT2_ADDRESS); + auctionHouse = new MockAuctionHouse(PROTOCOL, _PERMIT2_ADDRESS); quoteToken = new MockFeeOnTransferERC20("QUOTE", "QT", 18); quoteToken.setTransferFee(0); @@ -49,9 +49,9 @@ contract CollectPaymentTest is Test, Permit2User { } modifier givenUserHasApprovedRouter() { - // As USER, grant approval to transfer quote tokens to the router + // As USER, grant approval to transfer quote tokens to the auctionHouse vm.prank(USER); - quoteToken.approve(address(router), amount); + quoteToken.approve(address(auctionHouse), amount); _; } @@ -89,7 +89,12 @@ contract CollectPaymentTest is Test, Permit2User { approvalNonce = _getRandomUint256(); approvalDeadline = uint48(block.timestamp + 1 days); approvalSignature = _signPermit( - address(quoteToken), amount, approvalNonce, approvalDeadline, address(router), userKey + address(quoteToken), + amount, + approvalNonce, + approvalDeadline, + address(auctionHouse), + userKey ); _; } @@ -103,7 +108,7 @@ contract CollectPaymentTest is Test, Permit2User { // Consume the nonce vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); _; @@ -120,7 +125,7 @@ contract CollectPaymentTest is Test, Permit2User { amount, approvalNonce, approvalDeadline, - address(router), + address(auctionHouse), anotherUserKey ); _; @@ -146,7 +151,12 @@ contract CollectPaymentTest is Test, Permit2User { approvalNonce = _getRandomUint256(); approvalDeadline = uint48(block.timestamp - 1 days); approvalSignature = _signPermit( - address(quoteToken), amount, approvalNonce, approvalDeadline, address(router), userKey + address(quoteToken), + amount, + approvalNonce, + approvalDeadline, + address(auctionHouse), + userKey ); _; } @@ -158,13 +168,16 @@ contract CollectPaymentTest is Test, Permit2User { { // Expect the error bytes memory err = abi.encodeWithSelector( - Router.InsufficientAllowance.selector, address(quoteToken), _PERMIT2_ADDRESS, amount + AuctionHouse.InsufficientAllowance.selector, + address(quoteToken), + _PERMIT2_ADDRESS, + amount ); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -182,7 +195,7 @@ contract CollectPaymentTest is Test, Permit2User { // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -199,7 +212,7 @@ contract CollectPaymentTest is Test, Permit2User { // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -217,7 +230,7 @@ contract CollectPaymentTest is Test, Permit2User { // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -234,7 +247,7 @@ contract CollectPaymentTest is Test, Permit2User { // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -251,7 +264,7 @@ contract CollectPaymentTest is Test, Permit2User { // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -262,13 +275,14 @@ contract CollectPaymentTest is Test, Permit2User { whenPermit2ApprovalIsValid { // Expect the error - bytes memory err = - abi.encodeWithSelector(Router.InsufficientBalance.selector, address(quoteToken), amount); + bytes memory err = abi.encodeWithSelector( + AuctionHouse.InsufficientBalance.selector, address(quoteToken), amount + ); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -282,12 +296,12 @@ contract CollectPaymentTest is Test, Permit2User { { // Expect the error bytes memory err = - abi.encodeWithSelector(Router.UnsupportedToken.selector, address(quoteToken)); + abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(quoteToken)); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -300,15 +314,15 @@ contract CollectPaymentTest is Test, Permit2User { { // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); // Expect the user to have no balance assertEq(quoteToken.balanceOf(USER), 0); - // Expect the router to have the balance - assertEq(quoteToken.balanceOf(address(router)), amount); + // Expect the auctionHouse to have the balance + assertEq(quoteToken.balanceOf(address(auctionHouse)), amount); } // ============ Transfer flow ============ @@ -326,13 +340,14 @@ contract CollectPaymentTest is Test, Permit2User { function test_transfer_whenUserHasInsufficientBalance_reverts() public { // Expect the error - bytes memory err = - abi.encodeWithSelector(Router.InsufficientBalance.selector, address(quoteToken), amount); + bytes memory err = abi.encodeWithSelector( + AuctionHouse.InsufficientBalance.selector, address(quoteToken), amount + ); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -340,13 +355,16 @@ contract CollectPaymentTest is Test, Permit2User { function test_transfer_givenNoTokenApproval_reverts() public givenUserHasBalance(amount) { // Expect the error bytes memory err = abi.encodeWithSelector( - Router.InsufficientAllowance.selector, address(quoteToken), address(router), amount + AuctionHouse.InsufficientAllowance.selector, + address(quoteToken), + address(auctionHouse), + amount ); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -359,12 +377,12 @@ contract CollectPaymentTest is Test, Permit2User { { // Expect the error bytes memory err = - abi.encodeWithSelector(Router.UnsupportedToken.selector, address(quoteToken)); + abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(quoteToken)); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -372,15 +390,15 @@ contract CollectPaymentTest is Test, Permit2User { function test_transfer() public givenUserHasBalance(amount) givenUserHasApprovedRouter { // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); // Expect the user to have no balance assertEq(quoteToken.balanceOf(USER), 0); - // Expect the router to have the balance - assertEq(quoteToken.balanceOf(address(router)), amount); + // Expect the auctionHouse to have the balance + assertEq(quoteToken.balanceOf(address(auctionHouse)), amount); } // ============ Hooks flow ============ @@ -400,7 +418,7 @@ contract CollectPaymentTest is Test, Permit2User { // Set the addresses to track address[] memory addresses = new address[](3); addresses[0] = USER; - addresses[1] = address(router); + addresses[1] = address(auctionHouse); addresses[2] = address(hook); hook.setBalanceAddresses(addresses); @@ -418,7 +436,7 @@ contract CollectPaymentTest is Test, Permit2User { // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); } @@ -431,7 +449,7 @@ contract CollectPaymentTest is Test, Permit2User { { // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); @@ -454,7 +472,7 @@ contract CollectPaymentTest is Test, Permit2User { { // Call vm.prank(USER); - router.collectPayment( + auctionHouse.collectPayment( lotId, amount, quoteToken, hook, approvalDeadline, approvalNonce, approvalSignature ); diff --git a/test/Router/collectPayout.t.sol b/test/AuctionHouse/collectPayout.t.sol similarity index 76% rename from test/Router/collectPayout.t.sol rename to test/AuctionHouse/collectPayout.t.sol index dbb49216..88eadf25 100644 --- a/test/Router/collectPayout.t.sol +++ b/test/AuctionHouse/collectPayout.t.sol @@ -4,11 +4,11 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; -import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; +import {MockAuctionHouse} from "test/AuctionHouse/MockAuctionHouse.sol"; import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; -import {Router} from "src/AuctionHouse.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; import {IAllowlist} from "src/interfaces/IAllowlist.sol"; import {Auctioneer} from "src/bases/Auctioneer.sol"; @@ -16,7 +16,7 @@ import {Auctioneer} from "src/bases/Auctioneer.sol"; import {Veecode, wrapVeecode, toKeycode} from "src/modules/Modules.sol"; contract CollectPayoutTest is Test, Permit2User { - ConcreteRouter internal router; + MockAuctionHouse internal auctionHouse; address internal constant PROTOCOL = address(0x1); @@ -38,7 +38,7 @@ contract CollectPayoutTest is Test, Permit2User { // Set reasonable starting block vm.warp(1_000_000); - router = new ConcreteRouter(PROTOCOL, _PERMIT2_ADDRESS); + auctionHouse = new MockAuctionHouse(PROTOCOL, _PERMIT2_ADDRESS); quoteToken = new MockFeeOnTransferERC20("Quote Token", "QUOTE", 18); quoteToken.setTransferFee(0); @@ -58,7 +58,7 @@ contract CollectPayoutTest is Test, Permit2User { modifier givenOwnerHasApprovedRouter() { vm.prank(OWNER); - payoutToken.approve(address(router), type(uint256).max); + payoutToken.approve(address(auctionHouse), type(uint256).max); _; } @@ -85,7 +85,7 @@ contract CollectPayoutTest is Test, Permit2User { address[] memory addresses = new address[](4); addresses[0] = USER; addresses[1] = OWNER; - addresses[2] = address(router); + addresses[2] = address(auctionHouse); addresses[3] = address(hook); hook.setBalanceAddresses(addresses); @@ -109,7 +109,7 @@ contract CollectPayoutTest is Test, Permit2User { modifier givenHookHasApprovedRouter() { vm.prank(address(hook)); - payoutToken.approve(address(router), type(uint256).max); + payoutToken.approve(address(auctionHouse), type(uint256).max); _; } @@ -123,7 +123,7 @@ contract CollectPayoutTest is Test, Permit2User { // Call vm.prank(USER); - router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); } function test_givenAuctionHasHook_whenMidHookBreaksInvariant_reverts() @@ -134,12 +134,12 @@ contract CollectPayoutTest is Test, Permit2User { whenMidHookBreaksInvariant { // Expect revert - bytes memory err = abi.encodeWithSelector(Router.InvalidHook.selector); + bytes memory err = abi.encodeWithSelector(AuctionHouse.InvalidHook.selector); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); } function test_givenAuctionHasHook_feeOnTransfer_reverts() @@ -150,12 +150,12 @@ contract CollectPayoutTest is Test, Permit2User { givenTokenTakesFeeOnTransfer { // Expect revert - bytes memory err = abi.encodeWithSelector(Router.InvalidHook.selector); + bytes memory err = abi.encodeWithSelector(AuctionHouse.InvalidHook.selector); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); } function test_givenAuctionHasHook() @@ -166,16 +166,16 @@ contract CollectPayoutTest is Test, Permit2User { { // Call vm.prank(USER); - router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); // Expect the hook to be called prior to any transfer of the payout token assertEq(hook.midHookCalled(), true); assertEq(hook.midHookBalances(payoutToken, OWNER), 0, "mid-hook: owner balance mismatch"); assertEq(hook.midHookBalances(payoutToken, USER), 0, "mid-hook: user balance mismatch"); assertEq( - hook.midHookBalances(payoutToken, address(router)), + hook.midHookBalances(payoutToken, address(auctionHouse)), 0, - "mid-hook: router balance mismatch" + "mid-hook: auctionHouse balance mismatch" ); assertEq( hook.midHookBalances(payoutToken, address(hook)), @@ -187,10 +187,14 @@ contract CollectPayoutTest is Test, Permit2User { assertEq(hook.preHookCalled(), false); assertEq(hook.postHookCalled(), false); - // Expect payout token balance to be transferred to the router + // Expect payout token balance to be transferred to the auctionHouse assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); - assertEq(payoutToken.balanceOf(address(router)), payoutAmount, "router balance mismatch"); + assertEq( + payoutToken.balanceOf(address(auctionHouse)), + payoutAmount, + "auctionHouse balance mismatch" + ); assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); } @@ -199,7 +203,7 @@ contract CollectPayoutTest is Test, Permit2User { // [X] given the auction does not have hooks defined // [X] given the auction owner has insufficient balance of the payout token // [X] it reverts - // [X] given the auction owner has not approved the router to transfer the payout token + // [X] given the auction owner has not approved the auctionHouse to transfer the payout token // [X] it reverts // [X] given transferring the payout token would result in a lesser amount being received // [X] it reverts @@ -208,28 +212,28 @@ contract CollectPayoutTest is Test, Permit2User { function test_insufficientBalance_reverts() public { // Expect revert bytes memory err = abi.encodeWithSelector( - Router.InsufficientBalance.selector, address(payoutToken), payoutAmount + AuctionHouse.InsufficientBalance.selector, address(payoutToken), payoutAmount ); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); } function test_insufficientAllowance_reverts() public givenOwnerHasBalance(payoutAmount) { // Expect revert bytes memory err = abi.encodeWithSelector( - Router.InsufficientAllowance.selector, + AuctionHouse.InsufficientAllowance.selector, address(payoutToken), - address(router), + address(auctionHouse), payoutAmount ); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); } function test_feeOnTransfer_reverts() @@ -240,23 +244,23 @@ contract CollectPayoutTest is Test, Permit2User { { // Expect revert bytes memory err = - abi.encodeWithSelector(Router.UnsupportedToken.selector, address(payoutToken)); + abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(payoutToken)); vm.expectRevert(err); // Call vm.prank(USER); - router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); } function test_success() public givenOwnerHasBalance(payoutAmount) givenOwnerHasApprovedRouter { // Call vm.prank(USER); - router.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); - // Expect payout token balance to be transferred to the router + // Expect payout token balance to be transferred to the auctionHouse assertEq(payoutToken.balanceOf(OWNER), 0); assertEq(payoutToken.balanceOf(USER), 0); - assertEq(payoutToken.balanceOf(address(router)), payoutAmount); + assertEq(payoutToken.balanceOf(address(auctionHouse)), payoutAmount); assertEq(payoutToken.balanceOf(address(hook)), 0); } @@ -266,9 +270,9 @@ contract CollectPayoutTest is Test, Permit2User { // [ ] given the auction has hooks defined // [ ] given the hook breaks the invariant // [ ] it reverts - // [ ] it succeeds - derivative is minted to the router, mid hook is called before minting + // [ ] it succeeds - derivative is minted to the auctionHouse, mid hook is called before minting // [ ] given the auction does not have hooks defined - // [ ] it succeeds - derivative is minted to the router + // [ ] it succeeds - derivative is minted to the auctionHouse // transfers base token from auction house to recipient // [ ] given the base token is a derivative diff --git a/test/Router/sendPayment.t.sol b/test/AuctionHouse/sendPayment.t.sol similarity index 80% rename from test/Router/sendPayment.t.sol rename to test/AuctionHouse/sendPayment.t.sol index 14aadd2b..0dee7b3a 100644 --- a/test/Router/sendPayment.t.sol +++ b/test/AuctionHouse/sendPayment.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; -import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; +import {MockAuctionHouse} from "test/AuctionHouse/MockAuctionHouse.sol"; import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; @@ -12,7 +12,7 @@ import {Router} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; contract SendPaymentTest is Test, Permit2User { - ConcreteRouter internal router; + MockAuctionHouse internal auctionHouse; address internal constant PROTOCOL = address(0x1); @@ -29,7 +29,7 @@ contract SendPaymentTest is Test, Permit2User { // Set reasonable starting block vm.warp(1_000_000); - router = new ConcreteRouter(PROTOCOL, _PERMIT2_ADDRESS); + auctionHouse = new MockAuctionHouse(PROTOCOL, _PERMIT2_ADDRESS); quoteToken = new MockFeeOnTransferERC20("Quote Token", "QUOTE", 18); quoteToken.setTransferFee(0); @@ -47,7 +47,7 @@ contract SendPaymentTest is Test, Permit2User { address[] memory addresses = new address[](4); addresses[0] = address(USER); addresses[1] = address(OWNER); - addresses[2] = address(router); + addresses[2] = address(auctionHouse); addresses[3] = address(hook); hook.setBalanceAddresses(addresses); @@ -55,7 +55,7 @@ contract SendPaymentTest is Test, Permit2User { } modifier givenRouterHasBalance(uint256 amount_) { - quoteToken.mint(address(router), amount_); + quoteToken.mint(address(auctionHouse), amount_); _; } @@ -66,12 +66,12 @@ contract SendPaymentTest is Test, Permit2User { { // Call vm.prank(USER); - router.sendPayment(OWNER, paymentAmount, quoteToken, hook); + auctionHouse.sendPayment(OWNER, paymentAmount, quoteToken, hook); // Check balances assertEq(quoteToken.balanceOf(USER), 0, "user balance mismatch"); assertEq(quoteToken.balanceOf(OWNER), 0, "owner balance mismatch"); - assertEq(quoteToken.balanceOf(address(router)), 0, "router balance mismatch"); + assertEq(quoteToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); assertEq(quoteToken.balanceOf(address(hook)), paymentAmount, "hook balance mismatch"); // Hooks not called @@ -83,11 +83,11 @@ contract SendPaymentTest is Test, Permit2User { function test_givenAuctionHasNoHook() public givenRouterHasBalance(paymentAmount) { // Call vm.prank(USER); - router.sendPayment(OWNER, paymentAmount, quoteToken, hook); + auctionHouse.sendPayment(OWNER, paymentAmount, quoteToken, hook); // Check balances assertEq(quoteToken.balanceOf(USER), 0, "user balance mismatch"); assertEq(quoteToken.balanceOf(OWNER), paymentAmount, "owner balance mismatch"); - assertEq(quoteToken.balanceOf(address(router)), 0, "router balance mismatch"); + assertEq(quoteToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); } } diff --git a/test/Router/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol similarity index 76% rename from test/Router/sendPayout.t.sol rename to test/AuctionHouse/sendPayout.t.sol index 27c40460..b0944aa8 100644 --- a/test/Router/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -4,15 +4,15 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; -import {ConcreteRouter} from "test/Router/ConcreteRouter.sol"; +import {MockAuctionHouse} from "test/AuctionHouse/MockAuctionHouse.sol"; import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; -import {Router} from "src/AuctionHouse.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; contract SendPayoutTest is Test, Permit2User { - ConcreteRouter internal router; + MockAuctionHouse internal auctionHouse; address internal constant PROTOCOL = address(0x1); @@ -30,7 +30,7 @@ contract SendPayoutTest is Test, Permit2User { // Set reasonable starting block vm.warp(1_000_000); - router = new ConcreteRouter(PROTOCOL, _PERMIT2_ADDRESS); + auctionHouse = new MockAuctionHouse(PROTOCOL, _PERMIT2_ADDRESS); payoutToken = new MockFeeOnTransferERC20("Payout Token", "PAYOUT", 18); payoutToken.setTransferFee(0); @@ -42,7 +42,7 @@ contract SendPayoutTest is Test, Permit2User { } modifier givenRouterHasBalance(uint256 amount_) { - payoutToken.mint(address(router), amount_); + payoutToken.mint(address(auctionHouse), amount_); _; } @@ -55,7 +55,7 @@ contract SendPayoutTest is Test, Permit2User { // [X] it reverts // [ ] when the post hook invariant is broken // [ ] it reverts - // [X] it succeeds - transfers the payout from the router to the recipient + // [X] it succeeds - transfers the payout from the auctionHouse to the recipient modifier givenAuctionHasHook() { hook = new MockHook(address(0), address(payoutToken)); @@ -64,7 +64,7 @@ contract SendPayoutTest is Test, Permit2User { address[] memory addresses = new address[](5); addresses[0] = USER; addresses[1] = OWNER; - addresses[2] = address(router); + addresses[2] = address(auctionHouse); addresses[3] = address(hook); addresses[4] = RECIPIENT; @@ -88,7 +88,7 @@ contract SendPayoutTest is Test, Permit2User { // Call vm.prank(USER); - router.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); } function test_hooks_feeOnTransfer_reverts() @@ -99,23 +99,23 @@ contract SendPayoutTest is Test, Permit2User { { // Expect revert bytes memory err = - abi.encodeWithSelector(Router.UnsupportedToken.selector, address(payoutToken)); + abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(payoutToken)); vm.expectRevert(err); // Call vm.prank(USER); - router.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); } function test_hooks() public givenAuctionHasHook givenRouterHasBalance(payoutAmount) { // Call vm.prank(USER); - router.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); // Check balances assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); - assertEq(payoutToken.balanceOf(address(router)), 0, "router balance mismatch"); + assertEq(payoutToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); assertEq(payoutToken.balanceOf(RECIPIENT), payoutAmount, "recipient balance mismatch"); @@ -126,9 +126,9 @@ contract SendPayoutTest is Test, Permit2User { assertEq(hook.postHookBalances(payoutToken, USER), 0, "post hook user balance mismatch"); assertEq(hook.postHookBalances(payoutToken, OWNER), 0, "post hook owner balance mismatch"); assertEq( - hook.postHookBalances(payoutToken, address(router)), + hook.postHookBalances(payoutToken, address(auctionHouse)), 0, - "post hook router balance mismatch" + "post hook auctionHouse balance mismatch" ); assertEq( hook.postHookBalances(payoutToken, address(hook)), 0, "post hook hook balance mismatch" @@ -145,7 +145,7 @@ contract SendPayoutTest is Test, Permit2User { // [X] given the auction does not have hooks defined // [X] given transferring the payout token would result in a lesser amount being received // [X] it reverts - // [X] it succeeds - transfers the payout from the router to the recipient + // [X] it succeeds - transfers the payout from the auctionHouse to the recipient function test_noHooks_feeOnTransfer_reverts() public @@ -154,23 +154,23 @@ contract SendPayoutTest is Test, Permit2User { { // Expect revert bytes memory err = - abi.encodeWithSelector(Router.UnsupportedToken.selector, address(payoutToken)); + abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(payoutToken)); vm.expectRevert(err); // Call vm.prank(USER); - router.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); } function test_noHooks() public givenRouterHasBalance(payoutAmount) { // Call vm.prank(USER); - router.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); // Check balances assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); - assertEq(payoutToken.balanceOf(address(router)), 0, "router balance mismatch"); + assertEq(payoutToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); assertEq(payoutToken.balanceOf(RECIPIENT), payoutAmount, "recipient balance mismatch"); } From e3adb13cd9ee2fc13970c0a41e9d88579fc1c992 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 12:06:52 +0400 Subject: [PATCH 37/82] Simplify interface to _collectPayout() --- src/AuctionHouse.sol | 54 +++++++++++--------------- test/AuctionHouse/MockAuctionHouse.sol | 18 ++------- test/AuctionHouse/collectPayout.t.sol | 31 +++++++++++---- 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index a759166d..ab0483c5 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -270,17 +270,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _sendPayment(routing.owner, amountLessFees, routing.quoteToken, routing.hooks); // Collect payout from auction owner - _collectPayout( - params_.lotId, - routing.owner, - amountLessFees, - payoutAmount, - routing.baseToken, - routing.hooks, - routing.derivativeReference, - routing.derivativeParams, - routing.wrapDerivative - ); + _collectPayout(params_.lotId, amountLessFees, payoutAmount, routing); // Send payout to recipient _sendPayout( @@ -416,57 +406,59 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// - The mid-hook invariant is violated /// /// @param lotId_ Lot ID - /// @param lotOwner_ Owner of the lot /// @param paymentAmount_ Amount of quoteToken collected (in native decimals) /// @param payoutAmount_ Amount of payoutToken to collect (in native decimals) - /// @param payoutToken_ Payout token to collect - /// @param hooks_ Hooks contract to call (optional) + /// @param routingParams_ Routing parameters for the lot function _collectPayout( uint256 lotId_, - address lotOwner_, uint256 paymentAmount_, uint256 payoutAmount_, - ERC20 payoutToken_, - IHooks hooks_, - Veecode derivativeReference, - bytes memory derivativeParams, - bool wrapDerivative + Routing memory routingParams_ ) internal { // Get the balance of the payout token before the transfer - uint256 balanceBefore = payoutToken_.balanceOf(address(this)); + uint256 balanceBefore = routingParams_.baseToken.balanceOf(address(this)); // Call mid hook on hooks contract if provided - if (address(hooks_) != address(0)) { + if (address(routingParams_.hooks) != address(0)) { // The mid hook is expected to transfer the payout token to this contract - hooks_.mid(lotId_, paymentAmount_, payoutAmount_); + routingParams_.hooks.mid(lotId_, paymentAmount_, payoutAmount_); // Check that the mid hook transferred the expected amount of payout tokens - if (payoutToken_.balanceOf(address(this)) < balanceBefore + payoutAmount_) { + if (routingParams_.baseToken.balanceOf(address(this)) < balanceBefore + payoutAmount_) { revert InvalidHook(); } } // Otherwise fallback to a standard ERC20 transfer else { // Check that the auction owner has sufficient balance of the payout token - if (payoutToken_.balanceOf(lotOwner_) < payoutAmount_) { - revert InsufficientBalance(address(payoutToken_), payoutAmount_); + if (routingParams_.baseToken.balanceOf(routingParams_.owner) < payoutAmount_) { + revert InsufficientBalance(address(routingParams_.baseToken), payoutAmount_); } // Check that the auction owner has granted approval to transfer the payout token - if (payoutToken_.allowance(lotOwner_, address(this)) < payoutAmount_) { - revert InsufficientAllowance(address(payoutToken_), address(this), payoutAmount_); + if ( + routingParams_.baseToken.allowance(routingParams_.owner, address(this)) + < payoutAmount_ + ) { + revert InsufficientAllowance( + address(routingParams_.baseToken), address(this), payoutAmount_ + ); } // Transfer the payout token from the auction owner // `safeTransferFrom()` will revert upon failure - payoutToken_.safeTransferFrom(lotOwner_, address(this), payoutAmount_); + routingParams_.baseToken.safeTransferFrom( + routingParams_.owner, address(this), payoutAmount_ + ); // Check that it is not a fee-on-transfer token - if (payoutToken_.balanceOf(address(this)) < balanceBefore + payoutAmount_) { - revert UnsupportedToken(address(payoutToken_)); + if (routingParams_.baseToken.balanceOf(address(this)) < balanceBefore + payoutAmount_) { + revert UnsupportedToken(address(routingParams_.baseToken)); } } + // TODO payout token needs to be collected from the auction owner in case of derivative + // TODO handle derivative } diff --git a/test/AuctionHouse/MockAuctionHouse.sol b/test/AuctionHouse/MockAuctionHouse.sol index 3578068b..0eca5c3f 100644 --- a/test/AuctionHouse/MockAuctionHouse.sol +++ b/test/AuctionHouse/MockAuctionHouse.sol @@ -6,7 +6,7 @@ import {ERC20} from "solmate/tokens/ERC20.sol"; import {AuctionHouse} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; -import {wrapVeecode, toKeycode} from "src/modules/Modules.sol"; +import {Auctioneer} from "src/bases/Auctioneer.sol"; /// @notice Mock AuctionHouse contract for testing /// @dev It currently exposes some internal functions so that they can be tested in isolation @@ -36,23 +36,11 @@ contract MockAuctionHouse is AuctionHouse { function collectPayout( uint256 lotId_, - address lotOwner_, uint256 paymentAmount_, uint256 payoutAmount_, - ERC20 payoutToken_, - IHooks hooks_ + Auctioneer.Routing memory routingParams_ ) external { - return _collectPayout( - lotId_, - lotOwner_, - paymentAmount_, - payoutAmount_, - payoutToken_, - hooks_, - wrapVeecode(toKeycode("MOCK"), 1), - bytes(""), - false - ); + return _collectPayout(lotId_, paymentAmount_, payoutAmount_, routingParams_); } function sendPayment( diff --git a/test/AuctionHouse/collectPayout.t.sol b/test/AuctionHouse/collectPayout.t.sol index 88eadf25..cfa790dd 100644 --- a/test/AuctionHouse/collectPayout.t.sol +++ b/test/AuctionHouse/collectPayout.t.sol @@ -34,6 +34,8 @@ contract CollectPayoutTest is Test, Permit2User { bytes internal derivativeParams; bool internal wrapDerivative; + Auctioneer.Routing internal routingParams; + function setUp() public { // Set reasonable starting block vm.warp(1_000_000); @@ -49,6 +51,18 @@ contract CollectPayoutTest is Test, Permit2User { derivativeReference = wrapVeecode(toKeycode(""), 0); derivativeParams = bytes(""); wrapDerivative = false; + + routingParams = Auctioneer.Routing({ + auctionReference: wrapVeecode(toKeycode("MOCK"), 1), + owner: OWNER, + baseToken: payoutToken, + quoteToken: quoteToken, + hooks: hook, + allowlist: IAllowlist(address(0)), + derivativeReference: derivativeReference, + derivativeParams: derivativeParams, + wrapDerivative: wrapDerivative + }); } modifier givenOwnerHasBalance(uint256 amount_) { @@ -80,6 +94,7 @@ contract CollectPayoutTest is Test, Permit2User { modifier givenAuctionHasHook() { hook = new MockHook(address(quoteToken), address(payoutToken)); + routingParams.hooks = hook; // Set the addresses to track address[] memory addresses = new address[](4); @@ -123,7 +138,7 @@ contract CollectPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); } function test_givenAuctionHasHook_whenMidHookBreaksInvariant_reverts() @@ -139,7 +154,7 @@ contract CollectPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); } function test_givenAuctionHasHook_feeOnTransfer_reverts() @@ -155,7 +170,7 @@ contract CollectPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); } function test_givenAuctionHasHook() @@ -166,7 +181,7 @@ contract CollectPayoutTest is Test, Permit2User { { // Call vm.prank(USER); - auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); // Expect the hook to be called prior to any transfer of the payout token assertEq(hook.midHookCalled(), true); @@ -218,7 +233,7 @@ contract CollectPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); } function test_insufficientAllowance_reverts() public givenOwnerHasBalance(payoutAmount) { @@ -233,7 +248,7 @@ contract CollectPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); } function test_feeOnTransfer_reverts() @@ -249,13 +264,13 @@ contract CollectPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); } function test_success() public givenOwnerHasBalance(payoutAmount) givenOwnerHasApprovedRouter { // Call vm.prank(USER); - auctionHouse.collectPayout(lotId, OWNER, paymentAmount, payoutAmount, payoutToken, hook); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); // Expect payout token balance to be transferred to the auctionHouse assertEq(payoutToken.balanceOf(OWNER), 0); From 8c2ca1a5f98730e73c6810d9b44c73d218885d9f Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 12:29:15 +0400 Subject: [PATCH 38/82] Adds support for derivatives in _collectPayout() --- src/AuctionHouse.sol | 11 +- test/AuctionHouse/collectPayout.t.sol | 184 +++++++++++++++++++++++--- test/AuctionHouse/sendPayout.t.sol | 10 ++ 3 files changed, 182 insertions(+), 23 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index ab0483c5..51564d4c 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -396,6 +396,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 lot is a derivative, transfers the payout token to the derivative module /// /// This function reverts if: /// - Approval has not been granted to transfer the payout token @@ -457,9 +458,15 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } } - // TODO payout token needs to be collected from the auction owner in case of derivative + // If the lot is a derivative, transfer the base token as collateral to the derivative module + if (fromVeecode(routingParams_.derivativeReference) != bytes7("")) { + // Get the details of the derivative module + address derivativeModuleAddress = + _getModuleIfInstalled(routingParams_.derivativeReference); - // TODO handle derivative + // Transfer the base token to the derivative module + routingParams_.baseToken.safeTransfer(derivativeModuleAddress, payoutAmount_); + } } /// @notice Sends the payout token to the recipient diff --git a/test/AuctionHouse/collectPayout.t.sol b/test/AuctionHouse/collectPayout.t.sol index cfa790dd..0f8cd296 100644 --- a/test/AuctionHouse/collectPayout.t.sol +++ b/test/AuctionHouse/collectPayout.t.sol @@ -5,6 +5,7 @@ import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; import {MockAuctionHouse} from "test/AuctionHouse/MockAuctionHouse.sol"; +import {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule.sol"; import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; @@ -13,10 +14,11 @@ import {IHooks} from "src/interfaces/IHooks.sol"; import {IAllowlist} from "src/interfaces/IAllowlist.sol"; import {Auctioneer} from "src/bases/Auctioneer.sol"; -import {Veecode, wrapVeecode, toKeycode} from "src/modules/Modules.sol"; +import {Veecode, wrapVeecode, toVeecode, toKeycode} from "src/modules/Modules.sol"; contract CollectPayoutTest is Test, Permit2User { MockAuctionHouse internal auctionHouse; + MockDerivativeModule internal mockDerivativeModule; address internal constant PROTOCOL = address(0x1); @@ -41,6 +43,7 @@ contract CollectPayoutTest is Test, Permit2User { vm.warp(1_000_000); auctionHouse = new MockAuctionHouse(PROTOCOL, _PERMIT2_ADDRESS); + mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); quoteToken = new MockFeeOnTransferERC20("Quote Token", "QUOTE", 18); quoteToken.setTransferFee(0); @@ -48,7 +51,7 @@ contract CollectPayoutTest is Test, Permit2User { payoutToken = new MockFeeOnTransferERC20("Payout Token", "PAYOUT", 18); payoutToken.setTransferFee(0); - derivativeReference = wrapVeecode(toKeycode(""), 0); + derivativeReference = toVeecode(bytes7("")); derivativeParams = bytes(""); wrapDerivative = false; @@ -97,11 +100,12 @@ contract CollectPayoutTest is Test, Permit2User { routingParams.hooks = hook; // Set the addresses to track - address[] memory addresses = new address[](4); + address[] memory addresses = new address[](5); addresses[0] = USER; addresses[1] = OWNER; addresses[2] = address(auctionHouse); addresses[3] = address(hook); + addresses[4] = address(mockDerivativeModule); hook.setBalanceAddresses(addresses); _; @@ -197,6 +201,11 @@ contract CollectPayoutTest is Test, Permit2User { payoutAmount, "mid-hook: hook balance mismatch" ); + assertEq( + hook.midHookBalances(payoutToken, address(mockDerivativeModule)), + 0, + "mid-hook: derivativeModule balance mismatch" + ); // Expect the other hooks not to be called assertEq(hook.preHookCalled(), false); @@ -211,6 +220,11 @@ contract CollectPayoutTest is Test, Permit2User { "auctionHouse balance mismatch" ); assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); + assertEq( + payoutToken.balanceOf(address(mockDerivativeModule)), + 0, + "derivativeModule balance mismatch" + ); } // ========== Non-hooks flow ========== // @@ -277,30 +291,158 @@ contract CollectPayoutTest is Test, Permit2User { assertEq(payoutToken.balanceOf(USER), 0); assertEq(payoutToken.balanceOf(address(auctionHouse)), payoutAmount); assertEq(payoutToken.balanceOf(address(hook)), 0); + assertEq(payoutToken.balanceOf(address(mockDerivativeModule)), 0); } // ========== Derivative flow ========== // - // [ ] given the auction has a derivative defined - // [ ] given the auction has hooks defined - // [ ] given the hook breaks the invariant - // [ ] it reverts - // [ ] it succeeds - derivative is minted to the auctionHouse, mid hook is called before minting - // [ ] given the auction does not have hooks defined - // [ ] it succeeds - derivative is minted to the auctionHouse - - // transfers base token from auction house to recipient - // [ ] given the base token is a derivative - // [ ] given a condenser is set - // [ ] it uses the condenser to determine derivative parameters - // [ ] given a condenser is not set - // [ ] it uses the routing derivative parameters - // [ ] it mints derivative tokens to the recipient using the derivative module - // [ ] given the base token is not a derivative - // [ ] it transfers the base token to the recipient + // [X] given the auction has a derivative defined + // [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] given the auction does not have hooks defined + // [X] given the auction owner has insufficient balance of the payout token + // [X] it reverts + // [X] given the auction owner has not approved the auctionHouse to transfer the payout token + // [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 modifier givenAuctionHasDerivative() { - derivativeReference = wrapVeecode(toKeycode("DERV"), 1); + // Install the derivative module + auctionHouse.installModule(mockDerivativeModule); + + // Update parameters + derivativeReference = mockDerivativeModule.VEECODE(); + routingParams.derivativeReference = derivativeReference; _; } + + function test_derivative_hasHook_whenHookBreaksInvariant_reverts() + public + givenAuctionHasDerivative + givenAuctionHasHook + givenHookHasBalance(payoutAmount) + givenHookHasApprovedRouter + whenMidHookBreaksInvariant + { + // Expect revert + bytes memory err = abi.encodeWithSelector(AuctionHouse.InvalidHook.selector); + vm.expectRevert(err); + + // Call + vm.prank(USER); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); + } + + function test_derivative_hasHook_success() + public + givenAuctionHasDerivative + givenAuctionHasHook + givenHookHasBalance(payoutAmount) + givenHookHasApprovedRouter + { + // Call + vm.prank(USER); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); + + // Expect payout token balance to be transferred to the derivative module + assertEq(payoutToken.balanceOf(OWNER), 0); + assertEq(payoutToken.balanceOf(USER), 0); + assertEq(payoutToken.balanceOf(address(auctionHouse)), 0); + assertEq(payoutToken.balanceOf(address(hook)), 0); + assertEq(payoutToken.balanceOf(address(mockDerivativeModule)), payoutAmount); + + // Expect the hook to be called prior to any transfer of the payout token + assertEq(hook.midHookCalled(), true); + assertEq(hook.midHookBalances(payoutToken, OWNER), 0, "mid-hook: owner balance mismatch"); + assertEq(hook.midHookBalances(payoutToken, USER), 0, "mid-hook: user balance mismatch"); + assertEq( + hook.midHookBalances(payoutToken, address(auctionHouse)), + 0, + "mid-hook: auctionHouse balance mismatch" + ); + assertEq( + hook.midHookBalances(payoutToken, address(hook)), + payoutAmount, + "mid-hook: hook balance mismatch" + ); + assertEq( + hook.midHookBalances(payoutToken, address(mockDerivativeModule)), + 0, + "mid-hook: derivativeModule balance mismatch" + ); + + // Expect the other hooks not to be called + assertEq(hook.preHookCalled(), false); + assertEq(hook.postHookCalled(), false); + } + + function test_derivative_insufficientBalance_reverts() public givenAuctionHasDerivative { + // Expect revert + bytes memory err = abi.encodeWithSelector( + AuctionHouse.InsufficientBalance.selector, address(payoutToken), payoutAmount + ); + vm.expectRevert(err); + + // Call + vm.prank(USER); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); + } + + function test_derivative_insufficientAllowance_reverts() + public + givenAuctionHasDerivative + givenOwnerHasBalance(payoutAmount) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + AuctionHouse.InsufficientAllowance.selector, + address(payoutToken), + address(auctionHouse), + payoutAmount + ); + vm.expectRevert(err); + + // Call + vm.prank(USER); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); + } + + function test_derivative_feeOnTransfer_reverts() + public + givenAuctionHasDerivative + givenOwnerHasBalance(payoutAmount) + givenOwnerHasApprovedRouter + givenTokenTakesFeeOnTransfer + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(payoutToken)); + vm.expectRevert(err); + + // Call + vm.prank(USER); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); + } + + function test_derivative_success() + public + givenAuctionHasDerivative + givenOwnerHasBalance(payoutAmount) + givenOwnerHasApprovedRouter + { + // Call + vm.prank(USER); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); + + // Expect payout token balance to be transferred to the derivative module + assertEq(payoutToken.balanceOf(OWNER), 0); + assertEq(payoutToken.balanceOf(USER), 0); + assertEq(payoutToken.balanceOf(address(auctionHouse)), 0); + assertEq(payoutToken.balanceOf(address(hook)), 0); + assertEq(payoutToken.balanceOf(address(mockDerivativeModule)), payoutAmount); + } } diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index b0944aa8..8c601f85 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -174,4 +174,14 @@ contract SendPayoutTest is Test, Permit2User { assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); assertEq(payoutToken.balanceOf(RECIPIENT), payoutAmount, "recipient balance mismatch"); } + + // transfers base token from auction house to recipient + // [ ] given the base token is a derivative + // [ ] given a condenser is set + // [ ] it uses the condenser to determine derivative parameters + // [ ] given a condenser is not set + // [ ] it uses the routing derivative parameters + // [ ] it mints derivative tokens to the recipient using the derivative module + // [ ] given the base token is not a derivative + // [ ] it transfers the base token to the recipient } From 36e1c756a356f564d49a7d8e0c7acc7ddd5df2e3 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 12:42:57 +0400 Subject: [PATCH 39/82] Shift _sendPayout() to take Routing parameters --- src/AuctionHouse.sol | 22 ++++----- test/AuctionHouse/MockAuctionHouse.sol | 23 +++++----- test/AuctionHouse/sendPayout.t.sol | 63 +++++++++++++++++++++++--- 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 51564d4c..a0ed0202 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -273,9 +273,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _collectPayout(params_.lotId, amountLessFees, payoutAmount, routing); // Send payout to recipient - _sendPayout( - params_.lotId, params_.recipient, payoutAmount, routing.baseToken, routing.hooks - ); + _sendPayout(params_.lotId, params_.recipient, payoutAmount, routing); // Emit event emit Purchase(params_.lotId, msg.sender, params_.referrer, params_.amount, payoutAmount); @@ -487,29 +485,27 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @param lotId_ Lot ID /// @param recipient_ Address to receive payout /// @param payoutAmount_ Amount of payoutToken to send (in native decimals) - /// @param payoutToken_ Payout token to send - /// @param hooks_ Hooks contract to call (optional) + /// @param routingParams_ Routing parameters for the lot function _sendPayout( uint256 lotId_, address recipient_, uint256 payoutAmount_, - ERC20 payoutToken_, - IHooks hooks_ + Routing memory routingParams_ ) internal { // Get the pre-transfer balance - uint256 balanceBefore = payoutToken_.balanceOf(recipient_); + uint256 balanceBefore = routingParams_.baseToken.balanceOf(recipient_); // Send payout token to recipient - payoutToken_.safeTransfer(recipient_, payoutAmount_); + routingParams_.baseToken.safeTransfer(recipient_, payoutAmount_); // Check that the recipient received the expected amount of payout tokens - if (payoutToken_.balanceOf(recipient_) < balanceBefore + payoutAmount_) { - revert UnsupportedToken(address(payoutToken_)); + if (routingParams_.baseToken.balanceOf(recipient_) < balanceBefore + payoutAmount_) { + revert UnsupportedToken(address(routingParams_.baseToken)); } // Call post hook on hooks contract if provided - if (address(hooks_) != address(0)) { - hooks_.post(lotId_, payoutAmount_); + if (address(routingParams_.hooks) != address(0)) { + routingParams_.hooks.post(lotId_, payoutAmount_); } } diff --git a/test/AuctionHouse/MockAuctionHouse.sol b/test/AuctionHouse/MockAuctionHouse.sol index 0eca5c3f..21dcf25a 100644 --- a/test/AuctionHouse/MockAuctionHouse.sol +++ b/test/AuctionHouse/MockAuctionHouse.sol @@ -34,15 +34,6 @@ contract MockAuctionHouse is AuctionHouse { ); } - function collectPayout( - uint256 lotId_, - uint256 paymentAmount_, - uint256 payoutAmount_, - Auctioneer.Routing memory routingParams_ - ) external { - return _collectPayout(lotId_, paymentAmount_, payoutAmount_, routingParams_); - } - function sendPayment( address lotOwner_, uint256 paymentAmount_, @@ -52,13 +43,21 @@ contract MockAuctionHouse is AuctionHouse { return _sendPayment(lotOwner_, paymentAmount_, quoteToken_, hooks_); } + function collectPayout( + uint256 lotId_, + uint256 paymentAmount_, + uint256 payoutAmount_, + Auctioneer.Routing memory routingParams_ + ) external { + return _collectPayout(lotId_, paymentAmount_, payoutAmount_, routingParams_); + } + function sendPayout( uint256 lotId_, address recipient_, uint256 payoutAmount_, - ERC20 payoutToken_, - IHooks hooks_ + Auctioneer.Routing memory routingParams_ ) external { - return _sendPayout(lotId_, recipient_, payoutAmount_, payoutToken_, hooks_); + return _sendPayout(lotId_, recipient_, payoutAmount_, routingParams_); } } diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index 8c601f85..f7dd8ac0 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -5,14 +5,20 @@ import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; import {MockAuctionHouse} from "test/AuctionHouse/MockAuctionHouse.sol"; +import {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule.sol"; import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; import {AuctionHouse} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; +import {IAllowlist} from "src/interfaces/IAllowlist.sol"; +import {Auctioneer} from "src/bases/Auctioneer.sol"; + +import {Veecode, toVeecode, wrapVeecode, toKeycode} from "src/modules/Modules.sol"; contract SendPayoutTest is Test, Permit2User { MockAuctionHouse internal auctionHouse; + MockDerivativeModule internal mockDerivativeModule; address internal constant PROTOCOL = address(0x1); @@ -23,17 +29,43 @@ contract SendPayoutTest is Test, Permit2User { // Function parameters uint256 internal lotId = 1; uint256 internal payoutAmount = 10e18; + MockFeeOnTransferERC20 internal quoteToken; MockFeeOnTransferERC20 internal payoutToken; MockHook internal hook; + Veecode internal derivativeReference; + bytes internal derivativeParams; + bool internal wrapDerivative; + + Auctioneer.Routing internal routingParams; function setUp() public { // Set reasonable starting block vm.warp(1_000_000); auctionHouse = new MockAuctionHouse(PROTOCOL, _PERMIT2_ADDRESS); + mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); + + quoteToken = new MockFeeOnTransferERC20("Quote Token", "QUOTE", 18); + quoteToken.setTransferFee(0); payoutToken = new MockFeeOnTransferERC20("Payout Token", "PAYOUT", 18); payoutToken.setTransferFee(0); + + derivativeReference = toVeecode(bytes7("")); + derivativeParams = bytes(""); + wrapDerivative = false; + + routingParams = Auctioneer.Routing({ + auctionReference: wrapVeecode(toKeycode("MOCK"), 1), + owner: OWNER, + baseToken: payoutToken, + quoteToken: quoteToken, + hooks: hook, + allowlist: IAllowlist(address(0)), + derivativeReference: derivativeReference, + derivativeParams: derivativeParams, + wrapDerivative: wrapDerivative + }); } modifier givenTokenTakesFeeOnTransfer() { @@ -59,14 +91,16 @@ contract SendPayoutTest is Test, Permit2User { modifier givenAuctionHasHook() { hook = new MockHook(address(0), address(payoutToken)); + routingParams.hooks = hook; // Set the addresses to track - address[] memory addresses = new address[](5); + address[] memory addresses = new address[](6); addresses[0] = USER; addresses[1] = OWNER; addresses[2] = address(auctionHouse); addresses[3] = address(hook); addresses[4] = RECIPIENT; + addresses[5] = address(mockDerivativeModule); hook.setBalanceAddresses(addresses); _; @@ -88,7 +122,7 @@ contract SendPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams); } function test_hooks_feeOnTransfer_reverts() @@ -104,13 +138,13 @@ contract SendPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams); } function test_hooks() public givenAuctionHasHook givenRouterHasBalance(payoutAmount) { // Call vm.prank(USER); - auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams); // Check balances assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); @@ -118,6 +152,11 @@ contract SendPayoutTest is Test, Permit2User { assertEq(payoutToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); assertEq(payoutToken.balanceOf(RECIPIENT), payoutAmount, "recipient balance mismatch"); + assertEq( + payoutToken.balanceOf(address(mockDerivativeModule)), + 0, + "derivative module balance mismatch" + ); // Check the hook was called at the right time assertEq(hook.preHookCalled(), false, "pre hook mismatch"); @@ -138,6 +177,11 @@ contract SendPayoutTest is Test, Permit2User { payoutAmount, "post hook recipient balance mismatch" ); + assertEq( + hook.postHookBalances(payoutToken, address(mockDerivativeModule)), + 0, + "post hook derivative module balance mismatch" + ); } // ========== Non-hooks flow ========== // @@ -159,13 +203,13 @@ contract SendPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams); } function test_noHooks() public givenRouterHasBalance(payoutAmount) { // Call vm.prank(USER); - auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, payoutToken, hook); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams); // Check balances assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); @@ -173,8 +217,15 @@ contract SendPayoutTest is Test, Permit2User { assertEq(payoutToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); assertEq(payoutToken.balanceOf(RECIPIENT), payoutAmount, "recipient balance mismatch"); + assertEq( + payoutToken.balanceOf(address(mockDerivativeModule)), + 0, + "derivative module balance mismatch" + ); } + // ========== Derivative flow ========== // + // transfers base token from auction house to recipient // [ ] given the base token is a derivative // [ ] given a condenser is set From 7f5b4cc010979f205fc2d0c621920d254c6fd5e5 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 13:24:46 +0400 Subject: [PATCH 40/82] Tests for condenser support in _sendPayout() --- src/AuctionHouse.sol | 6 +- test/AuctionHouse/MockAuctionHouse.sol | 5 +- test/AuctionHouse/purchase.t.sol | 2 +- test/AuctionHouse/sendPayout.t.sol | 183 ++++++++++++++++-- .../modules/Condenser/MockCondenserModule.sol | 11 +- .../Derivative/MockDerivativeModule.sol | 21 +- 6 files changed, 203 insertions(+), 25 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index a0ed0202..0485a163 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -273,7 +273,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _collectPayout(params_.lotId, amountLessFees, payoutAmount, routing); // Send payout to recipient - _sendPayout(params_.lotId, params_.recipient, payoutAmount, routing); + _sendPayout(params_.lotId, params_.recipient, payoutAmount, routing, auctionOutput); // Emit event emit Purchase(params_.lotId, msg.sender, params_.referrer, params_.amount, payoutAmount); @@ -486,11 +486,13 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @param recipient_ Address to receive payout /// @param payoutAmount_ Amount of payoutToken to send (in native decimals) /// @param routingParams_ Routing parameters for the lot + /// @param auctionOutput_ Custom data returned by the auction module function _sendPayout( uint256 lotId_, address recipient_, uint256 payoutAmount_, - Routing memory routingParams_ + Routing memory routingParams_, + bytes memory auctionOutput_ ) internal { // Get the pre-transfer balance uint256 balanceBefore = routingParams_.baseToken.balanceOf(recipient_); diff --git a/test/AuctionHouse/MockAuctionHouse.sol b/test/AuctionHouse/MockAuctionHouse.sol index 21dcf25a..0db87252 100644 --- a/test/AuctionHouse/MockAuctionHouse.sol +++ b/test/AuctionHouse/MockAuctionHouse.sol @@ -56,8 +56,9 @@ contract MockAuctionHouse is AuctionHouse { uint256 lotId_, address recipient_, uint256 payoutAmount_, - Auctioneer.Routing memory routingParams_ + Auctioneer.Routing memory routingParams_, + bytes memory auctionOutput_ ) external { - return _sendPayout(lotId_, recipient_, payoutAmount_, routingParams_); + return _sendPayout(lotId_, recipient_, payoutAmount_, routingParams_, auctionOutput_); } } diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 12179f82..3175026a 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -85,7 +85,7 @@ contract PurchaseTest is Test, Permit2User { mockAllowlist = new MockAllowlist(); mockHook = new MockHook(address(quoteToken), address(baseToken)); - mockDerivativeModule.setDerivativeToken(baseToken); + // mockDerivativeModule.setDerivativeToken(baseToken); auctionParams = Auction.AuctionParams({ start: uint48(block.timestamp), diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index f7dd8ac0..ccd6824b 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -5,8 +5,11 @@ import {Test} from "forge-std/Test.sol"; import {MockHook} from "test/modules/Auction/MockHook.sol"; import {MockAuctionHouse} from "test/AuctionHouse/MockAuctionHouse.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 {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; +import {MockERC6909} from "solmate/test/utils/mocks/MockERC6909.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; import {AuctionHouse} from "src/AuctionHouse.sol"; @@ -18,7 +21,9 @@ import {Veecode, toVeecode, wrapVeecode, toKeycode} from "src/modules/Modules.so contract SendPayoutTest is Test, Permit2User { MockAuctionHouse internal auctionHouse; + MockAtomicAuctionModule internal mockAuctionModule; MockDerivativeModule internal mockDerivativeModule; + MockCondenserModule internal mockCondenserModule; address internal constant PROTOCOL = address(0x1); @@ -31,10 +36,14 @@ contract SendPayoutTest is Test, Permit2User { uint256 internal payoutAmount = 10e18; MockFeeOnTransferERC20 internal quoteToken; MockFeeOnTransferERC20 internal payoutToken; + MockERC6909 internal derivativeToken; MockHook internal hook; Veecode internal derivativeReference; + uint256 internal derivativeTokenId; bytes internal derivativeParams; bool internal wrapDerivative; + uint256 internal auctionOutputMultiplier; + bytes internal auctionOutput; Auctioneer.Routing internal routingParams; @@ -43,7 +52,9 @@ contract SendPayoutTest is Test, Permit2User { vm.warp(1_000_000); auctionHouse = new MockAuctionHouse(PROTOCOL, _PERMIT2_ADDRESS); + mockAuctionModule = new MockAtomicAuctionModule(address(auctionHouse)); mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); + mockCondenserModule = new MockCondenserModule(address(auctionHouse)); quoteToken = new MockFeeOnTransferERC20("Quote Token", "QUOTE", 18); quoteToken.setTransferFee(0); @@ -51,12 +62,16 @@ contract SendPayoutTest is Test, Permit2User { payoutToken = new MockFeeOnTransferERC20("Payout Token", "PAYOUT", 18); payoutToken.setTransferFee(0); + derivativeToken = new MockERC6909(); + derivativeReference = toVeecode(bytes7("")); derivativeParams = bytes(""); wrapDerivative = false; + auctionOutputMultiplier = 2; + auctionOutput = abi.encode(auctionOutputMultiplier); // Does nothing unless the condenser is set routingParams = Auctioneer.Routing({ - auctionReference: wrapVeecode(toKeycode("MOCK"), 1), + auctionReference: mockAuctionModule.VEECODE(), owner: OWNER, baseToken: payoutToken, quoteToken: quoteToken, @@ -122,7 +137,7 @@ contract SendPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); } function test_hooks_feeOnTransfer_reverts() @@ -138,13 +153,13 @@ contract SendPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); } function test_hooks() public givenAuctionHasHook givenRouterHasBalance(payoutAmount) { // Call vm.prank(USER); - auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); // Check balances assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); @@ -203,13 +218,13 @@ contract SendPayoutTest is Test, Permit2User { // Call vm.prank(USER); - auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); } function test_noHooks() public givenRouterHasBalance(payoutAmount) { // Call vm.prank(USER); - auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); // Check balances assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); @@ -226,13 +241,157 @@ contract SendPayoutTest is Test, Permit2User { // ========== Derivative flow ========== // - // transfers base token from auction house to recipient // [ ] given the base token is a derivative // [ ] given a condenser is set - // [ ] it uses the condenser to determine derivative parameters + // [ ] given the derivative parameters are invalid + // [ ] it reverts + // [X] it uses the condenser to determine derivative parameters // [ ] given a condenser is not set - // [ ] it uses the routing derivative parameters - // [ ] it mints derivative tokens to the recipient using the derivative module - // [ ] given the base token is not a derivative - // [ ] it transfers the base token to the recipient + // [ ] given the derivative is wrapped + // [ ] given the derivative parameters are invalid + // [ ] it reverts + // [ ] it mints wrapped derivative tokens to the recipient using the derivative module + // [X] given the derivative is not wrapped + // [X] given the derivative parameters are invalid + // [X] it reverts + // [X] it mints derivative tokens to the recipient using the derivative module + + modifier givenAuctionHasDerivative() { + // Install the derivative module + auctionHouse.installModule(mockDerivativeModule); + + mockDerivativeModule.setDerivativeToken(derivativeToken); + + // Update parameters + derivativeReference = mockDerivativeModule.VEECODE(); + derivativeTokenId = 20; + derivativeParams = abi.encode(derivativeTokenId, 0); + routingParams.derivativeReference = derivativeReference; + routingParams.derivativeParams = derivativeParams; + _; + } + + modifier givenDerivativeIsWrapped() { + wrapDerivative = true; + routingParams.wrapDerivative = wrapDerivative; + _; + } + + modifier givenDerivativeHasCondenser() { + // Install the condenser module + auctionHouse.installModule(mockCondenserModule); + + // Set the condenser + auctionHouse.setCondenser( + mockAuctionModule.VEECODE(), + mockDerivativeModule.VEECODE(), + mockCondenserModule.VEECODE() + ); + _; + } + + modifier givenDerivativeParamsAreInvalid() { + derivativeParams = abi.encode(0); + routingParams.derivativeParams = derivativeParams; + _; + } + + function test_derivative_invalidParams() + public + givenAuctionHasDerivative + givenDerivativeParamsAreInvalid + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(MockDerivativeModule.InvalidDerivativeParams.selector); + vm.expectRevert(err); + + // Call + vm.prank(USER); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); + } + + function test_derivative() public givenAuctionHasDerivative { + // Call + vm.prank(USER); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); + + // Check balances of the derivative token + assertEq(derivativeToken.balanceOf(USER, derivativeTokenId), 0, "user balance mismatch"); + assertEq(derivativeToken.balanceOf(OWNER, derivativeTokenId), 0, "owner balance mismatch"); + assertEq( + derivativeToken.balanceOf(address(auctionHouse), derivativeTokenId), + 0, + "auctionHouse balance mismatch" + ); + assertEq( + derivativeToken.balanceOf(address(hook), derivativeTokenId), 0, "hook balance mismatch" + ); + assertEq( + derivativeToken.balanceOf(RECIPIENT, derivativeTokenId), + payoutAmount, + "recipient balance mismatch" + ); + assertEq( + derivativeToken.balanceOf(address(mockDerivativeModule), derivativeTokenId), + 0, + "derivative module balance mismatch" + ); + + // Check balances of payout token + assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); + assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); + assertEq(payoutToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); + assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); + assertEq(payoutToken.balanceOf(RECIPIENT), 0, "recipient balance mismatch"); + assertEq( + payoutToken.balanceOf(address(mockDerivativeModule)), + 0, // This would normally be non-zero, but we didn't transfer the collateral to it + "derivative module balance mismatch" + ); + } + + function test_derivative_condenser() + public + givenAuctionHasDerivative + givenDerivativeHasCondenser + { + // Call + vm.prank(USER); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); + + // Check balances of the derivative token + assertEq(derivativeToken.balanceOf(USER, derivativeTokenId), 0, "user balance mismatch"); + assertEq(derivativeToken.balanceOf(OWNER, derivativeTokenId), 0, "owner balance mismatch"); + assertEq( + derivativeToken.balanceOf(address(auctionHouse), derivativeTokenId), + 0, + "auctionHouse balance mismatch" + ); + assertEq( + derivativeToken.balanceOf(address(hook), derivativeTokenId), 0, "hook balance mismatch" + ); + assertEq( + derivativeToken.balanceOf(RECIPIENT, derivativeTokenId), + payoutAmount * auctionOutputMultiplier, // Condenser multiplies the payout + "recipient balance mismatch" + ); + assertEq( + derivativeToken.balanceOf(address(mockDerivativeModule), derivativeTokenId), + 0, + "derivative module balance mismatch" + ); + + // Check balances of payout token + assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); + assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); + assertEq(payoutToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); + assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); + assertEq(payoutToken.balanceOf(RECIPIENT), 0, "recipient balance mismatch"); + assertEq( + payoutToken.balanceOf(address(mockDerivativeModule)), + 0, // This would normally be non-zero, but we didn't transfer the collateral to it + "derivative module balance mismatch" + ); + } } diff --git a/test/modules/Condenser/MockCondenserModule.sol b/test/modules/Condenser/MockCondenserModule.sol index f211cce5..1bdd55b1 100644 --- a/test/modules/Condenser/MockCondenserModule.sol +++ b/test/modules/Condenser/MockCondenserModule.sol @@ -21,5 +21,14 @@ contract MockCondenserModule is CondenserModule { function condense( bytes memory auctionOutput_, bytes memory derivativeConfig_ - ) external pure virtual override returns (bytes memory) {} + ) external pure virtual override returns (bytes memory) { + // Get auction output + (uint256 auctionMultiplier) = abi.decode(auctionOutput_, (uint256)); + + // Get derivative params + (uint256 derivativeTokenId) = abi.decode(derivativeConfig_, (uint256)); + + // Return condensed output + return abi.encode(derivativeTokenId, auctionMultiplier); + } } diff --git a/test/modules/Derivative/MockDerivativeModule.sol b/test/modules/Derivative/MockDerivativeModule.sol index 4b5d61a4..9a6dfdc5 100644 --- a/test/modules/Derivative/MockDerivativeModule.sol +++ b/test/modules/Derivative/MockDerivativeModule.sol @@ -7,11 +7,13 @@ import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; // Auctions import {DerivativeModule} from "src/modules/Derivative.sol"; -import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {MockERC6909} from "solmate/test/utils/mocks/MockERC6909.sol"; contract MockDerivativeModule is DerivativeModule { bool internal validateFails; - MockERC20 internal derivativeToken; + MockERC6909 internal derivativeToken; + + error InvalidDerivativeParams(); constructor(address _owner) Module(_owner) {} @@ -33,16 +35,21 @@ contract MockDerivativeModule is DerivativeModule { bytes memory params_, uint256 amount_, bool wrapped_ - ) external virtual override returns (uint256, address, uint256) {} + ) external virtual override returns (uint256, address, uint256) { + // TODO wrapping + (uint256 tokenId, uint256 multiplier) = abi.decode(params_, (uint256, uint256)); + + uint256 outputAmount = multiplier == 0 ? amount_ : amount_ * multiplier; + + derivativeToken.mint(to_, tokenId, outputAmount); + } function mint( address to_, uint256 tokenId_, uint256 amount_, bool wrapped_ - ) external virtual override returns (uint256, address, uint256) { - derivativeToken.mint(to_, amount_); - } + ) external virtual override returns (uint256, address, uint256) {} function redeem(uint256 tokenId_, uint256 amount_, bool wrapped_) external virtual override {} @@ -83,7 +90,7 @@ contract MockDerivativeModule is DerivativeModule { validateFails = validateFails_; } - function setDerivativeToken(MockERC20 token_) external { + function setDerivativeToken(MockERC6909 token_) external { derivativeToken = token_; } } From 35292ee3b8da3774ee0cc6ad779a5ac3d2435f65 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 14:31:49 +0400 Subject: [PATCH 41/82] Add structs for module parameters --- src/AuctionHouse.sol | 42 +++++++++++++++---- test/AuctionHouse/sendPayout.t.sol | 13 +++--- .../Auction/MockAtomicAuctionModule.sol | 8 +++- .../modules/Condenser/MockCondenserModule.sol | 13 +++++- .../Derivative/MockDerivativeModule.sol | 11 +++-- 5 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 0485a163..cbe84e5c 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -470,6 +470,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @notice Sends the payout token to the recipient /// @dev This function handles the following: /// 1. Sends the payout token from the router to the recipient + /// 1a. If the lot is a derivative, mints the derivative token to the recipient /// 2. Calls the post hook on the hooks contract (if provided) /// /// This function assumes that: @@ -494,15 +495,42 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { Routing memory routingParams_, bytes memory auctionOutput_ ) internal { - // Get the pre-transfer balance - uint256 balanceBefore = routingParams_.baseToken.balanceOf(recipient_); + // If no derivative, then the payout is sent directly to the recipient + if (fromVeecode(routingParams_.derivativeReference) == bytes7("")) { + // Get the pre-transfer balance + uint256 balanceBefore = routingParams_.baseToken.balanceOf(recipient_); - // Send payout token to recipient - routingParams_.baseToken.safeTransfer(recipient_, payoutAmount_); + // Send payout token to recipient + routingParams_.baseToken.safeTransfer(recipient_, payoutAmount_); - // Check that the recipient received the expected amount of payout tokens - if (routingParams_.baseToken.balanceOf(recipient_) < balanceBefore + payoutAmount_) { - revert UnsupportedToken(address(routingParams_.baseToken)); + // Check that the recipient received the expected amount of payout tokens + if (routingParams_.baseToken.balanceOf(recipient_) < balanceBefore + payoutAmount_) { + revert UnsupportedToken(address(routingParams_.baseToken)); + } + } + // Otherwise, send parameters and payout to the derivative to mint to recipient + else { + // Get the module for the derivative type + // We assume that the module type has been checked when the lot was created + DerivativeModule module = + DerivativeModule(_getModuleIfInstalled(routingParams_.derivativeReference)); + + bytes memory derivativeParams = routingParams_.derivativeParams; + + // Lookup condensor module from combination of auction and derivative types + // If condenser specified, condense auction output and derivative params before sending to derivative module + Veecode condenserRef = + condensers[routingParams_.auctionReference][routingParams_.derivativeReference]; + if (fromVeecode(condenserRef) != bytes7("")) { + // Get condenser module + CondenserModule condenser = CondenserModule(_getModuleIfInstalled(condenserRef)); + + // Condense auction output and derivative params + derivativeParams = condenser.condense(auctionOutput_, derivativeParams); + } + + // Call the module to mint derivative tokens to the recipient + module.mint(recipient_, derivativeParams, payoutAmount_, routingParams_.wrapDerivative); } // Call post hook on hooks contract if provided diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index ccd6824b..9a945b6b 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -55,6 +55,7 @@ contract SendPayoutTest is Test, Permit2User { mockAuctionModule = new MockAtomicAuctionModule(address(auctionHouse)); mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); mockCondenserModule = new MockCondenserModule(address(auctionHouse)); + auctionHouse.installModule(mockAuctionModule); quoteToken = new MockFeeOnTransferERC20("Quote Token", "QUOTE", 18); quoteToken.setTransferFee(0); @@ -68,7 +69,8 @@ contract SendPayoutTest is Test, Permit2User { derivativeParams = bytes(""); wrapDerivative = false; auctionOutputMultiplier = 2; - auctionOutput = abi.encode(auctionOutputMultiplier); // Does nothing unless the condenser is set + auctionOutput = + abi.encode(MockAtomicAuctionModule.Output({multiplier: auctionOutputMultiplier})); // Does nothing unless the condenser is set routingParams = Auctioneer.Routing({ auctionReference: mockAuctionModule.VEECODE(), @@ -265,7 +267,8 @@ contract SendPayoutTest is Test, Permit2User { // Update parameters derivativeReference = mockDerivativeModule.VEECODE(); derivativeTokenId = 20; - derivativeParams = abi.encode(derivativeTokenId, 0); + derivativeParams = + abi.encode(MockDerivativeModule.Params({tokenId: derivativeTokenId, multiplier: 0})); routingParams.derivativeReference = derivativeReference; routingParams.derivativeParams = derivativeParams; _; @@ -301,10 +304,8 @@ contract SendPayoutTest is Test, Permit2User { givenAuctionHasDerivative givenDerivativeParamsAreInvalid { - // Expect revert - bytes memory err = - abi.encodeWithSelector(MockDerivativeModule.InvalidDerivativeParams.selector); - vm.expectRevert(err); + // Expect revert while decoding parameters + vm.expectRevert(); // Call vm.prank(USER); diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index b5407fb7..af8f01ba 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -11,6 +11,10 @@ contract MockAtomicAuctionModule is AuctionModule { mapping(uint256 => uint256) public payoutData; bool public purchaseReverts; + struct Output { + uint256 multiplier; + } + mapping(uint256 lotId => bool isCancelled) public cancelled; constructor(address _owner) AuctionModule(_owner) { @@ -52,7 +56,9 @@ contract MockAtomicAuctionModule is AuctionModule { payout = payoutData[id_] * amount_ / 1e5; } - auctionOutput = auctionData_; + Output memory output = Output({multiplier: 1}); + + auctionOutput = abi.encode(output); } function setPayoutMultiplier(uint256 id_, uint256 multiplier_) external virtual { diff --git a/test/modules/Condenser/MockCondenserModule.sol b/test/modules/Condenser/MockCondenserModule.sol index 1bdd55b1..1afd34ca 100644 --- a/test/modules/Condenser/MockCondenserModule.sol +++ b/test/modules/Condenser/MockCondenserModule.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.19; // Modules import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; +import {MockAtomicAuctionModule} from "test/modules/Auction/MockAtomicAuctionModule.sol"; +import {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule.sol"; + // Condenser import {CondenserModule} from "src/modules/Condenser.sol"; @@ -23,12 +26,18 @@ contract MockCondenserModule is CondenserModule { bytes memory derivativeConfig_ ) external pure virtual override returns (bytes memory) { // Get auction output - (uint256 auctionMultiplier) = abi.decode(auctionOutput_, (uint256)); + MockAtomicAuctionModule.Output memory auctionOutput = + abi.decode(auctionOutput_, (MockAtomicAuctionModule.Output)); // Get derivative params (uint256 derivativeTokenId) = abi.decode(derivativeConfig_, (uint256)); + MockDerivativeModule.Params memory derivativeParams = MockDerivativeModule.Params({ + tokenId: derivativeTokenId, + multiplier: auctionOutput.multiplier + }); + // Return condensed output - return abi.encode(derivativeTokenId, auctionMultiplier); + return abi.encode(derivativeParams); } } diff --git a/test/modules/Derivative/MockDerivativeModule.sol b/test/modules/Derivative/MockDerivativeModule.sol index 9a6dfdc5..b31aa58f 100644 --- a/test/modules/Derivative/MockDerivativeModule.sol +++ b/test/modules/Derivative/MockDerivativeModule.sol @@ -15,6 +15,11 @@ contract MockDerivativeModule is DerivativeModule { error InvalidDerivativeParams(); + struct Params { + uint256 tokenId; + uint256 multiplier; + } + constructor(address _owner) Module(_owner) {} function VEECODE() public pure virtual override returns (Veecode) { @@ -37,11 +42,11 @@ contract MockDerivativeModule is DerivativeModule { bool wrapped_ ) external virtual override returns (uint256, address, uint256) { // TODO wrapping - (uint256 tokenId, uint256 multiplier) = abi.decode(params_, (uint256, uint256)); + Params memory params = abi.decode(params_, (Params)); - uint256 outputAmount = multiplier == 0 ? amount_ : amount_ * multiplier; + uint256 outputAmount = params.multiplier == 0 ? amount_ : amount_ * params.multiplier; - derivativeToken.mint(to_, tokenId, outputAmount); + derivativeToken.mint(to_, params.tokenId, outputAmount); } function mint( From b7a394cece7cd774c23abb7f71c2d2a70af5e679 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 14:54:34 +0400 Subject: [PATCH 42/82] Check for invalid params with condenser --- test/AuctionHouse/sendPayout.t.sol | 22 +++++++++++++++---- .../modules/Condenser/MockCondenserModule.sol | 6 +++-- .../Derivative/MockDerivativeModule.sol | 4 ++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index 9a945b6b..e67c8a90 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -244,9 +244,9 @@ contract SendPayoutTest is Test, Permit2User { // ========== Derivative flow ========== // // [ ] given the base token is a derivative - // [ ] given a condenser is set - // [ ] given the derivative parameters are invalid - // [ ] it reverts + // [X] given a condenser is set + // [X] given the derivative parameters are invalid + // [X] it reverts // [X] it uses the condenser to determine derivative parameters // [ ] given a condenser is not set // [ ] given the derivative is wrapped @@ -294,7 +294,7 @@ contract SendPayoutTest is Test, Permit2User { } modifier givenDerivativeParamsAreInvalid() { - derivativeParams = abi.encode(0); + derivativeParams = abi.encode("one", "two", uint256(2)); routingParams.derivativeParams = derivativeParams; _; } @@ -352,6 +352,20 @@ contract SendPayoutTest is Test, Permit2User { ); } + function test_derivative_condenser_invalidParams_reverts() + public + givenAuctionHasDerivative + givenDerivativeHasCondenser + givenDerivativeParamsAreInvalid + { + // Expect revert while decoding parameters + vm.expectRevert(); + + // Call + vm.prank(USER); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); + } + function test_derivative_condenser() public givenAuctionHasDerivative diff --git a/test/modules/Condenser/MockCondenserModule.sol b/test/modules/Condenser/MockCondenserModule.sol index 1afd34ca..90711d29 100644 --- a/test/modules/Condenser/MockCondenserModule.sol +++ b/test/modules/Condenser/MockCondenserModule.sol @@ -30,10 +30,12 @@ contract MockCondenserModule is CondenserModule { abi.decode(auctionOutput_, (MockAtomicAuctionModule.Output)); // Get derivative params - (uint256 derivativeTokenId) = abi.decode(derivativeConfig_, (uint256)); + if (derivativeConfig_.length != 64) revert(""); + MockDerivativeModule.Params memory originalDerivativeParams = + abi.decode(derivativeConfig_, (MockDerivativeModule.Params)); MockDerivativeModule.Params memory derivativeParams = MockDerivativeModule.Params({ - tokenId: derivativeTokenId, + tokenId: originalDerivativeParams.tokenId, multiplier: auctionOutput.multiplier }); diff --git a/test/modules/Derivative/MockDerivativeModule.sol b/test/modules/Derivative/MockDerivativeModule.sol index b31aa58f..711f5c30 100644 --- a/test/modules/Derivative/MockDerivativeModule.sol +++ b/test/modules/Derivative/MockDerivativeModule.sol @@ -9,6 +9,8 @@ import {DerivativeModule} from "src/modules/Derivative.sol"; import {MockERC6909} from "solmate/test/utils/mocks/MockERC6909.sol"; +import {console2} from "forge-std/console2.sol"; + contract MockDerivativeModule is DerivativeModule { bool internal validateFails; MockERC6909 internal derivativeToken; @@ -41,6 +43,8 @@ contract MockDerivativeModule is DerivativeModule { uint256 amount_, bool wrapped_ ) external virtual override returns (uint256, address, uint256) { + if (params_.length != 64) revert(""); + // TODO wrapping Params memory params = abi.decode(params_, (Params)); From a681c4489372c18b4edcdd47aab4da10fc3e8778 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 15:07:34 +0400 Subject: [PATCH 43/82] Amend derivative test for purchase() --- test/AuctionHouse/purchase.t.sol | 45 +++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 3175026a..1c6b7c78 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -3,11 +3,12 @@ pragma solidity 0.8.19; // Libraries import {Test} from "forge-std/Test.sol"; -import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; // Mocks -import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {MockERC6909} from "solmate/test/utils/mocks/MockERC6909.sol"; import {MockAtomicAuctionModule} from "test/modules/Auction/MockAtomicAuctionModule.sol"; import {MockBatchAuctionModule} from "test/modules/Auction/MockBatchAuctionModule.sol"; import {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule.sol"; @@ -35,6 +36,7 @@ import { contract PurchaseTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; + MockERC6909 internal derivativeToken; MockAtomicAuctionModule internal mockAuctionModule; MockDerivativeModule internal mockDerivativeModule; MockCondenserModule internal mockCondenserModule; @@ -70,6 +72,7 @@ contract PurchaseTest is Test, Permit2User { uint256 internal approvalNonce; bytes internal approvalSignature; uint48 internal approvalDeadline; + uint256 internal derivativeTokenId; function setUp() external { aliceKey = _getRandomUint256(); @@ -77,6 +80,7 @@ contract PurchaseTest is Test, Permit2User { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); + derivativeToken = new MockERC6909(); auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); mockAuctionModule = new MockAtomicAuctionModule(address(auctionHouse)); @@ -85,8 +89,6 @@ contract PurchaseTest is Test, Permit2User { mockAllowlist = new MockAllowlist(); mockHook = new MockHook(address(quoteToken), address(baseToken)); - // mockDerivativeModule.setDerivativeToken(baseToken); - auctionParams = Auction.AuctionParams({ start: uint48(block.timestamp), duration: uint48(1 days), @@ -149,12 +151,11 @@ contract PurchaseTest is Test, Permit2User { _; } - modifier whenCondenserModuleIsInstalled() { + modifier givenDerivativeHasCondenser() { + // Install the condenser module auctionHouse.installModule(mockCondenserModule); - _; - } - modifier whenCondenserIsMapped() { + // Set the condenser auctionHouse.setCondenser( mockAuctionModule.VEECODE(), mockDerivativeModule.VEECODE(), @@ -539,11 +540,17 @@ contract PurchaseTest is Test, Permit2User { // ======== Derivative flow ======== // modifier givenAuctionHasDerivative() { - // Assumes the derivative module is already installed + // Install the derivative module + auctionHouse.installModule(mockDerivativeModule); + + mockDerivativeModule.setDerivativeToken(derivativeToken); // Set up a new auction with a derivative + derivativeTokenId = 20; routingParams.derivativeType = toKeycode("DERV"); - routingParams.derivativeParams = abi.encode(""); + routingParams.derivativeParams = + abi.encode(MockDerivativeModule.Params({tokenId: derivativeTokenId, multiplier: 0})); + vm.prank(auctionOwner); lotId = auctionHouse.auction(routingParams, auctionParams); @@ -557,16 +564,17 @@ contract PurchaseTest is Test, Permit2User { function test_derivative() public - givenDerivativeModuleIsInstalled givenAuctionHasDerivative givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenOwnerHasBaseTokenBalance(AMOUNT_OUT) givenQuoteTokenSpendingIsApproved + givenBaseTokenSpendingIsApproved { // Call vm.prank(alice); auctionHouse.purchase(purchaseParams); - // Check balances + // Check balances of the quote token assertEq(quoteToken.balanceOf(alice), 0); assertEq(quoteToken.balanceOf(recipient), 0); assertEq(quoteToken.balanceOf(address(mockHook)), 0); @@ -574,11 +582,22 @@ contract PurchaseTest is Test, Permit2User { quoteToken.balanceOf(address(auctionHouse)), amountInProtocolFee + amountInReferrerFee ); assertEq(quoteToken.balanceOf(auctionOwner), amountInLessFee); + assertEq(quoteToken.balanceOf(address(mockDerivativeModule)), 0); + // Check balances of the base token assertEq(baseToken.balanceOf(alice), 0); - assertEq(baseToken.balanceOf(recipient), AMOUNT_OUT); + assertEq(baseToken.balanceOf(recipient), 0); assertEq(baseToken.balanceOf(address(mockHook)), 0); assertEq(baseToken.balanceOf(address(auctionHouse)), 0); assertEq(baseToken.balanceOf(auctionOwner), 0); + assertEq(baseToken.balanceOf(address(mockDerivativeModule)), AMOUNT_OUT); + + // Check balances of the derivative token + assertEq(derivativeToken.balanceOf(alice, derivativeTokenId), 0); + assertEq(derivativeToken.balanceOf(recipient, derivativeTokenId), AMOUNT_OUT); + assertEq(derivativeToken.balanceOf(address(mockHook), derivativeTokenId), 0); + assertEq(derivativeToken.balanceOf(address(auctionHouse), derivativeTokenId), 0); + assertEq(derivativeToken.balanceOf(auctionOwner, derivativeTokenId), 0); + assertEq(derivativeToken.balanceOf(address(mockDerivativeModule), derivativeTokenId), 0); } } From 0f5cadeee943a9e435d19f11ba3c3a2834463f49 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 15:16:28 +0400 Subject: [PATCH 44/82] Support for allowlist proof --- src/AuctionHouse.sol | 4 +++- test/AuctionHouse/purchase.t.sol | 14 ++++++++++---- test/modules/Auction/MockAllowlist.sol | 17 ++++++++++------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index cbe84e5c..eb432a08 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -42,6 +42,7 @@ abstract contract Router is FeeManager { /// @param approvalNonce Nonce for permit approval signature /// @param auctionData Custom data used by the auction module /// @param approvalSignature Permit approval signature for the quoteToken + /// @param allowlistProof Proof of allowlist inclusion struct PurchaseParams { address recipient; address referrer; @@ -52,6 +53,7 @@ abstract contract Router is FeeManager { uint256 approvalNonce; bytes auctionData; bytes approvalSignature; + bytes allowlistProof; } // ========== STATE VARIABLES ========== // @@ -235,7 +237,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Check if the purchaser is on the allowlist if (address(routing.allowlist) != address(0)) { - if (!routing.allowlist.isAllowed(params_.lotId, msg.sender, bytes(""))) { + if (!routing.allowlist.isAllowed(params_.lotId, msg.sender, params_.allowlistProof)) { revert NotAuthorized(); } } diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 1c6b7c78..17a908bc 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -73,6 +73,7 @@ contract PurchaseTest is Test, Permit2User { bytes internal approvalSignature; uint48 internal approvalDeadline; uint256 internal derivativeTokenId; + bytes internal allowlistProof; function setUp() external { aliceKey = _getRandomUint256(); @@ -142,7 +143,8 @@ contract PurchaseTest is Test, Permit2User { minAmountOut: AMOUNT_OUT, approvalNonce: approvalNonce, auctionData: bytes(""), - approvalSignature: approvalSignature + approvalSignature: approvalSignature, + allowlistProof: allowlistProof }); } @@ -327,8 +329,6 @@ contract PurchaseTest is Test, Permit2User { // [X] when the caller is on the allowlist // [X] it succeeds - // TODO add support for allowlist proof - modifier givenAuctionHasAllowlist() { // Register a new auction with an allowlist routingParams.allowlist = mockAllowlist; @@ -345,8 +345,14 @@ contract PurchaseTest is Test, Permit2User { // Assumes the allowlist is set require(address(routingParams.allowlist) != address(0), "allowlist not set"); + // Set the allowlist proof + allowlistProof = abi.encode("i am allowed"); + // Set the caller to be on the allowlist - mockAllowlist.setAllowed(alice, true); + mockAllowlist.setAllowedWithProof(alice, allowlistProof, true); + + // Update the purchase params + purchaseParams.allowlistProof = allowlistProof; _; } diff --git a/test/modules/Auction/MockAllowlist.sol b/test/modules/Auction/MockAllowlist.sol index 431a2d7a..0a88e614 100644 --- a/test/modules/Auction/MockAllowlist.sol +++ b/test/modules/Auction/MockAllowlist.sol @@ -8,18 +8,21 @@ contract MockAllowlist is IAllowlist { uint256[] public registeredIds; - mapping(address => bool) public allowed; + mapping(address => mapping(bytes => bool)) public allowedWithProof; - function isAllowed(address address_, bytes calldata) external view override returns (bool) { - return allowed[address_]; + function isAllowed( + address address_, + bytes calldata proof_ + ) external view override returns (bool) { + return allowedWithProof[address_][proof_]; } function isAllowed( uint256, address address_, - bytes calldata + bytes calldata proof_ ) external view override returns (bool) { - return allowed[address_]; + return allowedWithProof[address_][proof_]; } function register(bytes calldata) external override {} @@ -40,7 +43,7 @@ contract MockAllowlist is IAllowlist { return registeredIds; } - function setAllowed(address account, bool allowed_) external { - allowed[account] = allowed_; + function setAllowedWithProof(address account, bytes calldata proof, bool allowed_) external { + allowedWithProof[account][proof] = allowed_; } } From 06fa50c3b9002c1cb84afc7225d60462fd0e9e4a Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 15:17:39 +0400 Subject: [PATCH 45/82] Change visibility --- src/AuctionHouse.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index eb432a08..3224a940 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -156,7 +156,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== STATE VARIABLES ========== // - IPermit2 public immutable _PERMIT2; + IPermit2 internal immutable _PERMIT2; // ========== CONSTRUCTOR ========== // From 7a7239d8c480dc6fc130a8c6e48269b89d7d66df Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 17:07:32 +0400 Subject: [PATCH 46/82] Tests and implementation for wrapped derivative --- src/AuctionHouse.sol | 17 ++ src/bases/Derivatizer.sol | 119 ++++++------ src/modules/Derivative.sol | 13 +- test/AuctionHouse/purchase.t.sol | 39 ++-- test/AuctionHouse/sendPayout.t.sol | 174 +++++++++++++++--- test/lib/mocks/MockWrappedDerivative.sol | 39 ++++ .../Derivative/MockDerivativeModule.sol | 74 +++++++- 7 files changed, 366 insertions(+), 109 deletions(-) create mode 100644 test/lib/mocks/MockWrappedDerivative.sol diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 3224a940..1780b2a8 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -164,6 +164,23 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _PERMIT2 = IPermit2(permit2_); } + // ========== DERIVATIVE MANAGEMENT ========== // + + /// @inheritdoc Derivatizer + function deploy( + Veecode dType, + bytes memory data, + bool wrapped + ) external override returns (uint256, address) { + // Load the derivative module, will revert if not installed or sunset + DerivativeModule derivative = DerivativeModule(_getModuleIfInstalled(dType)); + + // Call the deploy function on the derivative module + (uint256 tokenId, address wrappedToken) = derivative.deploy(data, wrapped); + + return (tokenId, wrappedToken); + } + // ========== DIRECT EXECUTION ========== // // ========== AUCTION FUNCTIONS ========== // diff --git a/src/bases/Derivatizer.sol b/src/bases/Derivatizer.sol index 17a49d78..c3634f49 100644 --- a/src/bases/Derivatizer.sol +++ b/src/bases/Derivatizer.sol @@ -1,65 +1,66 @@ /// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.19; -import {WithModules} from "src/modules/Modules.sol"; +import {WithModules, Veecode} from "src/modules/Modules.sol"; -abstract contract Derivatizer is WithModules { -// // ========== DERIVATIVE MANAGEMENT ========== // - -// // Return address will be zero if not wrapped -// function deploy( -// Keycode dType, -// bytes memory data, -// bool wrapped -// ) external virtual returns (uint256, address) { -// // Load the derivative module, will revert if not installed or sunset -// DerivativeModule derivative = DerivativeModule(_getLatestModuleIfActive(dType)); - -// // Call the deploy function on the derivative module -// (uint256 tokenId, address wrappedToken) = derivative.deploy(data, wrapped); - -// return (tokenId, wrappedToken); -// } - -// function mint( -// bytes memory data, -// uint256 amount, -// bool wrapped -// ) external virtual returns (bytes memory); -// function mint( -// uint256 tokenId, -// uint256 amount, -// bool wrapped -// ) external virtual returns (bytes memory); - -// function redeem(bytes memory data, uint256 amount) external virtual; - -// // function batchRedeem(bytes[] memory data, uint256[] memory amounts) external virtual; - -// function exercise(bytes memory data, uint256 amount) external virtual; +import {DerivativeModule} from "src/modules/Derivative.sol"; -// function reclaim(bytes memory data) external virtual; - -// function convert(bytes memory data, uint256 amount) external virtual; - -// // TODO Consider best inputs for UX -// function wrap(uint256 tokenId, uint256 amount) external virtual; -// function unwrap(uint256 tokenId, uint256 amount) external virtual; - -// // ========== DERIVATIVE INFORMATION ========== // - -// // TODO view function to format implementation specific token data correctly and return to user - -// function exerciseCost( -// bytes memory data, -// uint256 amount -// ) external view virtual returns (uint256); - -// function convertsTo( -// bytes memory data, -// uint256 amount -// ) external view virtual returns (uint256); - -// // Compute unique token ID for params on the submodule -// function computeId(bytes memory params_) external pure virtual returns (uint256); +abstract contract Derivatizer is WithModules { + // ========== DERIVATIVE MANAGEMENT ========== // + + /// @notice Deploys a new derivative token + /// + /// @param dType The derivative module code + /// @param data The derivative module parameters + /// @param wrapped Whether or not to wrap the derivative token + /// + /// @return tokenId The unique derivative token ID + /// @return wrappedToken The wrapped derivative token address (or zero) + function deploy( + Veecode dType, + bytes memory data, + bool wrapped + ) external virtual returns (uint256 tokenId, address wrappedToken); + + // function mint( + // bytes memory data, + // uint256 amount, + // bool wrapped + // ) external virtual returns (bytes memory); + // function mint( + // uint256 tokenId, + // uint256 amount, + // bool wrapped + // ) external virtual returns (bytes memory); + + // function redeem(bytes memory data, uint256 amount) external virtual; + + // // function batchRedeem(bytes[] memory data, uint256[] memory amounts) external virtual; + + // function exercise(bytes memory data, uint256 amount) external virtual; + + // function reclaim(bytes memory data) external virtual; + + // function convert(bytes memory data, uint256 amount) external virtual; + + // // TODO Consider best inputs for UX + // function wrap(uint256 tokenId, uint256 amount) external virtual; + // function unwrap(uint256 tokenId, uint256 amount) external virtual; + + // // ========== DERIVATIVE INFORMATION ========== // + + // // TODO view function to format implementation specific token data correctly and return to user + + // function exerciseCost( + // bytes memory data, + // uint256 amount + // ) external view virtual returns (uint256); + + // function convertsTo( + // bytes memory data, + // uint256 amount + // ) external view virtual returns (uint256); + + // // Compute unique token ID for params on the submodule + // function computeId(bytes memory params_) external pure virtual returns (uint256); } diff --git a/src/modules/Derivative.sol b/src/modules/Derivative.sol index 4c7fd586..eadbfabe 100644 --- a/src/modules/Derivative.sol +++ b/src/modules/Derivative.sol @@ -25,15 +25,16 @@ abstract contract Derivative { // ========== DERIVATIVE MANAGEMENT ========== // - /// @notice Deploy a new derivative token. Optionally, deploys an ERC20 wrapper for composability. - /// @param params_ ABI-encoded parameters for the derivative to be created - /// @param wrapped_ Whether (true) or not (false) the derivative should be wrapped in an ERC20 token for composability - /// @return tokenId_ The ID of the newly created derivative token - /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. + /// @notice Deploy a new derivative token. Optionally, deploys an ERC20 wrapper for composability. + /// + /// @param params_ ABI-encoded parameters for the derivative to be created + /// @param wrapped_ Whether (true) or not (false) the derivative should be wrapped in an ERC20 token for composability + /// @return tokenId_ The ID of the newly created derivative token + /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. function deploy( bytes memory params_, bool wrapped_ - ) external virtual returns (uint256, address); + ) external virtual returns (uint256 tokenId_, address wrappedAddress_); /// @notice Mint new derivative tokens. Deploys the derivative token if it does not already exist. /// @param to_ The address to mint the derivative tokens to diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 17a908bc..abf7884f 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -8,7 +8,6 @@ import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; // Mocks import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; -import {MockERC6909} from "solmate/test/utils/mocks/MockERC6909.sol"; import {MockAtomicAuctionModule} from "test/modules/Auction/MockAtomicAuctionModule.sol"; import {MockBatchAuctionModule} from "test/modules/Auction/MockBatchAuctionModule.sol"; import {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule.sol"; @@ -36,7 +35,6 @@ import { contract PurchaseTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; - MockERC6909 internal derivativeToken; MockAtomicAuctionModule internal mockAuctionModule; MockDerivativeModule internal mockDerivativeModule; MockCondenserModule internal mockCondenserModule; @@ -81,7 +79,6 @@ contract PurchaseTest is Test, Permit2User { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); - derivativeToken = new MockERC6909(); auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); mockAuctionModule = new MockAtomicAuctionModule(address(auctionHouse)); @@ -549,10 +546,12 @@ contract PurchaseTest is Test, Permit2User { // Install the derivative module auctionHouse.installModule(mockDerivativeModule); - mockDerivativeModule.setDerivativeToken(derivativeToken); + // Deploy a new derivative token + (uint256 tokenId,) = + auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(""), false); // Set up a new auction with a derivative - derivativeTokenId = 20; + derivativeTokenId = tokenId; routingParams.derivativeType = toKeycode("DERV"); routingParams.derivativeParams = abi.encode(MockDerivativeModule.Params({tokenId: derivativeTokenId, multiplier: 0})); @@ -599,11 +598,29 @@ contract PurchaseTest is Test, Permit2User { assertEq(baseToken.balanceOf(address(mockDerivativeModule)), AMOUNT_OUT); // Check balances of the derivative token - assertEq(derivativeToken.balanceOf(alice, derivativeTokenId), 0); - assertEq(derivativeToken.balanceOf(recipient, derivativeTokenId), AMOUNT_OUT); - assertEq(derivativeToken.balanceOf(address(mockHook), derivativeTokenId), 0); - assertEq(derivativeToken.balanceOf(address(auctionHouse), derivativeTokenId), 0); - assertEq(derivativeToken.balanceOf(auctionOwner, derivativeTokenId), 0); - assertEq(derivativeToken.balanceOf(address(mockDerivativeModule), derivativeTokenId), 0); + assertEq(mockDerivativeModule.derivativeToken().balanceOf(alice, derivativeTokenId), 0); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf(recipient, derivativeTokenId), + AMOUNT_OUT + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf(address(mockHook), derivativeTokenId), + 0 + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf( + address(auctionHouse), derivativeTokenId + ), + 0 + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf(auctionOwner, derivativeTokenId), 0 + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf( + address(mockDerivativeModule), derivativeTokenId + ), + 0 + ); } } diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index e67c8a90..965de2b3 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -9,9 +9,10 @@ import {MockAtomicAuctionModule} from "test/modules/Auction/MockAtomicAuctionMod import {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule.sol"; import {MockCondenserModule} from "test/modules/Condenser/MockCondenserModule.sol"; import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; -import {MockERC6909} from "solmate/test/utils/mocks/MockERC6909.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; +import {MockWrappedDerivative} from "test/lib/mocks/MockWrappedDerivative.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; import {AuctionHouse} from "src/AuctionHouse.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; import {IAllowlist} from "src/interfaces/IAllowlist.sol"; @@ -24,6 +25,7 @@ contract SendPayoutTest is Test, Permit2User { MockAtomicAuctionModule internal mockAuctionModule; MockDerivativeModule internal mockDerivativeModule; MockCondenserModule internal mockCondenserModule; + MockWrappedDerivative internal derivativeWrappedImplementation; address internal constant PROTOCOL = address(0x1); @@ -36,12 +38,12 @@ contract SendPayoutTest is Test, Permit2User { uint256 internal payoutAmount = 10e18; MockFeeOnTransferERC20 internal quoteToken; MockFeeOnTransferERC20 internal payoutToken; - MockERC6909 internal derivativeToken; MockHook internal hook; Veecode internal derivativeReference; uint256 internal derivativeTokenId; bytes internal derivativeParams; bool internal wrapDerivative; + ERC20 internal wrappedDerivative; uint256 internal auctionOutputMultiplier; bytes internal auctionOutput; @@ -57,14 +59,15 @@ contract SendPayoutTest is Test, Permit2User { mockCondenserModule = new MockCondenserModule(address(auctionHouse)); auctionHouse.installModule(mockAuctionModule); + derivativeWrappedImplementation = new MockWrappedDerivative("name", "symbol", 18); + mockDerivativeModule.setWrappedImplementation(derivativeWrappedImplementation); + quoteToken = new MockFeeOnTransferERC20("Quote Token", "QUOTE", 18); quoteToken.setTransferFee(0); payoutToken = new MockFeeOnTransferERC20("Payout Token", "PAYOUT", 18); payoutToken.setTransferFee(0); - derivativeToken = new MockERC6909(); - derivativeReference = toVeecode(bytes7("")); derivativeParams = bytes(""); wrapDerivative = false; @@ -243,16 +246,16 @@ contract SendPayoutTest is Test, Permit2User { // ========== Derivative flow ========== // - // [ ] given the base token is a derivative + // [X] given the base token is a derivative // [X] given a condenser is set // [X] given the derivative parameters are invalid // [X] it reverts // [X] it uses the condenser to determine derivative parameters - // [ ] given a condenser is not set - // [ ] given the derivative is wrapped - // [ ] given the derivative parameters are invalid - // [ ] it reverts - // [ ] it mints wrapped derivative tokens to the recipient using the derivative module + // [X] given a condenser is not set + // [X] given the derivative is wrapped + // [X] given the derivative parameters are invalid + // [X] it reverts + // [X] it mints wrapped derivative tokens to the recipient using the derivative module // [X] given the derivative is not wrapped // [X] given the derivative parameters are invalid // [X] it reverts @@ -262,11 +265,13 @@ contract SendPayoutTest is Test, Permit2User { // Install the derivative module auctionHouse.installModule(mockDerivativeModule); - mockDerivativeModule.setDerivativeToken(derivativeToken); + // Deploy a new derivative token + (uint256 tokenId,) = + auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(""), false); // Update parameters derivativeReference = mockDerivativeModule.VEECODE(); - derivativeTokenId = 20; + derivativeTokenId = tokenId; derivativeParams = abi.encode(MockDerivativeModule.Params({tokenId: derivativeTokenId, multiplier: 0})); routingParams.derivativeReference = derivativeReference; @@ -275,6 +280,17 @@ contract SendPayoutTest is Test, Permit2User { } modifier givenDerivativeIsWrapped() { + // Deploy a new wrapped derivative token + (uint256 tokenId_, address wrappedToken_) = + auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(""), true); + + // Update parameters + wrappedDerivative = ERC20(wrappedToken_); + derivativeTokenId = tokenId_; + derivativeParams = + abi.encode(MockDerivativeModule.Params({tokenId: derivativeTokenId, multiplier: 0})); + routingParams.derivativeParams = derivativeParams; + wrapDerivative = true; routingParams.wrapDerivative = wrapDerivative; _; @@ -318,23 +334,105 @@ contract SendPayoutTest is Test, Permit2User { auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); // Check balances of the derivative token - assertEq(derivativeToken.balanceOf(USER, derivativeTokenId), 0, "user balance mismatch"); - assertEq(derivativeToken.balanceOf(OWNER, derivativeTokenId), 0, "owner balance mismatch"); assertEq( - derivativeToken.balanceOf(address(auctionHouse), derivativeTokenId), + mockDerivativeModule.derivativeToken().balanceOf(USER, derivativeTokenId), + 0, + "user balance mismatch" + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf(OWNER, derivativeTokenId), + 0, + "owner balance mismatch" + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf( + address(auctionHouse), derivativeTokenId + ), 0, "auctionHouse balance mismatch" ); assertEq( - derivativeToken.balanceOf(address(hook), derivativeTokenId), 0, "hook balance mismatch" + mockDerivativeModule.derivativeToken().balanceOf(address(hook), derivativeTokenId), + 0, + "hook balance mismatch" ); assertEq( - derivativeToken.balanceOf(RECIPIENT, derivativeTokenId), + mockDerivativeModule.derivativeToken().balanceOf(RECIPIENT, derivativeTokenId), payoutAmount, "recipient balance mismatch" ); assertEq( - derivativeToken.balanceOf(address(mockDerivativeModule), derivativeTokenId), + mockDerivativeModule.derivativeToken().balanceOf( + address(mockDerivativeModule), derivativeTokenId + ), + 0, + "derivative module balance mismatch" + ); + + // Check balances of payout token + assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); + assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); + assertEq(payoutToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); + assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); + assertEq(payoutToken.balanceOf(RECIPIENT), 0, "recipient balance mismatch"); + assertEq( + payoutToken.balanceOf(address(mockDerivativeModule)), + 0, // This would normally be non-zero, but we didn't transfer the collateral to it + "derivative module balance mismatch" + ); + } + + function test_derivative_wrapped() public givenAuctionHasDerivative givenDerivativeIsWrapped { + // Call + vm.prank(USER); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); + + // Check balances of the wrapped derivative token + assertEq(wrappedDerivative.balanceOf(USER), 0, "user balance mismatch"); + assertEq(wrappedDerivative.balanceOf(OWNER), 0, "owner balance mismatch"); + assertEq( + wrappedDerivative.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch" + ); + assertEq(wrappedDerivative.balanceOf(address(hook)), 0, "hook balance mismatch"); + assertEq(wrappedDerivative.balanceOf(RECIPIENT), payoutAmount, "recipient balance mismatch"); + assertEq( + wrappedDerivative.balanceOf(address(mockDerivativeModule)), + 0, + "derivative module balance mismatch" + ); + + // Check balances of the derivative token + assertEq( + mockDerivativeModule.derivativeToken().balanceOf(USER, derivativeTokenId), + 0, + "user balance mismatch" + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf(OWNER, derivativeTokenId), + 0, + "owner balance mismatch" + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf( + address(auctionHouse), derivativeTokenId + ), + 0, + "auctionHouse balance mismatch" + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf(address(hook), derivativeTokenId), + 0, + "hook balance mismatch" + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf(RECIPIENT, derivativeTokenId), + 0, // No raw derivative + "recipient balance mismatch" + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf( + address(mockDerivativeModule), derivativeTokenId + ), 0, "derivative module balance mismatch" ); @@ -352,6 +450,20 @@ contract SendPayoutTest is Test, Permit2User { ); } + function test_derivative_wrapped_invalidParams() + public + givenAuctionHasDerivative + givenDerivativeIsWrapped + givenDerivativeParamsAreInvalid + { + // Expect revert while decoding parameters + vm.expectRevert(); + + // Call + vm.prank(USER); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); + } + function test_derivative_condenser_invalidParams_reverts() public givenAuctionHasDerivative @@ -376,23 +488,37 @@ contract SendPayoutTest is Test, Permit2User { auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); // Check balances of the derivative token - assertEq(derivativeToken.balanceOf(USER, derivativeTokenId), 0, "user balance mismatch"); - assertEq(derivativeToken.balanceOf(OWNER, derivativeTokenId), 0, "owner balance mismatch"); assertEq( - derivativeToken.balanceOf(address(auctionHouse), derivativeTokenId), + mockDerivativeModule.derivativeToken().balanceOf(USER, derivativeTokenId), + 0, + "user balance mismatch" + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf(OWNER, derivativeTokenId), + 0, + "owner balance mismatch" + ); + assertEq( + mockDerivativeModule.derivativeToken().balanceOf( + address(auctionHouse), derivativeTokenId + ), 0, "auctionHouse balance mismatch" ); assertEq( - derivativeToken.balanceOf(address(hook), derivativeTokenId), 0, "hook balance mismatch" + mockDerivativeModule.derivativeToken().balanceOf(address(hook), derivativeTokenId), + 0, + "hook balance mismatch" ); assertEq( - derivativeToken.balanceOf(RECIPIENT, derivativeTokenId), + mockDerivativeModule.derivativeToken().balanceOf(RECIPIENT, derivativeTokenId), payoutAmount * auctionOutputMultiplier, // Condenser multiplies the payout "recipient balance mismatch" ); assertEq( - derivativeToken.balanceOf(address(mockDerivativeModule), derivativeTokenId), + mockDerivativeModule.derivativeToken().balanceOf( + address(mockDerivativeModule), derivativeTokenId + ), 0, "derivative module balance mismatch" ); diff --git a/test/lib/mocks/MockWrappedDerivative.sol b/test/lib/mocks/MockWrappedDerivative.sol new file mode 100644 index 00000000..8fb216eb --- /dev/null +++ b/test/lib/mocks/MockWrappedDerivative.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ERC6909} from "solmate/tokens/ERC6909.sol"; + +import {Clone} from "src/lib/clones/Clone.sol"; + +contract MockWrappedDerivative is ERC20, Clone { + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) ERC20(name_, symbol_, decimals_) {} + + function underlyingToken() public pure returns (ERC6909) { + return ERC6909(_getArgAddress(0)); + } + + function underlyingTokenId() public pure returns (uint256) { + return _getArgUint256(20); // address offset is 20 + } + + function deposit(uint256 amount_, address to_) external { + // Transfer token to wrap + underlyingToken().transferFrom(msg.sender, address(this), underlyingTokenId(), amount_); + + // Mint wrapped token + _mint(to_, amount_); + } + + function withdraw(uint256 amount_, address to_) external { + // Burn wrapped token + _burn(msg.sender, amount_); + + // Transfer token to unwrap + underlyingToken().transfer(to_, underlyingTokenId(), amount_); + } +} diff --git a/test/modules/Derivative/MockDerivativeModule.sol b/test/modules/Derivative/MockDerivativeModule.sol index 711f5c30..4629c9a9 100644 --- a/test/modules/Derivative/MockDerivativeModule.sol +++ b/test/modules/Derivative/MockDerivativeModule.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; +import {ClonesWithImmutableArgs} from "src/lib/clones/ClonesWithImmutableArgs.sol"; + // Modules import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; @@ -8,12 +10,15 @@ import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; import {DerivativeModule} from "src/modules/Derivative.sol"; import {MockERC6909} from "solmate/test/utils/mocks/MockERC6909.sol"; - -import {console2} from "forge-std/console2.sol"; +import {MockWrappedDerivative} from "test/lib/mocks/MockWrappedDerivative.sol"; contract MockDerivativeModule is DerivativeModule { + using ClonesWithImmutableArgs for address; + bool internal validateFails; - MockERC6909 internal derivativeToken; + MockERC6909 public derivativeToken; + uint256 internal tokenCount; + MockWrappedDerivative internal wrappedImplementation; error InvalidDerivativeParams(); @@ -22,7 +27,9 @@ contract MockDerivativeModule is DerivativeModule { uint256 multiplier; } - constructor(address _owner) Module(_owner) {} + constructor(address _owner) Module(_owner) { + derivativeToken = new MockERC6909(); + } function VEECODE() public pure virtual override returns (Veecode) { return wrapVeecode(toKeycode("DERV"), 1); @@ -35,7 +42,37 @@ contract MockDerivativeModule is DerivativeModule { function deploy( bytes memory params_, bool wrapped_ - ) external virtual override returns (uint256, address) {} + ) external virtual override onlyParent returns (uint256, address) { + uint256 tokenId = tokenCount; + address wrappedAddress; + + if (wrapped_) { + // If there is no wrapped implementation, abort + if (address(wrappedImplementation) == address(0)) revert(""); + + // Deploy the wrapped implementation + wrappedAddress = address(wrappedImplementation).clone3( + abi.encodePacked(derivativeToken, tokenId), bytes32(tokenId) + ); + } + + // Create new token metadata + Token memory tokenData = Token({ + exists: true, + wrapped: wrappedAddress, + decimals: 18, + name: "Mock Derivative", + symbol: "MDER", + data: "" + }); + + // Store metadata + tokenMetadata[tokenId] = tokenData; + + tokenCount++; + + return (tokenId, wrappedAddress); + } function mint( address to_, @@ -45,12 +82,31 @@ contract MockDerivativeModule is DerivativeModule { ) external virtual override returns (uint256, address, uint256) { if (params_.length != 64) revert(""); - // TODO wrapping Params memory params = abi.decode(params_, (Params)); + // Check that tokenId exists + Token storage token = tokenMetadata[params.tokenId]; + if (!token.exists) revert(""); + + // Check that the wrapped status is correct + if (token.wrapped != address(0) && !wrapped_) revert(""); + uint256 outputAmount = params.multiplier == 0 ? amount_ : amount_ * params.multiplier; - derivativeToken.mint(to_, params.tokenId, outputAmount); + // If wrapped, mint and deposit + if (wrapped_) { + derivativeToken.mint(address(this), params.tokenId, outputAmount); + + derivativeToken.approve(token.wrapped, params.tokenId, outputAmount); + + MockWrappedDerivative(token.wrapped).deposit(outputAmount, to_); + } + // Otherwise mint as normal + else { + derivativeToken.mint(to_, params.tokenId, outputAmount); + } + + return (params.tokenId, token.wrapped, outputAmount); } function mint( @@ -99,7 +155,7 @@ contract MockDerivativeModule is DerivativeModule { validateFails = validateFails_; } - function setDerivativeToken(MockERC6909 token_) external { - derivativeToken = token_; + function setWrappedImplementation(MockWrappedDerivative implementation_) external { + wrappedImplementation = implementation_; } } From 63d0a6d41a09a9727b51d2397b886abf7fed1314 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 17 Jan 2024 17:11:09 +0400 Subject: [PATCH 47/82] Obsolete import --- src/bases/Derivatizer.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bases/Derivatizer.sol b/src/bases/Derivatizer.sol index c3634f49..bad642d5 100644 --- a/src/bases/Derivatizer.sol +++ b/src/bases/Derivatizer.sol @@ -3,8 +3,6 @@ pragma solidity 0.8.19; import {WithModules, Veecode} from "src/modules/Modules.sol"; -import {DerivativeModule} from "src/modules/Derivative.sol"; - abstract contract Derivatizer is WithModules { // ========== DERIVATIVE MANAGEMENT ========== // From ba58792c51e5c7a7088d9bc2be747db6250c9df2 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 12:20:07 +0400 Subject: [PATCH 48/82] Remove checks for token balance and allowance --- src/AuctionHouse.sol | 38 ++------------------------ test/AuctionHouse/collectPayment.t.sol | 26 +++--------------- test/AuctionHouse/collectPayout.t.sol | 26 +++--------------- 3 files changed, 10 insertions(+), 80 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 1780b2a8..b07aae60 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -142,10 +142,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { error NotAuthorized(); - error InsufficientBalance(address token_, uint256 requiredAmount_); - - error InsufficientAllowance(address token_, address spender_, uint256 requiredAmount_); - error UnsupportedToken(address token_); error InvalidHook(); @@ -363,11 +359,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { hooks_.pre(lotId_, amount_); } - // Check that the user has sufficient balance of the quote token - if (quoteToken_.balanceOf(msg.sender) < amount_) { - revert InsufficientBalance(address(quoteToken_), amount_); - } - // If a Permit2 approval signature is provided, use it to transfer the quote token if (approvalSignature_.length != 0) { _permit2Transfer( @@ -448,23 +439,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } // Otherwise fallback to a standard ERC20 transfer else { - // Check that the auction owner has sufficient balance of the payout token - if (routingParams_.baseToken.balanceOf(routingParams_.owner) < payoutAmount_) { - revert InsufficientBalance(address(routingParams_.baseToken), payoutAmount_); - } - - // Check that the auction owner has granted approval to transfer the payout token - if ( - routingParams_.baseToken.allowance(routingParams_.owner, address(this)) - < payoutAmount_ - ) { - revert InsufficientAllowance( - address(routingParams_.baseToken), address(this), payoutAmount_ - ); - } - // Transfer the payout token from the auction owner - // `safeTransferFrom()` will revert upon failure + // `safeTransferFrom()` will revert upon failure or the lack of allowance or balance routingParams_.baseToken.safeTransferFrom( routingParams_.owner, address(this), payoutAmount_ ); @@ -572,15 +548,10 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @param amount_ Amount of tokens to transfer (in native decimals) /// @param token_ Token to transfer function _transfer(uint256 amount_, ERC20 token_) internal { - // Check that the user has granted approval to transfer the quote token - if (token_.allowance(msg.sender, address(this)) < amount_) { - revert InsufficientAllowance(address(token_), address(this), amount_); - } - uint256 balanceBefore = token_.balanceOf(address(this)); // Transfer the quote token from the user - // `safeTransferFrom()` will revert upon failure + // `safeTransferFrom()` will revert upon failure or the lack of allowance or balance token_.safeTransferFrom(msg.sender, address(this), amount_); // Check that it is not a fee-on-transfer token @@ -612,11 +583,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 approvalNonce_, bytes memory approvalSignature_ ) internal { - // Check that the user has granted approval to PERMIT2 to transfer the quote token - if (token_.allowance(msg.sender, address(_PERMIT2)) < amount_) { - revert InsufficientAllowance(address(token_), address(_PERMIT2), amount_); - } - uint256 balanceBefore = token_.balanceOf(address(this)); // Use PERMIT2 to transfer the token from the user diff --git a/test/AuctionHouse/collectPayment.t.sol b/test/AuctionHouse/collectPayment.t.sol index ce264081..1badf1ac 100644 --- a/test/AuctionHouse/collectPayment.t.sol +++ b/test/AuctionHouse/collectPayment.t.sol @@ -167,13 +167,7 @@ contract CollectPaymentTest is Test, Permit2User { whenPermit2ApprovalIsValid { // Expect the error - bytes memory err = abi.encodeWithSelector( - AuctionHouse.InsufficientAllowance.selector, - address(quoteToken), - _PERMIT2_ADDRESS, - amount - ); - vm.expectRevert(err); + vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); // Call vm.prank(USER); @@ -275,10 +269,7 @@ contract CollectPaymentTest is Test, Permit2User { whenPermit2ApprovalIsValid { // Expect the error - bytes memory err = abi.encodeWithSelector( - AuctionHouse.InsufficientBalance.selector, address(quoteToken), amount - ); - vm.expectRevert(err); + vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); // Call vm.prank(USER); @@ -340,10 +331,7 @@ contract CollectPaymentTest is Test, Permit2User { function test_transfer_whenUserHasInsufficientBalance_reverts() public { // Expect the error - bytes memory err = abi.encodeWithSelector( - AuctionHouse.InsufficientBalance.selector, address(quoteToken), amount - ); - vm.expectRevert(err); + vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); // Call vm.prank(USER); @@ -354,13 +342,7 @@ contract CollectPaymentTest is Test, Permit2User { function test_transfer_givenNoTokenApproval_reverts() public givenUserHasBalance(amount) { // Expect the error - bytes memory err = abi.encodeWithSelector( - AuctionHouse.InsufficientAllowance.selector, - address(quoteToken), - address(auctionHouse), - amount - ); - vm.expectRevert(err); + vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); // Call vm.prank(USER); diff --git a/test/AuctionHouse/collectPayout.t.sol b/test/AuctionHouse/collectPayout.t.sol index 0f8cd296..6375a0dd 100644 --- a/test/AuctionHouse/collectPayout.t.sol +++ b/test/AuctionHouse/collectPayout.t.sol @@ -240,10 +240,7 @@ contract CollectPayoutTest is Test, Permit2User { function test_insufficientBalance_reverts() public { // Expect revert - bytes memory err = abi.encodeWithSelector( - AuctionHouse.InsufficientBalance.selector, address(payoutToken), payoutAmount - ); - vm.expectRevert(err); + vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); // Call vm.prank(USER); @@ -252,13 +249,7 @@ contract CollectPayoutTest is Test, Permit2User { function test_insufficientAllowance_reverts() public givenOwnerHasBalance(payoutAmount) { // Expect revert - bytes memory err = abi.encodeWithSelector( - AuctionHouse.InsufficientAllowance.selector, - address(payoutToken), - address(auctionHouse), - payoutAmount - ); - vm.expectRevert(err); + vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); // Call vm.prank(USER); @@ -382,10 +373,7 @@ contract CollectPayoutTest is Test, Permit2User { function test_derivative_insufficientBalance_reverts() public givenAuctionHasDerivative { // Expect revert - bytes memory err = abi.encodeWithSelector( - AuctionHouse.InsufficientBalance.selector, address(payoutToken), payoutAmount - ); - vm.expectRevert(err); + vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); // Call vm.prank(USER); @@ -398,13 +386,7 @@ contract CollectPayoutTest is Test, Permit2User { givenOwnerHasBalance(payoutAmount) { // Expect revert - bytes memory err = abi.encodeWithSelector( - AuctionHouse.InsufficientAllowance.selector, - address(payoutToken), - address(auctionHouse), - payoutAmount - ); - vm.expectRevert(err); + vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); // Call vm.prank(USER); From af8e8bc80aae56bbe2ef65d9baca252f302e12a7 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 13:09:48 +0400 Subject: [PATCH 49/82] Add collateral token to deployment parameters for the mock derivative module. Shift burden of transferring collateral token to the derivative minter. --- src/AuctionHouse.sol | 14 +- src/modules/Derivative.sol | 24 ++- test/AuctionHouse/collectPayout.t.sol | 38 +++-- test/AuctionHouse/purchase.t.sol | 6 +- test/AuctionHouse/sendPayout.t.sol | 158 +++++++++++++----- .../modules/Condenser/MockCondenserModule.sol | 6 +- .../Derivative/MockDerivativeModule.sol | 28 +++- 7 files changed, 190 insertions(+), 84 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index b07aae60..f0e120e3 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -404,7 +404,6 @@ 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 lot is a derivative, transfers the payout token to the derivative module /// /// This function reverts if: /// - Approval has not been granted to transfer the payout token @@ -450,16 +449,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { revert UnsupportedToken(address(routingParams_.baseToken)); } } - - // If the lot is a derivative, transfer the base token as collateral to the derivative module - if (fromVeecode(routingParams_.derivativeReference) != bytes7("")) { - // Get the details of the derivative module - address derivativeModuleAddress = - _getModuleIfInstalled(routingParams_.derivativeReference); - - // Transfer the base token to the derivative module - routingParams_.baseToken.safeTransfer(derivativeModuleAddress, payoutAmount_); - } } /// @notice Sends the payout token to the recipient @@ -524,6 +513,9 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { derivativeParams = condenser.condense(auctionOutput_, derivativeParams); } + // Approve the module to transfer payout tokens when minting + routingParams_.baseToken.safeApprove(address(module), payoutAmount_); + // Call the module to mint derivative tokens to the recipient module.mint(recipient_, derivativeParams, payoutAmount_, routingParams_.wrapDerivative); } diff --git a/src/modules/Derivative.sol b/src/modules/Derivative.sol index eadbfabe..85329a85 100644 --- a/src/modules/Derivative.sol +++ b/src/modules/Derivative.sol @@ -36,20 +36,26 @@ abstract contract Derivative { bool wrapped_ ) external virtual returns (uint256 tokenId_, address wrappedAddress_); - /// @notice Mint new derivative tokens. Deploys the derivative token if it does not already exist. - /// @param to_ The address to mint the derivative tokens to - /// @param params_ ABI-encoded parameters for the derivative to be created - /// @param amount_ The amount of derivative tokens to create - /// @param wrapped_ Whether (true) or not (false) the derivative should be wrapped in an ERC20 token for composability - /// @return tokenId_ The ID of the newly created derivative token - /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. - /// @return amountCreated_ The amount of derivative tokens created + /// @notice Mint new derivative tokens. + /// @notice Deploys the derivative token if it does not already exist. + /// @notice The module is expected to transfer the collateral token to itself. + /// + /// @param to_ The address to mint the derivative tokens to + /// @param params_ ABI-encoded parameters for the derivative to be created + /// @param amount_ The amount of derivative tokens to create + /// @param wrapped_ Whether (true) or not (false) the derivative should be wrapped in an ERC20 token for composability + /// @return tokenId_ The ID of the newly created derivative token + /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. + /// @return amountCreated_ The amount of derivative tokens created function mint( address to_, bytes memory params_, uint256 amount_, bool wrapped_ - ) external virtual returns (uint256, address, uint256); + ) + external + virtual + returns (uint256 tokenId_, address wrappedAddress_, uint256 amountCreated_); /// @notice Mint new derivative tokens for a specific token Id /// @param to_ The address to mint the derivative tokens to diff --git a/test/AuctionHouse/collectPayout.t.sol b/test/AuctionHouse/collectPayout.t.sol index 6375a0dd..32e82d6d 100644 --- a/test/AuctionHouse/collectPayout.t.sol +++ b/test/AuctionHouse/collectPayout.t.sol @@ -340,11 +340,19 @@ contract CollectPayoutTest is Test, Permit2User { auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); // Expect payout token balance to be transferred to the derivative module - assertEq(payoutToken.balanceOf(OWNER), 0); - assertEq(payoutToken.balanceOf(USER), 0); - assertEq(payoutToken.balanceOf(address(auctionHouse)), 0); - assertEq(payoutToken.balanceOf(address(hook)), 0); - assertEq(payoutToken.balanceOf(address(mockDerivativeModule)), payoutAmount); + 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" + ); // Expect the hook to be called prior to any transfer of the payout token assertEq(hook.midHookCalled(), true); @@ -420,11 +428,19 @@ contract CollectPayoutTest is Test, Permit2User { vm.prank(USER); auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); - // Expect payout token balance to be transferred to the derivative module - assertEq(payoutToken.balanceOf(OWNER), 0); - assertEq(payoutToken.balanceOf(USER), 0); - assertEq(payoutToken.balanceOf(address(auctionHouse)), 0); - assertEq(payoutToken.balanceOf(address(hook)), 0); - assertEq(payoutToken.balanceOf(address(mockDerivativeModule)), payoutAmount); + // Expect payout token balance to be transferred to the auction house + 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 abf7884f..20435847 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -547,14 +547,16 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.installModule(mockDerivativeModule); // Deploy a new derivative token + MockDerivativeModule.DeployParams memory deployParams = + MockDerivativeModule.DeployParams({collateralToken: address(baseToken)}); (uint256 tokenId,) = - auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(""), false); + auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(deployParams), false); // Set up a new auction with a derivative derivativeTokenId = tokenId; routingParams.derivativeType = toKeycode("DERV"); routingParams.derivativeParams = - abi.encode(MockDerivativeModule.Params({tokenId: derivativeTokenId, multiplier: 0})); + abi.encode(MockDerivativeModule.MintParams({tokenId: derivativeTokenId, multiplier: 0})); vm.prank(auctionOwner); lotId = auctionHouse.auction(routingParams, auctionParams); diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index 965de2b3..b809f1ef 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -93,7 +93,7 @@ contract SendPayoutTest is Test, Permit2User { _; } - modifier givenRouterHasBalance(uint256 amount_) { + modifier givenAuctionHouseHasBalance(uint256 amount_) { payoutToken.mint(address(auctionHouse), amount_); _; } @@ -135,7 +135,7 @@ contract SendPayoutTest is Test, Permit2User { public givenAuctionHasHook givenPostHookReverts - givenRouterHasBalance(payoutAmount) + givenAuctionHouseHasBalance(payoutAmount) { // Expect revert vm.expectRevert("revert"); @@ -148,7 +148,7 @@ contract SendPayoutTest is Test, Permit2User { function test_hooks_feeOnTransfer_reverts() public givenAuctionHasHook - givenRouterHasBalance(payoutAmount) + givenAuctionHouseHasBalance(payoutAmount) givenTokenTakesFeeOnTransfer { // Expect revert @@ -161,7 +161,16 @@ contract SendPayoutTest is Test, Permit2User { auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); } - function test_hooks() public givenAuctionHasHook givenRouterHasBalance(payoutAmount) { + function test_hooks_insufficientBalance_reverts() public givenAuctionHasHook { + // Expect revert + vm.expectRevert(bytes("TRANSFER_FAILED")); + + // Call + vm.prank(USER); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); + } + + function test_hooks() public givenAuctionHasHook givenAuctionHouseHasBalance(payoutAmount) { // Call vm.prank(USER); auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); @@ -213,7 +222,7 @@ contract SendPayoutTest is Test, Permit2User { function test_noHooks_feeOnTransfer_reverts() public - givenRouterHasBalance(payoutAmount) + givenAuctionHouseHasBalance(payoutAmount) givenTokenTakesFeeOnTransfer { // Expect revert @@ -226,7 +235,16 @@ contract SendPayoutTest is Test, Permit2User { auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); } - function test_noHooks() public givenRouterHasBalance(payoutAmount) { + function test_noHooks_insufficientBalance_reverts() public { + // Expect revert + vm.expectRevert(bytes("TRANSFER_FAILED")); + + // Call + vm.prank(USER); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); + } + + function test_noHooks() public givenAuctionHouseHasBalance(payoutAmount) { // Call vm.prank(USER); auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); @@ -266,14 +284,16 @@ contract SendPayoutTest is Test, Permit2User { auctionHouse.installModule(mockDerivativeModule); // Deploy a new derivative token + MockDerivativeModule.DeployParams memory deployParams = + MockDerivativeModule.DeployParams({collateralToken: address(payoutToken)}); (uint256 tokenId,) = - auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(""), false); + auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(deployParams), false); // Update parameters derivativeReference = mockDerivativeModule.VEECODE(); derivativeTokenId = tokenId; derivativeParams = - abi.encode(MockDerivativeModule.Params({tokenId: derivativeTokenId, multiplier: 0})); + abi.encode(MockDerivativeModule.MintParams({tokenId: derivativeTokenId, multiplier: 0})); routingParams.derivativeReference = derivativeReference; routingParams.derivativeParams = derivativeParams; _; @@ -281,14 +301,16 @@ contract SendPayoutTest is Test, Permit2User { modifier givenDerivativeIsWrapped() { // Deploy a new wrapped derivative token + MockDerivativeModule.DeployParams memory deployParams = + MockDerivativeModule.DeployParams({collateralToken: address(payoutToken)}); (uint256 tokenId_, address wrappedToken_) = - auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(""), true); + auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(deployParams), true); // Update parameters wrappedDerivative = ERC20(wrappedToken_); derivativeTokenId = tokenId_; derivativeParams = - abi.encode(MockDerivativeModule.Params({tokenId: derivativeTokenId, multiplier: 0})); + abi.encode(MockDerivativeModule.MintParams({tokenId: derivativeTokenId, multiplier: 0})); routingParams.derivativeParams = derivativeParams; wrapDerivative = true; @@ -317,6 +339,7 @@ contract SendPayoutTest is Test, Permit2User { function test_derivative_invalidParams() public + givenAuctionHouseHasBalance(payoutAmount) givenAuctionHasDerivative givenDerivativeParamsAreInvalid { @@ -328,7 +351,20 @@ contract SendPayoutTest is Test, Permit2User { auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); } - function test_derivative() public givenAuctionHasDerivative { + function test_derivative_insufficientBalance_reverts() public givenAuctionHasDerivative { + // Expect revert + vm.expectRevert(bytes("TRANSFER_FROM_FAILED")); + + // Call + vm.prank(USER); + auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); + } + + function test_derivative() + public + givenAuctionHouseHasBalance(payoutAmount) + givenAuctionHasDerivative + { // Call vm.prank(USER); auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); @@ -337,121 +373,151 @@ contract SendPayoutTest is Test, Permit2User { assertEq( mockDerivativeModule.derivativeToken().balanceOf(USER, derivativeTokenId), 0, - "user balance mismatch" + "derivative token: user balance mismatch" ); assertEq( mockDerivativeModule.derivativeToken().balanceOf(OWNER, derivativeTokenId), 0, - "owner balance mismatch" + "derivative token: owner balance mismatch" ); assertEq( mockDerivativeModule.derivativeToken().balanceOf( address(auctionHouse), derivativeTokenId ), 0, - "auctionHouse balance mismatch" + "derivative token: auctionHouse balance mismatch" ); assertEq( mockDerivativeModule.derivativeToken().balanceOf(address(hook), derivativeTokenId), 0, - "hook balance mismatch" + "derivative token: hook balance mismatch" ); assertEq( mockDerivativeModule.derivativeToken().balanceOf(RECIPIENT, derivativeTokenId), payoutAmount, - "recipient balance mismatch" + "derivative token: recipient balance mismatch" ); assertEq( mockDerivativeModule.derivativeToken().balanceOf( address(mockDerivativeModule), derivativeTokenId ), 0, - "derivative module balance mismatch" + "derivative token: derivative module balance mismatch" ); // Check balances of payout token - assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); - assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); - assertEq(payoutToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); - assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); - assertEq(payoutToken.balanceOf(RECIPIENT), 0, "recipient balance mismatch"); + assertEq(payoutToken.balanceOf(USER), 0, "payout token: user balance mismatch"); + assertEq(payoutToken.balanceOf(OWNER), 0, "payout token: owner balance mismatch"); + assertEq( + payoutToken.balanceOf(address(auctionHouse)), + 0, + "payout token: auctionHouse balance mismatch" + ); + assertEq(payoutToken.balanceOf(address(hook)), 0, "payout token: hook balance mismatch"); + assertEq(payoutToken.balanceOf(RECIPIENT), 0, "payout token: recipient balance mismatch"); assertEq( payoutToken.balanceOf(address(mockDerivativeModule)), - 0, // This would normally be non-zero, but we didn't transfer the collateral to it - "derivative module balance mismatch" + payoutAmount, + "payout token: derivative module balance mismatch" ); } - function test_derivative_wrapped() public givenAuctionHasDerivative givenDerivativeIsWrapped { + function test_derivative_wrapped() + public + givenAuctionHouseHasBalance(payoutAmount) + givenAuctionHasDerivative + givenDerivativeIsWrapped + { // Call vm.prank(USER); auctionHouse.sendPayout(lotId, RECIPIENT, payoutAmount, routingParams, auctionOutput); // Check balances of the wrapped derivative token - assertEq(wrappedDerivative.balanceOf(USER), 0, "user balance mismatch"); - assertEq(wrappedDerivative.balanceOf(OWNER), 0, "owner balance mismatch"); assertEq( - wrappedDerivative.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch" + wrappedDerivative.balanceOf(USER), 0, "wrapped derivative token: user balance mismatch" + ); + assertEq( + wrappedDerivative.balanceOf(OWNER), + 0, + "wrapped derivative token: owner balance mismatch" + ); + assertEq( + wrappedDerivative.balanceOf(address(auctionHouse)), + 0, + "wrapped derivative token: auctionHouse balance mismatch" + ); + assertEq( + wrappedDerivative.balanceOf(address(hook)), + 0, + "wrapped derivative token: hook balance mismatch" + ); + assertEq( + wrappedDerivative.balanceOf(RECIPIENT), + payoutAmount, + "wrapped derivative token: recipient balance mismatch" ); - assertEq(wrappedDerivative.balanceOf(address(hook)), 0, "hook balance mismatch"); - assertEq(wrappedDerivative.balanceOf(RECIPIENT), payoutAmount, "recipient balance mismatch"); assertEq( wrappedDerivative.balanceOf(address(mockDerivativeModule)), 0, - "derivative module balance mismatch" + "wrapped derivative token: derivative module balance mismatch" ); // Check balances of the derivative token assertEq( mockDerivativeModule.derivativeToken().balanceOf(USER, derivativeTokenId), 0, - "user balance mismatch" + "derivative token: user balance mismatch" ); assertEq( mockDerivativeModule.derivativeToken().balanceOf(OWNER, derivativeTokenId), 0, - "owner balance mismatch" + "derivative token: owner balance mismatch" ); assertEq( mockDerivativeModule.derivativeToken().balanceOf( address(auctionHouse), derivativeTokenId ), 0, - "auctionHouse balance mismatch" + "derivative token: auctionHouse balance mismatch" ); assertEq( mockDerivativeModule.derivativeToken().balanceOf(address(hook), derivativeTokenId), 0, - "hook balance mismatch" + "derivative token: hook balance mismatch" ); assertEq( mockDerivativeModule.derivativeToken().balanceOf(RECIPIENT, derivativeTokenId), 0, // No raw derivative - "recipient balance mismatch" + "derivative token: recipient balance mismatch" ); assertEq( mockDerivativeModule.derivativeToken().balanceOf( address(mockDerivativeModule), derivativeTokenId ), 0, - "derivative module balance mismatch" + "derivative token: derivative module balance mismatch" ); // Check balances of payout token - assertEq(payoutToken.balanceOf(USER), 0, "user balance mismatch"); - assertEq(payoutToken.balanceOf(OWNER), 0, "owner balance mismatch"); - assertEq(payoutToken.balanceOf(address(auctionHouse)), 0, "auctionHouse balance mismatch"); - assertEq(payoutToken.balanceOf(address(hook)), 0, "hook balance mismatch"); - assertEq(payoutToken.balanceOf(RECIPIENT), 0, "recipient balance mismatch"); + assertEq(payoutToken.balanceOf(USER), 0, "payout token: user balance mismatch"); + assertEq(payoutToken.balanceOf(OWNER), 0, "payout token: owner balance mismatch"); + assertEq( + payoutToken.balanceOf(address(auctionHouse)), + 0, + "payout token: auctionHouse balance mismatch" + ); + assertEq(payoutToken.balanceOf(address(hook)), 0, "payout token: hook balance mismatch"); + assertEq(payoutToken.balanceOf(RECIPIENT), 0, "payout token: recipient balance mismatch"); assertEq( payoutToken.balanceOf(address(mockDerivativeModule)), - 0, // This would normally be non-zero, but we didn't transfer the collateral to it - "derivative module balance mismatch" + payoutAmount, + "payout token: derivative module balance mismatch" ); } function test_derivative_wrapped_invalidParams() public + givenAuctionHouseHasBalance(payoutAmount) givenAuctionHasDerivative givenDerivativeIsWrapped givenDerivativeParamsAreInvalid @@ -466,6 +532,7 @@ contract SendPayoutTest is Test, Permit2User { function test_derivative_condenser_invalidParams_reverts() public + givenAuctionHouseHasBalance(payoutAmount) givenAuctionHasDerivative givenDerivativeHasCondenser givenDerivativeParamsAreInvalid @@ -480,6 +547,7 @@ contract SendPayoutTest is Test, Permit2User { function test_derivative_condenser() public + givenAuctionHouseHasBalance(payoutAmount) givenAuctionHasDerivative givenDerivativeHasCondenser { @@ -531,7 +599,7 @@ contract SendPayoutTest is Test, Permit2User { assertEq(payoutToken.balanceOf(RECIPIENT), 0, "recipient balance mismatch"); assertEq( payoutToken.balanceOf(address(mockDerivativeModule)), - 0, // This would normally be non-zero, but we didn't transfer the collateral to it + payoutAmount, "derivative module balance mismatch" ); } diff --git a/test/modules/Condenser/MockCondenserModule.sol b/test/modules/Condenser/MockCondenserModule.sol index 90711d29..168eede4 100644 --- a/test/modules/Condenser/MockCondenserModule.sol +++ b/test/modules/Condenser/MockCondenserModule.sol @@ -31,10 +31,10 @@ contract MockCondenserModule is CondenserModule { // Get derivative params if (derivativeConfig_.length != 64) revert(""); - MockDerivativeModule.Params memory originalDerivativeParams = - abi.decode(derivativeConfig_, (MockDerivativeModule.Params)); + MockDerivativeModule.MintParams memory originalDerivativeParams = + abi.decode(derivativeConfig_, (MockDerivativeModule.MintParams)); - MockDerivativeModule.Params memory derivativeParams = MockDerivativeModule.Params({ + MockDerivativeModule.MintParams memory derivativeParams = MockDerivativeModule.MintParams({ tokenId: originalDerivativeParams.tokenId, multiplier: auctionOutput.multiplier }); diff --git a/test/modules/Derivative/MockDerivativeModule.sol b/test/modules/Derivative/MockDerivativeModule.sol index 4629c9a9..46e58ab6 100644 --- a/test/modules/Derivative/MockDerivativeModule.sol +++ b/test/modules/Derivative/MockDerivativeModule.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.19; import {ClonesWithImmutableArgs} from "src/lib/clones/ClonesWithImmutableArgs.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; // Modules import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; @@ -14,6 +16,7 @@ import {MockWrappedDerivative} from "test/lib/mocks/MockWrappedDerivative.sol"; contract MockDerivativeModule is DerivativeModule { using ClonesWithImmutableArgs for address; + using SafeTransferLib for ERC20; bool internal validateFails; MockERC6909 public derivativeToken; @@ -22,7 +25,11 @@ contract MockDerivativeModule is DerivativeModule { error InvalidDerivativeParams(); - struct Params { + struct DeployParams { + address collateralToken; + } + + struct MintParams { uint256 tokenId; uint256 multiplier; } @@ -46,6 +53,13 @@ contract MockDerivativeModule is DerivativeModule { uint256 tokenId = tokenCount; address wrappedAddress; + // Check length + if (params_.length != 32) revert InvalidDerivativeParams(); + + // Decode params + DeployParams memory decodedParams = abi.decode(params_, (DeployParams)); + if (decodedParams.collateralToken == address(0)) revert InvalidDerivativeParams(); + if (wrapped_) { // If there is no wrapped implementation, abort if (address(wrappedImplementation) == address(0)) revert(""); @@ -63,7 +77,7 @@ contract MockDerivativeModule is DerivativeModule { decimals: 18, name: "Mock Derivative", symbol: "MDER", - data: "" + data: params_ // Should collateralToken be present on every set of metadata? }); // Store metadata @@ -82,7 +96,9 @@ contract MockDerivativeModule is DerivativeModule { ) external virtual override returns (uint256, address, uint256) { if (params_.length != 64) revert(""); - Params memory params = abi.decode(params_, (Params)); + // TODO this should be deploying a new derivative token if it doesn't exist + + MintParams memory params = abi.decode(params_, (MintParams)); // Check that tokenId exists Token storage token = tokenMetadata[params.tokenId]; @@ -91,6 +107,12 @@ contract MockDerivativeModule is DerivativeModule { // Check that the wrapped status is correct if (token.wrapped != address(0) && !wrapped_) revert(""); + // Decode extra token data + DeployParams memory decodedParams = abi.decode(token.data, (DeployParams)); + + // Transfer collateral token to this contract + ERC20(decodedParams.collateralToken).safeTransferFrom(msg.sender, address(this), amount_); + uint256 outputAmount = params.multiplier == 0 ? amount_ : amount_ * params.multiplier; // If wrapped, mint and deposit From 1ca17df215d2b7f38ae42653904e7509696c37d1 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 13:13:24 +0400 Subject: [PATCH 50/82] Remove deploy from AuctionHouse and Derivatizer interface --- src/AuctionHouse.sol | 17 ---- src/bases/Derivatizer.sol | 88 +++++++++---------- test/AuctionHouse/purchase.t.sol | 3 +- test/AuctionHouse/sendPayout.t.sol | 5 +- .../Derivative/MockDerivativeModule.sol | 2 +- 5 files changed, 48 insertions(+), 67 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index f0e120e3..bb6b40e9 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -160,23 +160,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _PERMIT2 = IPermit2(permit2_); } - // ========== DERIVATIVE MANAGEMENT ========== // - - /// @inheritdoc Derivatizer - function deploy( - Veecode dType, - bytes memory data, - bool wrapped - ) external override returns (uint256, address) { - // Load the derivative module, will revert if not installed or sunset - DerivativeModule derivative = DerivativeModule(_getModuleIfInstalled(dType)); - - // Call the deploy function on the derivative module - (uint256 tokenId, address wrappedToken) = derivative.deploy(data, wrapped); - - return (tokenId, wrappedToken); - } - // ========== DIRECT EXECUTION ========== // // ========== AUCTION FUNCTIONS ========== // diff --git a/src/bases/Derivatizer.sol b/src/bases/Derivatizer.sol index bad642d5..32589a79 100644 --- a/src/bases/Derivatizer.sol +++ b/src/bases/Derivatizer.sol @@ -4,61 +4,61 @@ pragma solidity 0.8.19; import {WithModules, Veecode} from "src/modules/Modules.sol"; abstract contract Derivatizer is WithModules { - // ========== DERIVATIVE MANAGEMENT ========== // +// ========== DERIVATIVE MANAGEMENT ========== // - /// @notice Deploys a new derivative token - /// - /// @param dType The derivative module code - /// @param data The derivative module parameters - /// @param wrapped Whether or not to wrap the derivative token - /// - /// @return tokenId The unique derivative token ID - /// @return wrappedToken The wrapped derivative token address (or zero) - function deploy( - Veecode dType, - bytes memory data, - bool wrapped - ) external virtual returns (uint256 tokenId, address wrappedToken); +// /// @notice Deploys a new derivative token +// /// +// /// @param dType The derivative module code +// /// @param data The derivative module parameters +// /// @param wrapped Whether or not to wrap the derivative token +// /// +// /// @return tokenId The unique derivative token ID +// /// @return wrappedToken The wrapped derivative token address (or zero) +// function deploy( +// Veecode dType, +// bytes memory data, +// bool wrapped +// ) external virtual returns (uint256 tokenId, address wrappedToken); - // function mint( - // bytes memory data, - // uint256 amount, - // bool wrapped - // ) external virtual returns (bytes memory); - // function mint( - // uint256 tokenId, - // uint256 amount, - // bool wrapped - // ) external virtual returns (bytes memory); +// function mint( +// bytes memory data, +// uint256 amount, +// bool wrapped +// ) external virtual returns (bytes memory); +// function mint( +// uint256 tokenId, +// uint256 amount, +// bool wrapped +// ) external virtual returns (bytes memory); - // function redeem(bytes memory data, uint256 amount) external virtual; +// function redeem(bytes memory data, uint256 amount) external virtual; - // // function batchRedeem(bytes[] memory data, uint256[] memory amounts) external virtual; +// // function batchRedeem(bytes[] memory data, uint256[] memory amounts) external virtual; - // function exercise(bytes memory data, uint256 amount) external virtual; +// function exercise(bytes memory data, uint256 amount) external virtual; - // function reclaim(bytes memory data) external virtual; +// function reclaim(bytes memory data) external virtual; - // function convert(bytes memory data, uint256 amount) external virtual; +// function convert(bytes memory data, uint256 amount) external virtual; - // // TODO Consider best inputs for UX - // function wrap(uint256 tokenId, uint256 amount) external virtual; - // function unwrap(uint256 tokenId, uint256 amount) external virtual; +// // TODO Consider best inputs for UX +// function wrap(uint256 tokenId, uint256 amount) external virtual; +// function unwrap(uint256 tokenId, uint256 amount) external virtual; - // // ========== DERIVATIVE INFORMATION ========== // +// // ========== DERIVATIVE INFORMATION ========== // - // // TODO view function to format implementation specific token data correctly and return to user +// // TODO view function to format implementation specific token data correctly and return to user - // function exerciseCost( - // bytes memory data, - // uint256 amount - // ) external view virtual returns (uint256); +// function exerciseCost( +// bytes memory data, +// uint256 amount +// ) external view virtual returns (uint256); - // function convertsTo( - // bytes memory data, - // uint256 amount - // ) external view virtual returns (uint256); +// function convertsTo( +// bytes memory data, +// uint256 amount +// ) external view virtual returns (uint256); - // // Compute unique token ID for params on the submodule - // function computeId(bytes memory params_) external pure virtual returns (uint256); +// // Compute unique token ID for params on the submodule +// function computeId(bytes memory params_) external pure virtual returns (uint256); } diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 20435847..1f412238 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -549,8 +549,7 @@ contract PurchaseTest is Test, Permit2User { // Deploy a new derivative token MockDerivativeModule.DeployParams memory deployParams = MockDerivativeModule.DeployParams({collateralToken: address(baseToken)}); - (uint256 tokenId,) = - auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(deployParams), false); + (uint256 tokenId,) = mockDerivativeModule.deploy(abi.encode(deployParams), false); // Set up a new auction with a derivative derivativeTokenId = tokenId; diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index b809f1ef..d996f16f 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -286,8 +286,7 @@ contract SendPayoutTest is Test, Permit2User { // Deploy a new derivative token MockDerivativeModule.DeployParams memory deployParams = MockDerivativeModule.DeployParams({collateralToken: address(payoutToken)}); - (uint256 tokenId,) = - auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(deployParams), false); + (uint256 tokenId,) = mockDerivativeModule.deploy(abi.encode(deployParams), false); // Update parameters derivativeReference = mockDerivativeModule.VEECODE(); @@ -304,7 +303,7 @@ contract SendPayoutTest is Test, Permit2User { MockDerivativeModule.DeployParams memory deployParams = MockDerivativeModule.DeployParams({collateralToken: address(payoutToken)}); (uint256 tokenId_, address wrappedToken_) = - auctionHouse.deploy(mockDerivativeModule.VEECODE(), abi.encode(deployParams), true); + mockDerivativeModule.deploy(abi.encode(deployParams), true); // Update parameters wrappedDerivative = ERC20(wrappedToken_); diff --git a/test/modules/Derivative/MockDerivativeModule.sol b/test/modules/Derivative/MockDerivativeModule.sol index 46e58ab6..8cae52f2 100644 --- a/test/modules/Derivative/MockDerivativeModule.sol +++ b/test/modules/Derivative/MockDerivativeModule.sol @@ -49,7 +49,7 @@ contract MockDerivativeModule is DerivativeModule { function deploy( bytes memory params_, bool wrapped_ - ) external virtual override onlyParent returns (uint256, address) { + ) external virtual override returns (uint256, address) { uint256 tokenId = tokenCount; address wrappedAddress; 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 51/82] 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 52/82] 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 53/82] 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 54/82] 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 55/82] 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 56/82] 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 57/82] 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 58/82] 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 59/82] 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 60/82] 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 61/82] 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 62/82] 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 63/82] 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 64/82] 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 65/82] 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 f6a058388473db79d15120644e8e151070899cb9 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 11:25:02 +0400 Subject: [PATCH 66/82] transferFrom --- src/AuctionHouse.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index bb6b40e9..c098a83a 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -344,13 +344,13 @@ 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( + _permit2TransferFrom( amount_, quoteToken_, approvalDeadline_, approvalNonce_, approvalSignature_ ); } // Otherwise fallback to a standard ERC20 transfer else { - _transfer(amount_, quoteToken_); + _transferFrom(amount_, quoteToken_); } } @@ -522,7 +522,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// /// @param amount_ Amount of tokens to transfer (in native decimals) /// @param token_ Token to transfer - function _transfer(uint256 amount_, ERC20 token_) internal { + function _transferFrom(uint256 amount_, ERC20 token_) internal { uint256 balanceBefore = token_.balanceOf(address(this)); // Transfer the quote token from the user @@ -551,7 +551,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @param approvalDeadline_ Deadline for Permit2 approval signature /// @param approvalNonce_ Nonce for Permit2 approval signature /// @param approvalSignature_ Permit2 approval signature for the token - function _permit2Transfer( + function _permit2TransferFrom( uint256 amount_, ERC20 token_, uint48 approvalDeadline_, From 7b1b42d313ddde7b7e48c0599451a4c020bcac3b Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 12:11:36 +0400 Subject: [PATCH 67/82] Add underlying token to deploy/mint functions. Correct implementation of MockDerivativeModule. --- src/AuctionHouse.sol | 8 +- src/modules/Derivative.sol | 26 ++-- test/AuctionHouse/purchase.t.sol | 12 +- test/AuctionHouse/sendPayout.t.sol | 21 +-- .../modules/Condenser/MockCondenserModule.sol | 9 +- .../Derivative/MockDerivativeModule.sol | 128 ++++++++++-------- 6 files changed, 119 insertions(+), 85 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index c098a83a..5efca599 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -500,7 +500,13 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { routingParams_.baseToken.safeApprove(address(module), payoutAmount_); // Call the module to mint derivative tokens to the recipient - module.mint(recipient_, derivativeParams, payoutAmount_, routingParams_.wrapDerivative); + module.mint( + recipient_, + address(routingParams_.baseToken), + derivativeParams, + payoutAmount_, + routingParams_.wrapDerivative + ); } // Call post hook on hooks contract if provided diff --git a/src/modules/Derivative.sol b/src/modules/Derivative.sol index 85329a85..652cbf14 100644 --- a/src/modules/Derivative.sol +++ b/src/modules/Derivative.sol @@ -27,11 +27,13 @@ abstract contract Derivative { /// @notice Deploy a new derivative token. Optionally, deploys an ERC20 wrapper for composability. /// - /// @param params_ ABI-encoded parameters for the derivative to be created - /// @param wrapped_ Whether (true) or not (false) the derivative should be wrapped in an ERC20 token for composability - /// @return tokenId_ The ID of the newly created derivative token - /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. + /// @param underlyingToken_ The address of the underlying token + /// @param params_ ABI-encoded parameters for the derivative to be created + /// @param wrapped_ Whether (true) or not (false) the derivative should be wrapped in an ERC20 token for composability + /// @return tokenId_ The ID of the newly created derivative token + /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. function deploy( + address underlyingToken_, bytes memory params_, bool wrapped_ ) external virtual returns (uint256 tokenId_, address wrappedAddress_); @@ -40,15 +42,17 @@ abstract contract Derivative { /// @notice Deploys the derivative token if it does not already exist. /// @notice The module is expected to transfer the collateral token to itself. /// - /// @param to_ The address to mint the derivative tokens to - /// @param params_ ABI-encoded parameters for the derivative to be created - /// @param amount_ The amount of derivative tokens to create - /// @param wrapped_ Whether (true) or not (false) the derivative should be wrapped in an ERC20 token for composability - /// @return tokenId_ The ID of the newly created derivative token - /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. - /// @return amountCreated_ The amount of derivative tokens created + /// @param to_ The address to mint the derivative tokens to + /// @param underlyingToken_ The address of the underlying token + /// @param params_ ABI-encoded parameters for the derivative to be created + /// @param amount_ The amount of derivative tokens to create + /// @param wrapped_ Whether (true) or not (false) the derivative should be wrapped in an ERC20 token for composability + /// @return tokenId_ The ID of the newly created derivative token + /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. + /// @return amountCreated_ The amount of derivative tokens created function mint( address to_, + address underlyingToken_, bytes memory params_, uint256 amount_, bool wrapped_ diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 1f412238..e5892b3f 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -56,6 +56,8 @@ contract PurchaseTest is Test, Permit2User { uint256 internal constant AMOUNT_IN = 1e18; uint256 internal AMOUNT_OUT; + uint48 internal constant DERIVATIVE_EXPIRY = 1 days; + uint48 internal referrerFee; uint48 internal protocolFee; @@ -547,15 +549,15 @@ contract PurchaseTest is Test, Permit2User { auctionHouse.installModule(mockDerivativeModule); // Deploy a new derivative token - MockDerivativeModule.DeployParams memory deployParams = - MockDerivativeModule.DeployParams({collateralToken: address(baseToken)}); - (uint256 tokenId,) = mockDerivativeModule.deploy(abi.encode(deployParams), false); + MockDerivativeModule.DerivativeParams memory deployParams = + MockDerivativeModule.DerivativeParams({expiry: DERIVATIVE_EXPIRY, multiplier: 0}); + (uint256 tokenId,) = + mockDerivativeModule.deploy(address(baseToken), abi.encode(deployParams), false); // Set up a new auction with a derivative derivativeTokenId = tokenId; routingParams.derivativeType = toKeycode("DERV"); - routingParams.derivativeParams = - abi.encode(MockDerivativeModule.MintParams({tokenId: derivativeTokenId, multiplier: 0})); + routingParams.derivativeParams = abi.encode(deployParams); vm.prank(auctionOwner); lotId = auctionHouse.auction(routingParams, auctionParams); diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index d996f16f..3c280f68 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -33,6 +33,8 @@ contract SendPayoutTest is Test, Permit2User { address internal OWNER = address(0x3); address internal RECIPIENT = address(0x4); + uint48 internal constant DERIVATIVE_EXPIRY = 1 days; + // Function parameters uint256 internal lotId = 1; uint256 internal payoutAmount = 10e18; @@ -284,15 +286,15 @@ contract SendPayoutTest is Test, Permit2User { auctionHouse.installModule(mockDerivativeModule); // Deploy a new derivative token - MockDerivativeModule.DeployParams memory deployParams = - MockDerivativeModule.DeployParams({collateralToken: address(payoutToken)}); - (uint256 tokenId,) = mockDerivativeModule.deploy(abi.encode(deployParams), false); + MockDerivativeModule.DerivativeParams memory deployParams = + MockDerivativeModule.DerivativeParams({expiry: DERIVATIVE_EXPIRY, multiplier: 0}); + (uint256 tokenId,) = + mockDerivativeModule.deploy(address(payoutToken), abi.encode(deployParams), false); // Update parameters derivativeReference = mockDerivativeModule.VEECODE(); derivativeTokenId = tokenId; - derivativeParams = - abi.encode(MockDerivativeModule.MintParams({tokenId: derivativeTokenId, multiplier: 0})); + derivativeParams = abi.encode(deployParams); routingParams.derivativeReference = derivativeReference; routingParams.derivativeParams = derivativeParams; _; @@ -300,16 +302,15 @@ contract SendPayoutTest is Test, Permit2User { modifier givenDerivativeIsWrapped() { // Deploy a new wrapped derivative token - MockDerivativeModule.DeployParams memory deployParams = - MockDerivativeModule.DeployParams({collateralToken: address(payoutToken)}); + MockDerivativeModule.DerivativeParams memory deployParams = + MockDerivativeModule.DerivativeParams({expiry: DERIVATIVE_EXPIRY + 1, multiplier: 0}); // Different expiry which leads to a different token id (uint256 tokenId_, address wrappedToken_) = - mockDerivativeModule.deploy(abi.encode(deployParams), true); + mockDerivativeModule.deploy(address(payoutToken), abi.encode(deployParams), true); // Update parameters wrappedDerivative = ERC20(wrappedToken_); derivativeTokenId = tokenId_; - derivativeParams = - abi.encode(MockDerivativeModule.MintParams({tokenId: derivativeTokenId, multiplier: 0})); + derivativeParams = abi.encode(deployParams); routingParams.derivativeParams = derivativeParams; wrapDerivative = true; diff --git a/test/modules/Condenser/MockCondenserModule.sol b/test/modules/Condenser/MockCondenserModule.sol index 168eede4..d6325698 100644 --- a/test/modules/Condenser/MockCondenserModule.sol +++ b/test/modules/Condenser/MockCondenserModule.sol @@ -31,11 +31,12 @@ contract MockCondenserModule is CondenserModule { // Get derivative params if (derivativeConfig_.length != 64) revert(""); - MockDerivativeModule.MintParams memory originalDerivativeParams = - abi.decode(derivativeConfig_, (MockDerivativeModule.MintParams)); + MockDerivativeModule.DerivativeParams memory originalDerivativeParams = + abi.decode(derivativeConfig_, (MockDerivativeModule.DerivativeParams)); - MockDerivativeModule.MintParams memory derivativeParams = MockDerivativeModule.MintParams({ - tokenId: originalDerivativeParams.tokenId, + MockDerivativeModule.DerivativeParams memory derivativeParams = MockDerivativeModule + .DerivativeParams({ + expiry: originalDerivativeParams.expiry, multiplier: auctionOutput.multiplier }); diff --git a/test/modules/Derivative/MockDerivativeModule.sol b/test/modules/Derivative/MockDerivativeModule.sol index 8cae52f2..62041f11 100644 --- a/test/modules/Derivative/MockDerivativeModule.sol +++ b/test/modules/Derivative/MockDerivativeModule.sol @@ -20,17 +20,12 @@ contract MockDerivativeModule is DerivativeModule { bool internal validateFails; MockERC6909 public derivativeToken; - uint256 internal tokenCount; MockWrappedDerivative internal wrappedImplementation; error InvalidDerivativeParams(); - struct DeployParams { - address collateralToken; - } - - struct MintParams { - uint256 tokenId; + struct DerivativeParams { + uint48 expiry; uint256 multiplier; } @@ -47,88 +42,61 @@ contract MockDerivativeModule is DerivativeModule { } function deploy( + address underlyingToken_, bytes memory params_, bool wrapped_ ) external virtual override returns (uint256, address) { - uint256 tokenId = tokenCount; - address wrappedAddress; + if (underlyingToken_ == address(0)) revert InvalidDerivativeParams(); // Check length - if (params_.length != 32) revert InvalidDerivativeParams(); + if (params_.length != 64) revert InvalidDerivativeParams(); // Decode params - DeployParams memory decodedParams = abi.decode(params_, (DeployParams)); - if (decodedParams.collateralToken == address(0)) revert InvalidDerivativeParams(); - - if (wrapped_) { - // If there is no wrapped implementation, abort - if (address(wrappedImplementation) == address(0)) revert(""); - - // Deploy the wrapped implementation - wrappedAddress = address(wrappedImplementation).clone3( - abi.encodePacked(derivativeToken, tokenId), bytes32(tokenId) - ); - } - - // Create new token metadata - Token memory tokenData = Token({ - exists: true, - wrapped: wrappedAddress, - decimals: 18, - name: "Mock Derivative", - symbol: "MDER", - data: params_ // Should collateralToken be present on every set of metadata? - }); - - // Store metadata - tokenMetadata[tokenId] = tokenData; - - tokenCount++; + DerivativeParams memory decodedParams = abi.decode(params_, (DerivativeParams)); + (uint256 tokenId, address wrappedAddress) = + _deployIfNeeded(underlyingToken_, decodedParams, wrapped_); return (tokenId, wrappedAddress); } function mint( address to_, + address underlyingToken_, bytes memory params_, uint256 amount_, bool wrapped_ ) external virtual override returns (uint256, address, uint256) { if (params_.length != 64) revert(""); - // TODO this should be deploying a new derivative token if it doesn't exist - - MintParams memory params = abi.decode(params_, (MintParams)); + DerivativeParams memory decodedParams = abi.decode(params_, (DerivativeParams)); - // Check that tokenId exists - Token storage token = tokenMetadata[params.tokenId]; - if (!token.exists) revert(""); + // Deploy if needed + (uint256 tokenId, address wrappedAddress) = + _deployIfNeeded(underlyingToken_, decodedParams, wrapped_); // Check that the wrapped status is correct - if (token.wrapped != address(0) && !wrapped_) revert(""); - - // Decode extra token data - DeployParams memory decodedParams = abi.decode(token.data, (DeployParams)); + if (wrappedAddress != address(0) && !wrapped_) revert(""); // Transfer collateral token to this contract - ERC20(decodedParams.collateralToken).safeTransferFrom(msg.sender, address(this), amount_); + ERC20(underlyingToken_).safeTransferFrom(msg.sender, address(this), amount_); - uint256 outputAmount = params.multiplier == 0 ? amount_ : amount_ * params.multiplier; + uint256 outputAmount = + decodedParams.multiplier == 0 ? amount_ : amount_ * decodedParams.multiplier; // If wrapped, mint and deposit if (wrapped_) { - derivativeToken.mint(address(this), params.tokenId, outputAmount); + derivativeToken.mint(address(this), tokenId, outputAmount); - derivativeToken.approve(token.wrapped, params.tokenId, outputAmount); + derivativeToken.approve(wrappedAddress, tokenId, outputAmount); - MockWrappedDerivative(token.wrapped).deposit(outputAmount, to_); + MockWrappedDerivative(wrappedAddress).deposit(outputAmount, to_); } // Otherwise mint as normal else { - derivativeToken.mint(to_, params.tokenId, outputAmount); + derivativeToken.mint(to_, tokenId, outputAmount); } - return (params.tokenId, token.wrapped, outputAmount); + return (tokenId, wrappedAddress, outputAmount); } function mint( @@ -180,4 +148,56 @@ contract MockDerivativeModule is DerivativeModule { function setWrappedImplementation(MockWrappedDerivative implementation_) external { wrappedImplementation = implementation_; } + + function _computeId(ERC20 base_, uint48 expiry_) internal pure returns (uint256) { + return + uint256(keccak256(abi.encodePacked(VEECODE(), keccak256(abi.encode(base_, expiry_))))); + } + + function _getNameAndSymbol( + ERC20 base_, + uint48 expiry_ + ) internal view returns (string memory, string memory) { + return ( + string(abi.encodePacked(base_.name(), "-", expiry_)), + string(abi.encodePacked(base_.symbol(), "-", expiry_)) + ); + } + + function _deployIfNeeded( + address underlyingToken_, + DerivativeParams memory params_, + bool wrapped_ + ) internal returns (uint256, address) { + address wrappedAddress; + + // Generate the token id + uint256 tokenId = _computeId(ERC20(underlyingToken_), params_.expiry); + + // Check if the derivative exists + Token storage token = tokenMetadata[tokenId]; + if (!token.exists) { + if (wrapped_) { + // If there is no wrapped implementation, abort + if (address(wrappedImplementation) == address(0)) revert(""); + + // Deploy the wrapped implementation + wrappedAddress = address(wrappedImplementation).clone3( + abi.encodePacked(derivativeToken, tokenId), bytes32(tokenId) + ); + token.wrapped = wrappedAddress; + } + + // Store derivative data + token.exists = true; + (token.name, token.symbol) = _getNameAndSymbol(ERC20(underlyingToken_), params_.expiry); + token.decimals = ERC20(underlyingToken_).decimals(); + token.data = abi.encode(params_); + + // Store metadata + tokenMetadata[tokenId] = token; + } + + return (tokenId, token.wrapped); + } } 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 68/82] 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 69/82] 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 70/82] 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 71/82] 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 72/82] 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 73/82] 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 74/82] 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 75/82] 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 76/82] 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 77/82] 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 78/82] 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 629a14619d3da542f643566fe39ea35c454abaed Mon Sep 17 00:00:00 2001 From: Oighty Date: Mon, 22 Jan 2024 13:18:35 -0600 Subject: [PATCH 79/82] docs: design updates --- design/LSBBA.md | 245 ++++++++++++++++++++++++++++++++++++++ design/SEALED_VERSIONS.md | 47 ++++++++ 2 files changed, 292 insertions(+) create mode 100644 design/LSBBA.md create mode 100644 design/SEALED_VERSIONS.md diff --git a/design/LSBBA.md b/design/LSBBA.md new file mode 100644 index 00000000..bbc8bf8d --- /dev/null +++ b/design/LSBBA.md @@ -0,0 +1,245 @@ +# LSBBA: Local Sealed Bid Batch Auctions (name needs work) + +LSSBA is a fully on-chain sealed bid batch auction system built on the Axis Protocol that uses RSA encryption and a multi-step settlement process to avoid issues of previous sealed bid auction designs. The purpose of the system is to allow any seller to create sealed bid batch auctions for any ERC20 token pair. Sellers create an auction for a set amount of base tokens (capacity) and provide an RSA public key for buyers to encrypt their bids with. Buyers encrypt a portion of their bids (the amount out) off-chain using the RSA public key and submit them to the contract. The amount of quote tokens is public and the tokens they're bidding are sent to the contract on the bid. Once an auction ends, the settlement happens in two steps. First, the bids are decrypted off-chain, submitted to the contract, and verified by the contract. This can be done by anyone who has the RSA private key to decrypt the bids off-chain. If the Seller uses the Axis dapp to deploy a market, the RSA keypair is generated by an API and the private key is released after the auction concludes so that anyone can do the decryption. Once all the bids are decrypted, anyone can settle the auction and pay out proceeds to the winners. Those who do not win the auction can claim a refund following settlement. + +## User Features + +### Sellers +- Permissionlessly create sealed bid batch auctions, which improve execution over open bid auctions +- Auctions can be created for any ERC20 token pair +- Two transactions to create auction: 1. approve base token (if not already), 2. create auction +- Can limit auction participants using an allowlist +- Can use hooks to customize transfer logic for auction proceeds +- Can create and auction a derivative of the base token if desired +- Can enforce a minimum price and minimum capacity filled in order for auction to settle +- System is on-chain and transparent + +### Buyers +- Permissionlessly place bids on any auction (assuming no allowlist) +- Bids are encrypted. Protocol key service hides private key from everyone, even sellers, so that no one can peek at your bids until after the auction is over. +- Settlement is permissionless +- System is on-chain and transparent + + +## Components and Design Decisions +This version of a sealed bid auction system has a few key properties that make it attractive: +1. Maximally Permissionless. The entire auction process happens on-chain and can be executed without relying on our off-chain infrastructure. +2. Transparent. All bids are encrypted and stored on-chain, but the bids are not decrypted until after the auction ends. This allows anyone to verify the bids and the settlement process. +3. Simple. As an early version of the system, we wanted to get something out that isn't too difficult to build. + +However, the user experience is not as good as an off-chain or hybrid system could be. Specifically, users must submit transactions to bid, cancel a bid, and claim a refund if they do not win the auction. We plan for this system to only be used on chains where gas costs are low to mitigate the cost to both buyers and sellers. Additionally, because newer L2 chains don't have the same available of off-chain infrastructure and services as mainnet, it isn't possible to do some of the hybrid designs on these chains (e.g. web3 functions). + +### Smart Contracts + +#### Axis Protocol + - Auction House + - LSBBA (Auction Module) + - Vesting Module + +#### Permit2 Approvals +Gasless for buyers after initial approval +[Permit2](https://github.com/Uniswap/permit2): Signature-based approvals for any ERC20 token + - [Integration Guide](https://blog.uniswap.org/permit2-integration-guide) + +### Encryption Key Management +What: Need to be able to encrypt bids and other data with a key that no interested party controls until the auction ends. This is to prevent insider dealing or other bad behavior. + +Solution: API and private database that creates RSA keypairs and store the private key until an auction ends. If an auction is cancelled, the private key is never released. + +### UI +SPA hosted on [IPFS](https://docs.ipfs.tech/how-to/websites-on-ipfs/single-page-website/) with [Eth.limo](https://eth.limo/) domain resolution to ENS owned by the protocol + +## Actions + +### Seller Creates Auction +```mermaid +sequenceDiagram + autoNumber + participant Seller + participant UI + participant API + participant Database + participant AuctionHouse + participant LSBBA + participant Watcher + + Seller->>UI: Navigate to Create Auction page + activate UI + Seller->>UI: Input auction data (baseToken, quoteToken, start, duration, minPrice, capacity, optionally hooks, allowlist, and derivative data) + Seller->>UI: Click "Create Auction" + UI->>API: Request new RSA keypair + activate API + API->>API: Generate new RSA keypair + API->>Database: Store private key + API-->UI: Return public key + deactivate API + UI->>UI: Create transaction to create auction with input data + public key + UI-->Seller: Present transaction to Seller to sign + Seller->>UI: Sign transaction to create auction + UI->>AuctionHouse: Send transaction to blockchain + activate AuctionHouse + AuctionHouse->>AuctionHouse: Validate and store routing & derivative parameters + AuctionHouse->>LSBBA: Call auction module with auction params and lot ID + activate LSBBA + LSBBA->>LSBBA: Validate and store auction parameters + LSBBA-->AuctionHouse: Hand execution back to AuctionHouse + LSBBA-->Watcher: Emit event for auction creation with public key + Watcher->>Database: Store auction ID and conclusion timestamp for public key + deactivate LSBBA + AuctionHouse-->UI: Finish execution and return result + deactivate AuctionHouse + UI-->Seller: Show transaction status + deactivate UI +``` + +### Buyer Places Bid +```mermaid +sequenceDiagram + autoNumber + participant Buyer + participant UI + participant Permit2 + participant AuctionHouse + participant LSBBA + + Buyer->>UI: Navigate to Auction page + UI->>AuctionHouse: Fetch data for auction ID (base token, quote token, public key, start, conclusion, auction type, derivative type + info, capacity, min bid size) + AuctionHouse->>LSBBA: Get data stored on module to return + AuctionHouse-->UI: Return results + UI-->Buyer: Display data for user + Buyer->>UI: Input bid data (amount, minAmountOut) + UI->>Permit2: Check user's approval for quote token + alt user hasn't approved Permit2 + UI-->Buyer: Display "Approve Permit2" button + Buyer->>UI: Click "Approve Permit2" button + UI-->Buyer: Display transaction for signing + Buyer->>UI: Sign approval transaction + UI->>Permit2: Send approval transaction + Permit2-->UI: Return execution result + end + UI-->Buyer: Display "Place bid" button + Buyer->>UI: Click "Place bid" button + UI->>UI: Get random seed for encryption + UI->>UI: Encrypt minAmountOut with public key from auction + UI->>UI: Construct permit2 approval for AuctionHouse + UI-->Buyer: Display permit2 signature request + Buyer->>UI: Sign permit2 signature request + UI->>UI: Create bid transaction with permit2 approval and bid data (amount, encrypted minAmountOut) + UI-->Buyer: Display transaction for signing + Buyer->>UI: Sign bid transaction + UI->>AuctionHouse: Send bid transaction to blockchain + activate AuctionHouse + AuctionHouse->>AuctionHouse: Validate permit2 approval and transfer quote tokens + AuctionHouse->>LSBBA: Call bid function with auction ID, amount, and encrypted minAmountOut + activate LSBBA + LSBBA->>LSBBA: Validate bid parameters and store encrypted bid data + LSBBA-->AuctionHouse: Hand execution back to AuctionHouse + deactivate LSBBA + AuctionHouse-->UI: Return transaction result + deactivate AuctionHouse + UI-->Buyer: Display submission result to user +``` + +### Buyer Cancels Bid +TODO + +### Seller Cancels Auction +TODO + +### Auction Settlement Part 1: Decryption +After an auction has concluded, the bids must be decrypted and sorted by price. We do this by releasing the RSA private key from the database via the API to anyone on the dapp looking at the auction page. Decryption works by providing an array of (amountOut, seed) for the bids in the order they were submitted to the contract. The seed was randomly generated on submission and can be extracted by decrypting the bid. The API provides a convenience function for returning an array for the next bids that need to be decrypted from an auction (using its ID). It may take several decryption transactions depending on the number of bids and gas limit. Once, all bids are decrypted on the contract, we can move to part 2 of the settlement process. + +Note: this requires direct transactions to the LSBBA auction module since the `decryptBids` function is not generic and won't be on the AuctionHouse + +```mermaid +sequenceDiagram + autoNumber + participant User + participant UI + participant API + participant Database + participant LSBBA + + User->>UI: Navigate to Auction page + UI-->User: Display auction status and percent of bids decrypted + User->>UI: Click "Decrypt bids" button + UI->>API: Request next bids to decrypt for auction ID + activate API + API->>Database: Get private key for auction ID + API->>LSBBA: Get encrypted bids and nextDecryptId for auction ID + activate LSBBA + LSBBA->>LSBBA: Get encrypted bids for auction ID + LSBBA-->API: Return encrypted bids + deactivate LSBBA + API->>API: Decrypt bids + API-->UI: Return array of decrypted bids + deactivate API + UI->>UI: Create transaction to decrypt bids with array + UI-->User: Display transaction for signing + User->>UI: Sign transaction to decrypt bids + UI->>LSBBA: Send transaction to blockchain + activate LSBBA + LSBBA->>LSBBA: Validate and store decrypted bids + alt all bids decrypted + LSBBA->>LSBBA: Set auction status to "decrypted" + end + LSBBA-->UI: Return transaction result + deactivate LSBBA + UI-->User: Display transaction result and update auction status +``` + +### Auction Settlement Part 2: Evaluate winners and send proceeds +The final step to settle an auction is to evaluate the sorted, decrypted bids to determine the marginal price for the auction and distribute funds to the winners. If the auction did not reach the minimum price or minimum fill capacity, then the auction is settled without winners, and users can claim refunds. If there are winners, then they are paid out the base tokens (or derivative) and the seller receives the proceeds. Users that do not win must claim a refund. The settlement function is open and can be called by anyone after the decryption process is complete. + +```mermaid +sequenceDiagram + autoNumber + participant User + participant UI + participant AuctionHouse + participant LSBBA + participant VestingModule + participant Seller + participant Buyers + + User->>UI: Navigate to Auction page + UI-->User: Display auction status + User->>UI: Click "Settle auction" button + UI-->User: Display transaction for signing + User->>UI: Sign transaction to settle auction + UI->>AuctionHouse: Send transaction to blockchain + activate AuctionHouse + AuctionHouse->>AuctionHouse: Validate auction status and settle auction + AuctionHouse->>LSBBA: Call settle function with auction ID + activate LSBBA + LSBBA->>LSBBA: Validate auction status and settle auction + alt auction did not reach minimum price or capacity + LSBBA->>LSBBA: Set auction status to "settled" + else auction reached minimum price and capacity + LSBBA->>LSBBA: Set auction status to "settled" + LSBBA->>LSBBA: Calculate marginal price + LSBBA->>LSBBA: Distribute proceeds to winners + LSBBA->>LSBBA: Distribute refunds to losers + end + LSBBA-->AuctionHouse: Hand execution back to AuctionHouse + deactivate LSBBA + AuctionHouse->>AuctionHouse: Collect fees + alt seller specified hook for proceeds + AuctionHouse->>Seller: Call hook with proceeds + Seller->>Seller: Execute hook logic + Seller-->AuctionHouse: Send base tokens and return execution to AuctionHouse + else + Seller-->AuctionHouse: Base tokens transferred from Seller to AuctionHouse + end + alt payout is a derivative of base token + AuctionHouse->>VestingModule: Mint vesting tokens to winners + VestingModule-->Buyers: Send vesting tokens to winners + else + AuctionHouse-->Buyers: Send payouts to winners + end + AuctionHouse->>AuctionHouse: Set auction status to "settled" + AuctionHouse-->UI: Return transaction result + deactivate AuctionHouse + UI-->User: Display transaction result and update auction status +``` + diff --git a/design/SEALED_VERSIONS.md b/design/SEALED_VERSIONS.md new file mode 100644 index 00000000..5f570eac --- /dev/null +++ b/design/SEALED_VERSIONS.md @@ -0,0 +1,47 @@ +# Sealed Bid Auction Development + +## Context + +Bond Protocol, rebranding to Axis, is changing directions from our current "atomic" auction products to focus on developing and deploying a Sealed Bid Batch Auction product. The primary expected use cases are token launches, issuing token derivatives, etc. We believe a decentralized sealed bid batch auction can allow for superior price discovery and outcomes for teams over current market solutions. + +## Design + +### Base Auction Flow +There are three main steps to any batch auction: +1. Bid submission - Users submit bids over an alloted time period +2. Evaluation - The submitted bids are evaluated using the auction logic and winners are determined +3. Settlement - The proceeds of the auction are distributed to the winners and the auction is closed + +It's important to note that any of these three steps can be performed on-chain or off-chain. + +### Auction Fairness +An auction system must assure users of inclusion and fair treatment of their bids. Specifically, it needs to provide guarantees around: +1. Completeness - All bids that are submitted are evaluated +2. Accuracy - The evaluation of bids is performed correctly + +Depending on the design of the system, these properties may be inherent or difficult to achieve. There is typically a tradeoff between UX/efficiency and these guarantees. + +### Types of Batch Auctions +There are four main types of batch auctions that could be built based on where the bids are submitted and where the bid evaluation logic is performed. We will refer to these as "local", for on-chain, and "external", for off-chain, to avoid using similar hyphenated words. Settlement is assumed to be on-chain. +1. Local (aka on-chain) bid submission and evaluation +Fully on-chain auction, similar to Gnosis auction, but with encryption. Since you cannot pre-sort the bids, it requires an intermediate step to decrypt and sort all submitted bids before settlement to avoid issues with the gas limit. The risk of DoS attacks is decreased by requiring bid deposits, but this leaks some information. This solution natively guarantees both "completeness" and "accuracy" of the bids. + +2. External bid submission and local evaluation + +Bids are collected off-chain, filtered for bids that aren't valid (e.g. below min price or too small), sorted, and submitted on-chain, where the auction price logic is performed to determine the winners. This version natively adheres to the "accuracy" guarantee through on-chain evaluation of bids, but requires trusting the "completeness" property of the bids that are submitted for evaluation. This is difficult to do in a decentralized way. + +3. Local submission and external evaluation + +Bids are submitted on-chain (encrypted). Once the auction ends, an external party can decrypt the bid data, perform bid evaluation off-chain, and submit the winning bids with a validity proof of the evaluation. This version natively adheres to the "completness" property, but requires trusting the "accuracy" of the submitted evaluation. A validity proof can provide this in a decentralized way. This may be a workable solution if a ZK proof could be constructed correctly to verify the off-chain evaluation. Currently, we are running into issues with the size of the required circuit(s). More information on this below. + +4. External bid submission and evaluation +Bids are collected off-chain and the settlement algorithm is performed to determine the winning bids. the winning bids are submitted on-chain for settlement of payments. This would have the best UX and is most gas efficient, but is the least decentralized. It requires trusting both the "completeness" and "accuracy" of the submitted evaluation. + +There is also a fifth option where Settlement is also performed externally and proved locally, which equates to a validium, but that isn't considered in detail here. + +There are many potential product designs that we have considered. Each has tradeoffs along a few major dimensions: +- Decentralization +- User Experience +- Level of privacy +- Efficiency (i.e. gas costs) + From ffa9e0dd19d9cdb26b59cd6d2bbe09fdf67e3524 Mon Sep 17 00:00:00 2001 From: Oighty Date: Mon, 22 Jan 2024 16:32:37 -0600 Subject: [PATCH 80/82] docs: more design updates for LSBBA --- design/LSBBA.md | 55 ++++++++++++++++++++++++++++++++++++--- design/SEALED_VERSIONS.md | 2 +- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/design/LSBBA.md b/design/LSBBA.md index bbc8bf8d..f74652f9 100644 --- a/design/LSBBA.md +++ b/design/LSBBA.md @@ -20,7 +20,6 @@ LSSBA is a fully on-chain sealed bid batch auction system built on the Axis Prot - Settlement is permissionless - System is on-chain and transparent - ## Components and Design Decisions This version of a sealed bid auction system has a few key properties that make it attractive: 1. Maximally Permissionless. The entire auction process happens on-chain and can be executed without relying on our off-chain infrastructure. @@ -32,6 +31,7 @@ However, the user experience is not as good as an off-chain or hybrid system cou ### Smart Contracts #### Axis Protocol +Axis enables arbitrary auction and derivative combinations in a single settlement contract (the AuctionHouse). For this particular solution, there are only 3 contracts required. - Auction House - LSBBA (Auction Module) - Vesting Module @@ -46,8 +46,27 @@ What: Need to be able to encrypt bids and other data with a key that no interest Solution: API and private database that creates RSA keypairs and store the private key until an auction ends. If an auction is cancelled, the private key is never released. -### UI -SPA hosted on [IPFS](https://docs.ipfs.tech/how-to/websites-on-ipfs/single-page-website/) with [Eth.limo](https://eth.limo/) domain resolution to ENS owned by the protocol +Axis will provide a simple API that provides key management for RSA keypairs generated on auction creation. The API will generate a new keypair and store the private key in a database. The public key is returned to the user and stored on the auction contract. Once the auction ends, the private key is released from the database and can be used to decrypt the bids. The API will also provide a convenience function for returning the next bids that need to be decrypted for a given auction. This will allow anyone to decrypt the bids and submit them to the contract for verification. + +The API will be written in Rust and the database will be a MongoDB instance following the architecture of the Bond Protocol Limit Orders system. This can be easily hosted on Railway and provides good CI/CD support. + +In addition to the API, there will be a Rust service that watches for new auction creation events, checks whether the API provided the key, and stores the auction ID and conclusion timestamp in the database. This will allow the API to release the private key at the correct time. We need this service since we cannot be sure of the auction ID when a key is generated (it may be front-run by another transaction, for example). + +### UI / dApp +We will provide a user interface (aka dApp) for both Sellers and Buyers to interact with the product. The key user actions are defined below in the Actions section. The core pages we be: +- List of auctions (TBD on design and filtering between statuses) +- Create auction page - for sellers to create new auctions +- Auction page - Details the status and available actions for a given auction. The auction page will need to support these differents states: + - Created - Auction has been created, but not started + - Live - Auction is created and currently accepting bids. Buyers should be able to bid and cancel bids they have made. + - Concluded - Auction has ended and bids are being decrypted. Anyone should be able to decrypt the bids and submit them to the contract for verification. + - Decrypted - Bids are decrypted and awaiting settlement. Anyone should be able to settle the auction. + - Settled - Auction payouts have been issued. Buyers that did not win can claim refunds. + +The architecture will be a Single Page App (SPA) hosted on [IPFS](https://docs.ipfs.tech/how-to/websites-on-ipfs/single-page-website/) with [Eth.limo](https://eth.limo/) domain resolution to ENS owned by the protocol. + +#### Subgraph +In order to display user bids on the dapp, it is most convenient to use a subgraph to index the bids from events emitted from the smart contracts. Therefore, we will need to create a subgraph for the AuctionHouse (and possibly other modules depending on where the events reside). ## Actions @@ -141,7 +160,35 @@ sequenceDiagram ``` ### Buyer Cancels Bid -TODO +Buyers are able to cancel bids they make prior to the auction concluding and receive their deposit back. The bid must be deleted from the stored bids so as to not require it to be decrypted. + +```mermaid +sequenceDiagram + autoNumber + participant Buyer + participant UI + participant AuctionHouse + participant LSBBA + participant Subgraph + + Buyer->>UI: Navigate to Auction page + UI->>AuctionHouse: Fetch data for auction ID (base token, quote token, public key, start, conclusion, auction type, derivative type + info, capacity, min bid size) + UI->>Subgraph: Fetch bids for user on auction ID + UI-->Buyer: Display bids for user + Buyer->>UI: Click "Cancel bid" button + UI->>AuctionHouse: Send `cancelBid(lotId, bidId)` transaction to blockchain. + activate AuctionHouse + Note over AuctionHouse: Not exactly sure the separation of duties between module and AuctionHouse yet + AuctionHouse->>AuctionHouse: Validate and delete bid, if it exists + AuctionHouse->>LSBBA: Call cancelBid function with auction ID and bid ID + activate LSBBA + LSBBA->>LSBBA: Validate and delete bid (ensuring that it doesn't mess up settlement) + LSBBA-->AuctionHouse: Hand execution back to AuctionHouse + deactivate LSBBA + AuctionHouse-->UI: Return transaction result + deactivate AuctionHouse + UI-->Buyer: Display transaction result and update bids +``` ### Seller Cancels Auction TODO diff --git a/design/SEALED_VERSIONS.md b/design/SEALED_VERSIONS.md index 5f570eac..26be20b2 100644 --- a/design/SEALED_VERSIONS.md +++ b/design/SEALED_VERSIONS.md @@ -2,7 +2,7 @@ ## Context -Bond Protocol, rebranding to Axis, is changing directions from our current "atomic" auction products to focus on developing and deploying a Sealed Bid Batch Auction product. The primary expected use cases are token launches, issuing token derivatives, etc. We believe a decentralized sealed bid batch auction can allow for superior price discovery and outcomes for teams over current market solutions. +Bond Protocol, rebranding to Axis, is changing directions from our current "atomic" auction products to focus on developing and deploying a Sealed Bid Batch Auction product. The primary expected use cases are token sales and issuing token derivatives. We believe a decentralized sealed bid batch auction can allow for superior price discovery and outcomes for teams over current market solutions. ## Design From 923895e72ff4403ec0b6f1a9de4efee788d4d78e Mon Sep 17 00:00:00 2001 From: Oighty Date: Mon, 22 Jan 2024 17:11:56 -0600 Subject: [PATCH 81/82] docs: add'l design --- design/LSBBA.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/design/LSBBA.md b/design/LSBBA.md index f74652f9..7b389b55 100644 --- a/design/LSBBA.md +++ b/design/LSBBA.md @@ -1,6 +1,6 @@ # LSBBA: Local Sealed Bid Batch Auctions (name needs work) -LSSBA is a fully on-chain sealed bid batch auction system built on the Axis Protocol that uses RSA encryption and a multi-step settlement process to avoid issues of previous sealed bid auction designs. The purpose of the system is to allow any seller to create sealed bid batch auctions for any ERC20 token pair. Sellers create an auction for a set amount of base tokens (capacity) and provide an RSA public key for buyers to encrypt their bids with. Buyers encrypt a portion of their bids (the amount out) off-chain using the RSA public key and submit them to the contract. The amount of quote tokens is public and the tokens they're bidding are sent to the contract on the bid. Once an auction ends, the settlement happens in two steps. First, the bids are decrypted off-chain, submitted to the contract, and verified by the contract. This can be done by anyone who has the RSA private key to decrypt the bids off-chain. If the Seller uses the Axis dapp to deploy a market, the RSA keypair is generated by an API and the private key is released after the auction concludes so that anyone can do the decryption. Once all the bids are decrypted, anyone can settle the auction and pay out proceeds to the winners. Those who do not win the auction can claim a refund following settlement. +LSBBA is a fully on-chain sealed bid batch auction system built on the Axis Protocol that uses RSA encryption and a multi-step settlement process to avoid issues of previous sealed bid auction designs. The purpose of the system is to allow any seller to create sealed bid batch auctions for any ERC20 token pair. Sellers create an auction for a set amount of base tokens (capacity) and provide an RSA public key for buyers to encrypt their bids with. Buyers encrypt a portion of their bids (the amount out) off-chain using the RSA public key and submit them to the contract. The amount of quote tokens is public and the tokens they're bidding are sent to the contract on the bid. Once an auction ends, the settlement happens in two steps. First, the bids are decrypted off-chain, submitted to the contract, and verified by the contract. This can be done by anyone who has the RSA private key to decrypt the bids off-chain. If the Seller uses the Axis dapp to deploy a market, the RSA keypair is generated by an API and the private key is released after the auction concludes so that anyone can do the decryption. Once all the bids are decrypted, anyone can settle the auction and pay out proceeds to the winners. Those who do not win the auction can claim a refund following settlement. ## User Features @@ -71,6 +71,8 @@ In order to display user bids on the dapp, it is most convenient to use a subgra ## Actions ### Seller Creates Auction +A seller creating an auction is the first step in the lifecycle. They provide common auction parameters as well as auction specific parameters to the AuctionHouse contract to kick it off. They must approve the AuctionHouse for the token they are selling (base token) or provide a Hooks contract that will settle the auction + ```mermaid sequenceDiagram autoNumber @@ -191,7 +193,37 @@ sequenceDiagram ``` ### Seller Cancels Auction -TODO +Sellers are able to specify a start time for their auction in the future. If they want to cancel the auction before it starts, they can do so and the auction will be deleted. If the auction has already started and is accepting bids, then the seller can no longer cancel it. + +```mermaid +sequenceDiagram + autoNumber + participant Seller + participant UI + participant AuctionHouse + participant LSBBA + participant Watcher + + Seller->>UI: Navigate to Auction page + UI->>AuctionHouse: Fetch data for auction ID (base token, quote token, public key, start, conclusion, auction type, derivative type + info, capacity, min bid size) + UI-->Seller: Display auction data + Seller->>UI: Click "Cancel auction" button + UI->>AuctionHouse: Send `cancelAuction(lotId)` transaction to blockchain. + activate AuctionHouse + AuctionHouse->>AuctionHouse: Validate and delete auction + AuctionHouse->>LSBBA: Call cancelAuction function with auction ID + activate LSBBA + LSBBA->>LSBBA: Validate and delete auction + LSBBA-->AuctionHouse: Hand execution back to AuctionHouse + LSBBA-->Watcher: Emit event for auction cancellation + Watcher->>Watcher: Get auction ID from event + Watcher->>Database: Delete auction ID and conclusion timestamp for public key + deactivate LSBBA + AuctionHouse-->UI: Return transaction result + deactivate AuctionHouse + UI-->Seller: Display transaction result and update auction status +``` + ### Auction Settlement Part 1: Decryption After an auction has concluded, the bids must be decrypted and sorted by price. We do this by releasing the RSA private key from the database via the API to anyone on the dapp looking at the auction page. Decryption works by providing an array of (amountOut, seed) for the bids in the order they were submitted to the contract. The seed was randomly generated on submission and can be extracted by decrypting the bid. The API provides a convenience function for returning an array for the next bids that need to be decrypted from an auction (using its ID). It may take several decryption transactions depending on the number of bids and gas limit. Once, all bids are decrypted on the contract, we can move to part 2 of the settlement process. 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 82/82] 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) {