diff --git a/deployments/.base-sepolia-1742317640.json b/deployments/.base-sepolia-1742317640.json new file mode 100644 index 00000000..8f741c38 --- /dev/null +++ b/deployments/.base-sepolia-1742317640.json @@ -0,0 +1,5 @@ +{ + "deployments.callbacks.BatchCappedMerkleAllowlist": "0x98e489De04f8Ea4A93e5AD6862D8e8847781158d", + "deployments.callbacks.BatchMerkleAllowlist": "0x985942dEC200DD6dBFf2D60d3425299fbB0a07fE", + "deployments.callbacks.BatchAllocatedMerkleAllowlist": "0x985a5C530dd14F469b9032d5ca1B8B7CB6878109" +} diff --git a/script/deploy/Deploy.s.sol b/script/deploy/Deploy.s.sol index fad95d83..e32e14e3 100644 --- a/script/deploy/Deploy.s.sol +++ b/script/deploy/Deploy.s.sol @@ -439,6 +439,8 @@ contract Deploy is Script, WithDeploySequence, WithSalts { address atomicAuctionHouse = _getAddressNotZero("deployments.AtomicAuctionHouse"); string memory deploymentKey = _getDeploymentKey(sequenceName_); console2.log(" deploymentKey:", deploymentKey); + + // 10011000 = 0x98 Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -451,10 +453,11 @@ contract Deploy is Script, WithDeploySequence, WithSalts { }); // Get the salt - bytes32 salt_ = _getSalt( + bytes32 salt_ = _generateSalt( deploymentKey, type(CappedMerkleAllowlist).creationCode, - abi.encode(atomicAuctionHouse, permissions) + abi.encode(atomicAuctionHouse, permissions), + "98" ); // Revert if the salt is not set @@ -482,6 +485,8 @@ contract Deploy is Script, WithDeploySequence, WithSalts { address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); string memory deploymentKey = _getDeploymentKey(sequenceName_); console2.log(" deploymentKey:", deploymentKey); + + // 10011000 = 0x98 Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -494,10 +499,11 @@ contract Deploy is Script, WithDeploySequence, WithSalts { }); // Get the salt - bytes32 salt_ = _getSalt( + bytes32 salt_ = _generateSalt( deploymentKey, type(CappedMerkleAllowlist).creationCode, - abi.encode(batchAuctionHouse, permissions) + abi.encode(batchAuctionHouse, permissions), + "98" ); // Revert if the salt is not set @@ -525,6 +531,8 @@ contract Deploy is Script, WithDeploySequence, WithSalts { address atomicAuctionHouse = _getAddressNotZero("deployments.AtomicAuctionHouse"); string memory deploymentKey = _getDeploymentKey(sequenceName_); console2.log(" deploymentKey:", deploymentKey); + + // 10011000 = 0x98 Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -537,10 +545,11 @@ contract Deploy is Script, WithDeploySequence, WithSalts { }); // Get the salt - bytes32 salt_ = _getSalt( + bytes32 salt_ = _generateSalt( deploymentKey, type(MerkleAllowlist).creationCode, - abi.encode(atomicAuctionHouse, permissions) + abi.encode(atomicAuctionHouse, permissions), + "98" ); // Revert if the salt is not set @@ -568,6 +577,8 @@ contract Deploy is Script, WithDeploySequence, WithSalts { address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); string memory deploymentKey = _getDeploymentKey(sequenceName_); console2.log(" deploymentKey:", deploymentKey); + + // 10011000 = 0x98 Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -580,10 +591,11 @@ contract Deploy is Script, WithDeploySequence, WithSalts { }); // Get the salt - bytes32 salt_ = _getSalt( + bytes32 salt_ = _generateSalt( deploymentKey, type(MerkleAllowlist).creationCode, - abi.encode(batchAuctionHouse, permissions) + abi.encode(batchAuctionHouse, permissions), + "98" ); // Revert if the salt is not set @@ -611,6 +623,8 @@ contract Deploy is Script, WithDeploySequence, WithSalts { address atomicAuctionHouse = _getAddressNotZero("deployments.AtomicAuctionHouse"); string memory deploymentKey = _getDeploymentKey(sequenceName_); console2.log(" deploymentKey:", deploymentKey); + + // 10011000 = 0x98 Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -623,10 +637,11 @@ contract Deploy is Script, WithDeploySequence, WithSalts { }); // Get the salt - bytes32 salt_ = _getSalt( + bytes32 salt_ = _generateSalt( deploymentKey, type(TokenAllowlist).creationCode, - abi.encode(atomicAuctionHouse, permissions) + abi.encode(atomicAuctionHouse, permissions), + "98" ); // Revert if the salt is not set @@ -654,6 +669,8 @@ contract Deploy is Script, WithDeploySequence, WithSalts { address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); string memory deploymentKey = _getDeploymentKey(sequenceName_); console2.log(" deploymentKey:", deploymentKey); + + // 10011000 = 0x98 Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -666,10 +683,11 @@ contract Deploy is Script, WithDeploySequence, WithSalts { }); // Get the salt - bytes32 salt_ = _getSalt( + bytes32 salt_ = _generateSalt( deploymentKey, type(TokenAllowlist).creationCode, - abi.encode(batchAuctionHouse, permissions) + abi.encode(batchAuctionHouse, permissions), + "98" ); // Revert if the salt is not set @@ -697,6 +715,8 @@ contract Deploy is Script, WithDeploySequence, WithSalts { address atomicAuctionHouse = _getAddressNotZero("deployments.AtomicAuctionHouse"); string memory deploymentKey = _getDeploymentKey(sequenceName_); console2.log(" deploymentKey:", deploymentKey); + + // 10011000 = 0x98 Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -709,10 +729,11 @@ contract Deploy is Script, WithDeploySequence, WithSalts { }); // Get the salt - bytes32 salt_ = _getSalt( + bytes32 salt_ = _generateSalt( deploymentKey, type(AllocatedMerkleAllowlist).creationCode, - abi.encode(atomicAuctionHouse, permissions) + abi.encode(atomicAuctionHouse, permissions), + "98" ); // Revert if the salt is not set @@ -740,6 +761,8 @@ contract Deploy is Script, WithDeploySequence, WithSalts { address batchAuctionHouse = _getAddressNotZero("deployments.BatchAuctionHouse"); string memory deploymentKey = _getDeploymentKey(sequenceName_); console2.log(" deploymentKey:", deploymentKey); + + // 10011000 = 0x98 Callbacks.Permissions memory permissions = Callbacks.Permissions({ onCreate: true, onCancel: false, @@ -752,10 +775,11 @@ contract Deploy is Script, WithDeploySequence, WithSalts { }); // Get the salt - bytes32 salt_ = _getSalt( + bytes32 salt_ = _generateSalt( deploymentKey, type(AllocatedMerkleAllowlist).creationCode, - abi.encode(batchAuctionHouse, permissions) + abi.encode(batchAuctionHouse, permissions), + "98" ); // Revert if the salt is not set diff --git a/script/env.json b/script/env.json index 02381313..53f812bf 100644 --- a/script/env.json +++ b/script/env.json @@ -87,9 +87,9 @@ }, "deployments": { "callbacks": { - "BatchAllocatedMerkleAllowlist": "0x98C8ffFf24bcfC3A5B0b463c43F10932Cedb7B8F", - "BatchCappedMerkleAllowlist": "0x9859AcCA8a9afEbb9b3986036d4E0efc0246cEeA", - "BatchMerkleAllowlist": "0x98d64E00D9d6550913E73C940Ff476Cf1723d834", + "BatchAllocatedMerkleAllowlist": "0x985a5C530dd14F469b9032d5ca1B8B7CB6878109", + "BatchCappedMerkleAllowlist": "0x98e489De04f8Ea4A93e5AD6862D8e8847781158d", + "BatchMerkleAllowlist": "0x985942dEC200DD6dBFf2D60d3425299fbB0a07fE", "BatchTokenAllowlist": "0x9801e45362a2bb7C9F22486CC3F5cA9224e9CC55", "BatchUniswapV2DirectToLiquidity": "0xE6546c03B1b9DFC4238f0A2923FdefD5E4af7659", "BatchUniswapV3DirectToLiquidity": "0xE68b21C071534781BC4c40E6BF1bCFC23638fF4B" diff --git a/src/callbacks/allowlists/AllocatedMerkleAllowlist.sol b/src/callbacks/allowlists/AllocatedMerkleAllowlist.sol index 9ffbae66..ba838e63 100644 --- a/src/callbacks/allowlists/AllocatedMerkleAllowlist.sol +++ b/src/callbacks/allowlists/AllocatedMerkleAllowlist.sol @@ -83,6 +83,14 @@ contract AllocatedMerkleAllowlist is MerkleAllowlist { ) internal { // Validate that the buyer is allowed to participate + // If the merkle root is zero, anyone can participate + // Given anyone can spin up a new wallet, it also doesn't make sense to have a buyer limit + if (lotMerkleRoot[lotId_] == bytes32(0)) { + // Update the buyer's spent amount + lotBuyerSpent[lotId_][buyer_] += amount_; + return; + } + // Decode the merkle proof and allocated amount from buyer submitted callback data (bytes32[] memory proof, uint256 allocatedAmount) = abi.decode(callbackData_, (bytes32[], uint256)); diff --git a/src/callbacks/allowlists/CappedMerkleAllowlist.sol b/src/callbacks/allowlists/CappedMerkleAllowlist.sol index 321600a7..545e769b 100644 --- a/src/callbacks/allowlists/CappedMerkleAllowlist.sol +++ b/src/callbacks/allowlists/CappedMerkleAllowlist.sol @@ -48,7 +48,7 @@ contract CappedMerkleAllowlist is MerkleAllowlist { /// @param callbackData_ abi-encoded data: (bytes32, uint256) representing the merkle root and buyer limit function _onCreate( uint96 lotId_, - address, + address seller_, address, address, uint256, @@ -67,6 +67,10 @@ contract CappedMerkleAllowlist is MerkleAllowlist { lotMerkleRoot[lotId_] = merkleRoot; lotBuyerLimit[lotId_] = buyerLimit; emit MerkleRootSet(lotId_, merkleRoot); + + // Set the lot admin to the seller address + lotAdmin[lotId_] = seller_; + emit LotAdminSet(lotId_, seller_); } /// @inheritdoc MerkleAllowlist @@ -95,6 +99,14 @@ contract CappedMerkleAllowlist is MerkleAllowlist { // ========== INTERNAL FUNCTIONS ========== // function _canBuy(uint96 lotId_, address buyer_, uint256 amount_) internal { + // If the merkle root is zero, anyone can participate + if (lotMerkleRoot[lotId_] == bytes32(0)) { + // Update the buyer spent amount + // Given anyone can spin up a new wallet, it also doesn't make sense to have a buyer limit + lotBuyerSpent[lotId_][buyer_] += amount_; + return; + } + // Check if the buyer has already spent their limit if (lotBuyerSpent[lotId_][buyer_] + amount_ > lotBuyerLimit[lotId_]) { revert Callback_ExceedsLimit(); diff --git a/src/callbacks/allowlists/MerkleAllowlist.sol b/src/callbacks/allowlists/MerkleAllowlist.sol index aeb43dfc..395679ba 100644 --- a/src/callbacks/allowlists/MerkleAllowlist.sol +++ b/src/callbacks/allowlists/MerkleAllowlist.sol @@ -6,17 +6,12 @@ import {MerkleProof} from "@openzeppelin-contracts-4.9.2/utils/cryptography/Merk import {BaseCallback} from "@axis-core-1.0.4/bases/BaseCallback.sol"; import {Callbacks} from "@axis-core-1.0.4/lib/Callbacks.sol"; -import {IAuctionHouse} from "@axis-core-1.0.4/interfaces/IAuctionHouse.sol"; +import {IMerkleAllowlist} from "./interfaces/IMerkleAllowlist.sol"; /// @title MerkleAllowlist /// @notice This contract implements a merkle tree-based allowlist for buyers to participate in an auction. /// In this implementation, buyers do not have a limit on the amount they can purchase/bid. -contract MerkleAllowlist is BaseCallback { - // ========== EVENTS ========== // - - /// @notice Emitted when the merkle root is set - event MerkleRootSet(uint96 lotId, bytes32 merkleRoot); - +contract MerkleAllowlist is BaseCallback, IMerkleAllowlist { // ========== STATE VARIABLES ========== // /// @notice The root of the merkle tree that represents the allowlist for a lot @@ -24,6 +19,10 @@ contract MerkleAllowlist is BaseCallback { /// In particular, leaf values (such as `(address)` or `(address,uint256)`) should be double-hashed. mapping(uint96 lotId => bytes32 merkleRoot) public lotMerkleRoot; + /// @notice The admin for the given lot id + /// @dev The admin is permitted to set the merkle root + mapping(uint96 lotId => address admin) public lotAdmin; + // ========== CONSTRUCTOR ========== // // PERMISSIONS @@ -54,10 +53,11 @@ contract MerkleAllowlist is BaseCallback { /// - The callback data is of an invalid length /// /// @param lotId_ The id of the lot + /// @param seller_ The address of the seller /// @param callbackData_ abi-encoded data: (bytes32) representing the merkle root function _onCreate( uint96 lotId_, - address, + address seller_, address, address, uint256, @@ -75,6 +75,10 @@ contract MerkleAllowlist is BaseCallback { // Set the merkle root lotMerkleRoot[lotId_] = merkleRoot; emit MerkleRootSet(lotId_, merkleRoot); + + // Set the lot admin to the seller address + lotAdmin[lotId_] = seller_; + emit LotAdminSet(lotId_, seller_); } /// @inheritdoc BaseCallback @@ -180,6 +184,11 @@ contract MerkleAllowlist is BaseCallback { address buyer_, bytes calldata callbackData_ ) internal view virtual { + // If the merkle root is zero, anyone can participate + if (lotMerkleRoot[lotId_] == bytes32(0)) { + return; + } + // Decode the merkle proof from the callback data bytes32[] memory proof = abi.decode(callbackData_, (bytes32[])); @@ -206,18 +215,37 @@ contract MerkleAllowlist is BaseCallback { /// - The auction has not been registered /// /// @param merkleRoot_ The new merkle root - function setMerkleRoot(uint96 lotId_, bytes32 merkleRoot_) external onlyRegisteredLot(lotId_) { - // We check that the lot is registered on this callback - - // Check that the caller is the lot's seller - (address seller,,,,,,,,) = IAuctionHouse(AUCTION_HOUSE).lotRouting(lotId_); - if (msg.sender != seller) { + function setMerkleRoot( + uint96 lotId_, + bytes32 merkleRoot_ + ) external override onlyRegisteredLot(lotId_) { + // Check that the caller is the lot's admin + if (lotAdmin[lotId_] != msg.sender) { revert Callback_NotAuthorized(); } // Set the new merkle root and emit an event lotMerkleRoot[lotId_] = merkleRoot_; - emit MerkleRootSet(lotId_, merkleRoot_); } + + /// @inheritdoc IMerkleAllowlist + function setLotAdmin( + uint96 lotId_, + address admin_ + ) external override onlyRegisteredLot(lotId_) { + // Validate that the caller is the lot's current admin + if (lotAdmin[lotId_] != msg.sender) { + revert Callback_NotAuthorized(); + } + + // Validate that the address is not the zero address + if (admin_ == address(0)) { + revert Callback_InvalidParams(); + } + + // Set the new admin + lotAdmin[lotId_] = admin_; + emit LotAdminSet(lotId_, admin_); + } } diff --git a/src/callbacks/allowlists/interfaces/IMerkleAllowlist.sol b/src/callbacks/allowlists/interfaces/IMerkleAllowlist.sol new file mode 100644 index 00000000..00b3197e --- /dev/null +++ b/src/callbacks/allowlists/interfaces/IMerkleAllowlist.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +/// @title IMerkleAllowlist +/// @notice Defines the interface for the MerkleAllowlist contract, which provides a merkle tree-based allowlist for buyers to participate in an auction. +interface IMerkleAllowlist { + // ========== EVENTS ========== // + + /// @notice Emitted when the merkle root is set + event MerkleRootSet(uint96 lotId, bytes32 merkleRoot); + + /// @notice Emitted when the lot admin is set + event LotAdminSet(uint96 lotId, address admin); + + // ========== FUNCTIONS ========== // + + /// @notice Gets the merkle root for the allowlist + /// + /// @param lotId_ The ID of the lot + /// @return merkleRoot The merkle root for the allowlist + function lotMerkleRoot( + uint96 lotId_ + ) external view returns (bytes32 merkleRoot); + + /// @notice Sets the merkle root for the allowlist + /// This function can be called by the lot's seller to update the merkle root after `onCreate()`. + /// Setting the merkle root to zero indicates that the allowlist is disabled and anyone can participate. + /// @dev This function performs the following: + /// - Performs validation + /// - Sets the merkle root + /// - Emits a MerkleRootSet event + /// + /// @param lotId_ The ID of the lot + /// @param merkleRoot_ The new merkle root + function setMerkleRoot(uint96 lotId_, bytes32 merkleRoot_) external; + + /// @notice Gets the admin for the given lot id + /// The admin is permitted to set the merkle root + /// + /// @param lotId_ The ID of the lot + /// @return admin The lot admin + function lotAdmin( + uint96 lotId_ + ) external view returns (address admin); + + /// @notice Sets the lot admin + /// @dev This function performs the following: + /// - Performs validation + /// - Sets the new admin + /// - Emits a LotAdminSet event + /// + /// @param lotId_ The ID of the lot + /// @param admin_ The new admin + function setLotAdmin(uint96 lotId_, address admin_) external; +} diff --git a/test/callbacks/AllocatedMerkleAllowlistAtomic.t.sol b/test/callbacks/AllocatedMerkleAllowlistAtomic.t.sol index 08b0dcd9..8e88272b 100644 --- a/test/callbacks/AllocatedMerkleAllowlistAtomic.t.sol +++ b/test/callbacks/AllocatedMerkleAllowlistAtomic.t.sol @@ -111,6 +111,14 @@ contract AllocatedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, Tes _; } + modifier givenAtomicOnCreateMerkleRootZero() { + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, _SELLER, _BASE_TOKEN, _QUOTE_TOKEN, _LOT_CAPACITY, false, abi.encode(bytes32(0)) + ); + _; + } + function _onPurchase( uint96 lotId_, address buyer_, @@ -128,11 +136,14 @@ contract AllocatedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, Tes // [X] it reverts // [X] if the caller is not the auction house // [X] it reverts + // [X] if the merkle root is zero + // [X] it sets the merkle root to zero // [X] if the seller is not the seller for the allowlist // [X] it sets the merkle root and buyer limit // [X] if the lot is already registered // [X] it reverts // [X] it sets the merkle root and buyer limit + // [X] it sets the lot admin to the seller function test_onCreate_allowlistParametersIncorrectFormat_reverts() public { // Expect revert @@ -200,9 +211,22 @@ contract AllocatedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, Tes ); } + function test_onCreate_merkleRootZero() public { + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, _SELLER, _BASE_TOKEN, _QUOTE_TOKEN, _LOT_CAPACITY, false, abi.encode(bytes32(0)) + ); + + // Assert + assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); + assertEq(_allowlist.lotMerkleRoot(_lotId), bytes32(0), "lotMerkleRoot"); + } + function test_onCreate() public givenAtomicOnCreate { assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + assertEq(_allowlist.lotAdmin(_lotId), _SELLER, "lotAdmin"); } // onPurchase @@ -212,6 +236,8 @@ contract AllocatedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, Tes // [X] it reverts // [X] if the lot is not registered // [X] it reverts + // [X] if the merkle root is zero + // [X] it succeeds for any buyer and any amount // [X] if the buyer is not in the merkle tree // [X] it reverts // [X] if the amount is greater than the buyer limit @@ -251,6 +277,21 @@ contract AllocatedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, Tes _onPurchase(_lotId, _BUYER, 1e18, _BUYER_ALLOCATED_AMOUNT); } + function test_onPurchase_merkleRootZero( + address buyer_, + uint256 amount_ + ) public givenAtomicOnCreateMerkleRootZero { + vm.assume(buyer_ != _BUYER && buyer_ != _BUYER_TWO); + uint256 amount = bound(amount_, 1, _LOT_CAPACITY); + + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onPurchase(_lotId, buyer_, amount, 0, false, ""); + + // Assert + assertEq(_allowlist.lotBuyerSpent(_lotId, buyer_), amount, "lotBuyerSpent"); + } + function test_onPurchase_buyerNotInMerkleTree_reverts() public givenAtomicOnCreate { // Expect revert bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); @@ -306,14 +347,57 @@ contract AllocatedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, Tes assertEq(_allowlist.lotBuyerSpent(_lotId, _BUYER), amount, "lotBuyerSpent"); } + // setLotAdmin + // [X] when the caller is not the lot admin + // [X] it reverts + // [X] given the lot is not registered + // [X] it reverts + // [X] when the new admin is the zero address + // [X] it reverts + // [X] it sets the lot admin + + function test_setLotAdmin_callerNotAdmin() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_newAdminZeroAddress_reverts() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, address(0)); + } + + function test_setLotAdmin() public givenAtomicOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + assertEq(_allowlist.lotAdmin(_lotId), _SELLER_TWO, "lotAdmin"); + } + // setMerkleRoot - // [X] when the caller is not the lot seller + // [X] when the caller is not the lot admin // [X] it reverts - // [X] when the lot is not registered + // [X] given the lot is not registered // [X] it reverts + // [X] given the lot admin has been changed + // [X] the merkle root is updated // [X] the merkle root is updated - function test_setMerkleRoot_callerNotSeller() public givenAtomicOnCreate { + function test_setMerkleRoot_callerNotAdmin() public givenAtomicOnCreate { // Expect revert bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); vm.expectRevert(err); @@ -330,6 +414,16 @@ contract AllocatedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, Tes _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); } + function test_setMerkleRoot_lotAdminChanged() public givenAtomicOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + vm.prank(_SELLER_TWO); + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + } + function test_setMerkleRoot() public givenAtomicOnCreate { bytes32 newMerkleRoot = 0x0fdc3942d9af344db31ff2e80c06bc4e558dc967ca5b4d421d741870f5ea40df; diff --git a/test/callbacks/AllocatedMerkleAllowlistBatch.t.sol b/test/callbacks/AllocatedMerkleAllowlistBatch.t.sol index 13efa959..72dc60dd 100644 --- a/test/callbacks/AllocatedMerkleAllowlistBatch.t.sol +++ b/test/callbacks/AllocatedMerkleAllowlistBatch.t.sol @@ -111,6 +111,14 @@ contract AllocatedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, Test _; } + modifier givenBatchOnCreateMerkleRootZero() { + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, _SELLER, _BASE_TOKEN, _QUOTE_TOKEN, _LOT_CAPACITY, false, abi.encode(bytes32(0)) + ); + _; + } + function _onBid( uint96 lotId_, address buyer_, @@ -126,11 +134,14 @@ contract AllocatedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, Test // [X] it reverts // [X] if the caller is not the auction house // [X] it reverts + // [X] if the merkle root is zero + // [X] it sets the merkle root to zero // [X] if the seller is not the seller for the allowlist // [X] it sets the merkle root // [X] if the lot is already registered // [X] it reverts // [X] it sets the merkle root + // [X] it sets the lot admin to the seller function test_onCreate_allowlistParametersIncorrectFormat_reverts() public { // Expect revert @@ -198,9 +209,22 @@ contract AllocatedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, Test ); } + function test_onCreate_merkleRootZero() public { + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, _SELLER, _BASE_TOKEN, _QUOTE_TOKEN, _LOT_CAPACITY, false, abi.encode(bytes32(0)) + ); + + // Assert + assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); + assertEq(_allowlist.lotMerkleRoot(_lotId), bytes32(0), "lotMerkleRoot"); + } + function test_onCreate() public givenBatchOnCreate { assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + assertEq(_allowlist.lotAdmin(_lotId), _SELLER, "lotAdmin"); } // onBid @@ -210,6 +234,8 @@ contract AllocatedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, Test // [X] it reverts // [X] if the lot is not registered // [X] it reverts + // [X] if the merkle root is zero + // [X] it succeeds for any buyer and any amount // [X] if the buyer is not in the merkle tree // [X] it reverts // [X] if the amount is greater than the buyer limit @@ -244,6 +270,21 @@ contract AllocatedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, Test _onBid(_lotId, _BUYER, 1e18, _BUYER_ALLOCATED_AMOUNT); } + function test_onBid_merkleRootZero( + address buyer_, + uint256 amount_ + ) public givenBatchOnCreateMerkleRootZero { + vm.assume(buyer_ != _BUYER && buyer_ != _BUYER_TWO); + uint256 amount = bound(amount_, 1, _LOT_CAPACITY); + + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onBid(_lotId, 1, buyer_, amount, ""); + + // Assert + assertEq(_allowlist.lotBuyerSpent(_lotId, buyer_), amount, "lotBuyerSpent"); + } + function test_onBid_buyerNotInMerkleTree_reverts() public givenBatchOnCreate { // Expect revert bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); @@ -299,14 +340,58 @@ contract AllocatedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, Test assertEq(_allowlist.lotBuyerSpent(_lotId, _BUYER), amount, "lotBuyerSpent"); } + // setLotAdmin + // [X] when the caller is not the lot admin + // [X] it reverts + // [X] given the lot is not registered + // [X] it reverts + // [X] when the new admin is the zero address + // [X] it reverts + // [X] it sets the lot admin + + function test_setLotAdmin_callerNotAdmin() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_newAdminZeroAddress_reverts() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, address(0)); + } + + function test_setLotAdmin() public givenBatchOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + assertEq(_allowlist.lotAdmin(_lotId), _SELLER_TWO, "lotAdmin"); + } + // setMerkleRoot - // [X] when the caller is not the lot seller + // [X] when the caller is not the lot admin // [X] it reverts - // [X] when the lot is not registered + // [X] given the lot is not registered // [X] it reverts + // [X] given the lot admin has been changed + // [X] the merkle root is updated // [X] the merkle root is updated - function test_setMerkleRoot_callerNotSeller() public givenBatchOnCreate { + function test_setMerkleRoot_callerNotAdmin() public givenBatchOnCreate { // Expect revert bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); vm.expectRevert(err); @@ -323,6 +408,16 @@ contract AllocatedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, Test _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); } + function test_setMerkleRoot_lotAdminChanged() public givenBatchOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + vm.prank(_SELLER_TWO); + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + } + function test_setMerkleRoot() public givenBatchOnCreate { bytes32 newMerkleRoot = 0x0fdc3942d9af344db31ff2e80c06bc4e558dc967ca5b4d421d741870f5ea40df; diff --git a/test/callbacks/CappedMerkleAllowlistAtomic.t.sol b/test/callbacks/CappedMerkleAllowlistAtomic.t.sol index 58a8ddcf..30576cb7 100644 --- a/test/callbacks/CappedMerkleAllowlistAtomic.t.sol +++ b/test/callbacks/CappedMerkleAllowlistAtomic.t.sol @@ -87,6 +87,20 @@ contract CappedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, TestCo _; } + modifier givenAtomicOnCreateMerkleRootZero() { + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(bytes32(0), _BUYER_LIMIT) + ); + _; + } + function _onPurchase(uint96 lotId_, address buyer_, uint256 amount_) internal { vm.prank(address(_auctionHouse)); _allowlist.onPurchase(lotId_, buyer_, amount_, 0, false, abi.encode(_merkleProof)); @@ -97,11 +111,14 @@ contract CappedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, TestCo // [X] it reverts // [X] if the caller is not the auction house // [X] it reverts + // [X] if the merkle root is zero + // [X] it sets the merkle root to zero // [X] if the seller is not the seller for the allowlist // [X] it sets the merkle root and buyer limit // [X] if the lot is already registered // [X] it reverts // [X] it sets the merkle root and buyer limit + // [X] it sets the lot admin to the seller function test_onCreate_allowlistParametersIncorrectFormat_reverts() public { // Expect revert @@ -170,10 +187,30 @@ contract CappedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, TestCo ); } + function test_onCreate_merkleRootZero() public { + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(bytes32(0), _BUYER_LIMIT) + ); + + // Assert + assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); + assertEq(_allowlist.lotMerkleRoot(_lotId), bytes32(0), "lotMerkleRoot"); + assertEq(_allowlist.lotBuyerLimit(_lotId), _BUYER_LIMIT, "lotBuyerLimit"); + } + function test_onCreate() public givenAtomicOnCreate { assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); assertEq(_allowlist.lotBuyerLimit(_lotId), _BUYER_LIMIT, "lotBuyerLimit"); + assertEq(_allowlist.lotAdmin(_lotId), _SELLER, "lotAdmin"); } // onPurchase @@ -181,6 +218,8 @@ contract CappedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, TestCo // [X] it reverts // [X] if the lot is not registered // [X] it reverts + // [X] if the merkle root is zero + // [X] it succeeds for any buyer and any amount // [X] if the buyer is not in the merkle tree // [X] it reverts // [X] if the amount is greater than the buyer limit @@ -205,6 +244,21 @@ contract CappedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, TestCo _onPurchase(_lotId, _BUYER, 1e18); } + function test_onPurchase_merkleRootZero( + address buyer_, + uint256 amount_ + ) public givenAtomicOnCreateMerkleRootZero { + vm.assume(buyer_ != _BUYER && buyer_ != _BUYER_TWO); + uint256 amount = bound(amount_, 1, _LOT_CAPACITY); + + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onPurchase(_lotId, buyer_, amount, 0, false, ""); + + // Assert + assertEq(_allowlist.lotBuyerSpent(_lotId, buyer_), amount, "lotBuyerSpent"); + } + function test_onPurchase_buyerNotInMerkleTree_reverts() public givenAtomicOnCreate { // Expect revert bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); @@ -245,4 +299,87 @@ contract CappedMerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, TestCo assertEq(_allowlist.lotBuyerSpent(_lotId, _BUYER), amount, "lotBuyerSpent"); } + + // setLotAdmin + // [X] when the caller is not the lot admin + // [X] it reverts + // [X] given the lot is not registered + // [X] it reverts + // [X] when the new admin is the zero address + // [X] it reverts + // [X] it sets the lot admin + + function test_setLotAdmin_callerNotAdmin() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_newAdminZeroAddress_reverts() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, address(0)); + } + + function test_setLotAdmin() public givenAtomicOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + assertEq(_allowlist.lotAdmin(_lotId), _SELLER_TWO, "lotAdmin"); + } + + // setMerkleRoot + // [X] when the caller is not the lot admin + // [X] it reverts + // [X] given the lot is not registered + // [X] it reverts + // [X] given the lot admin has been changed + // [X] the merkle root is updated + // [X] it sets the merkle root + + function test_setMerkleRoot_callerNotAdmin() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + } + + function test_setMerkleRoot_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + } + + function test_setMerkleRoot_lotAdminChanged() public givenAtomicOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + vm.prank(_SELLER_TWO); + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + } + + function test_setMerkleRoot() public givenAtomicOnCreate { + vm.prank(_SELLER); + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + } } diff --git a/test/callbacks/CappedMerkleAllowlistBatch.t.sol b/test/callbacks/CappedMerkleAllowlistBatch.t.sol index bf7c0e29..5e3f5f9a 100644 --- a/test/callbacks/CappedMerkleAllowlistBatch.t.sol +++ b/test/callbacks/CappedMerkleAllowlistBatch.t.sol @@ -87,6 +87,20 @@ contract CappedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, TestCon _; } + modifier givenBatchOnCreateMerkleRootZero() { + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(bytes32(0), _BUYER_LIMIT) + ); + _; + } + function _onBid(uint96 lotId_, address buyer_, uint256 amount_) internal { vm.prank(address(_auctionHouse)); _allowlist.onBid(lotId_, 1, buyer_, amount_, abi.encode(_merkleProof)); @@ -97,11 +111,14 @@ contract CappedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, TestCon // [X] it reverts // [X] if the caller is not the auction house // [X] it reverts + // [X] if the merkle root is zero + // [X] it sets the merkle root to zero // [X] if the seller is not the seller for the allowlist // [X] it sets the merkle root and buyer limit // [X] if the lot is already registered // [X] it reverts // [X] it sets the merkle root and buyer limit + // [X] it sets the lot admin to the seller function test_onCreate_allowlistParametersIncorrectFormat_reverts() public { // Expect revert @@ -170,10 +187,30 @@ contract CappedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, TestCon ); } + function test_onCreate_merkleRootZero() public { + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(bytes32(0), _BUYER_LIMIT) + ); + + // Assert + assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); + assertEq(_allowlist.lotMerkleRoot(_lotId), bytes32(0), "lotMerkleRoot"); + assertEq(_allowlist.lotBuyerLimit(_lotId), _BUYER_LIMIT, "lotBuyerLimit"); + } + function test_onCreate() public givenBatchOnCreate { assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); assertEq(_allowlist.lotBuyerLimit(_lotId), _BUYER_LIMIT, "lotBuyerLimit"); + assertEq(_allowlist.lotAdmin(_lotId), _SELLER, "lotAdmin"); } // onBid @@ -181,6 +218,8 @@ contract CappedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, TestCon // [X] it reverts // [X] if the lot is not registered // [X] it reverts + // [X] if the merkle root is zero + // [X] it succeeds for any buyer and any amount // [X] if the buyer is not in the merkle tree // [X] it reverts // [X] if the amount is greater than the buyer limit @@ -205,6 +244,21 @@ contract CappedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, TestCon _onBid(_lotId, _BUYER, 1e18); } + function test_onBid_merkleRootZero( + address buyer_, + uint256 amount_ + ) public givenBatchOnCreateMerkleRootZero { + vm.assume(buyer_ != _BUYER && buyer_ != _BUYER_TWO); + uint256 amount = bound(amount_, 1, _LOT_CAPACITY); + + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onBid(_lotId, 1, buyer_, amount, ""); + + // Assert + assertEq(_allowlist.lotBuyerSpent(_lotId, buyer_), amount, "lotBuyerSpent"); + } + function test_onBid_buyerNotInMerkleTree_reverts() public givenBatchOnCreate { // Expect revert bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); @@ -245,4 +299,87 @@ contract CappedMerkleAllowlistBatchTest is Test, Permit2User, WithSalts, TestCon assertEq(_allowlist.lotBuyerSpent(_lotId, _BUYER), amount, "lotBuyerSpent"); } + + // setLotAdmin + // [X] when the caller is not the lot admin + // [X] it reverts + // [X] given the lot is not registered + // [X] it reverts + // [X] when the new admin is the zero address + // [X] it reverts + // [X] it sets the lot admin + + function test_setLotAdmin_callerNotAdmin() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_newAdminZeroAddress_reverts() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, address(0)); + } + + function test_setLotAdmin() public givenBatchOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + assertEq(_allowlist.lotAdmin(_lotId), _SELLER_TWO, "lotAdmin"); + } + + // setMerkleRoot + // [X] when the caller is not the lot admin + // [X] it reverts + // [X] given the lot is not registered + // [X] it reverts + // [X] given the lot admin has been changed + // [X] the merkle root is updated + // [X] it sets the merkle root + + function test_setMerkleRoot_callerNotAdmin() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + } + + function test_setMerkleRoot_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + } + + function test_setMerkleRoot_lotAdminChanged() public givenBatchOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + vm.prank(_SELLER_TWO); + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + } + + function test_setMerkleRoot() public givenBatchOnCreate { + vm.prank(_SELLER); + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + } } diff --git a/test/callbacks/MerkleAllowlistAtomic.t.sol b/test/callbacks/MerkleAllowlistAtomic.t.sol new file mode 100644 index 00000000..c2c90cd6 --- /dev/null +++ b/test/callbacks/MerkleAllowlistAtomic.t.sol @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Test} from "@forge-std-1.9.1/Test.sol"; +import {Callbacks} from "@axis-core-1.0.4/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.4-test/lib/permit2/Permit2User.sol"; + +import {AtomicAuctionHouse} from "@axis-core-1.0.4/AtomicAuctionHouse.sol"; + +import {BaseCallback} from "@axis-core-1.0.4/bases/BaseCallback.sol"; + +import {MerkleAllowlist} from "../../src/callbacks/allowlists/MerkleAllowlist.sol"; + +import {WithSalts} from "../../script/salts/WithSalts.s.sol"; +import {TestConstants} from "../Constants.sol"; + +contract MerkleAllowlistAtomicTest is Test, Permit2User, WithSalts, TestConstants { + using Callbacks for MerkleAllowlist; + + address internal constant _PROTOCOL = address(0x3); + address internal constant _BUYER = address(0x4); + address internal constant _BUYER_TWO = address(0x5); + address internal constant _BASE_TOKEN = address(0x6); + address internal constant _QUOTE_TOKEN = address(0x7); + address internal constant _SELLER_TWO = address(0x8); + address internal constant _BUYER_THREE = address(0x9); + + uint256 internal constant _LOT_CAPACITY = 10e18; + + uint96 internal _lotId = 1; + + AtomicAuctionHouse internal _auctionHouse; + MerkleAllowlist internal _allowlist; + + // Includes _BUYER, _BUYER_TWO but not _BUYER_THREE + bytes32 internal constant _MERKLE_ROOT = + 0xc92348ba87c65979cc4f264810321a35efa64e795075908af2c507a22d4da472; + bytes32[] internal _merkleProof; + + function setUp() public { + // Create an AuctionHouse at a deterministic address, since it is used as input to callbacks + AtomicAuctionHouse auctionHouse = new AtomicAuctionHouse(_OWNER, _PROTOCOL, _permit2Address); + _auctionHouse = AtomicAuctionHouse(address(0x000000000000000000000000000000000000000A)); + vm.etch(address(_auctionHouse), address(auctionHouse).code); + vm.store(address(_auctionHouse), bytes32(uint256(0)), bytes32(abi.encode(_OWNER))); // Owner + vm.store(address(_auctionHouse), bytes32(uint256(6)), bytes32(abi.encode(1))); // Reentrancy + vm.store(address(_auctionHouse), bytes32(uint256(10)), bytes32(abi.encode(_PROTOCOL))); // Protocol + + // Generate a salt for the contract + Callbacks.Permissions memory permissions = Callbacks.Permissions({ + onCreate: true, + onCancel: false, + onCurate: false, + onPurchase: true, + onBid: false, + onSettle: false, + receiveQuoteTokens: false, + sendBaseTokens: false + }); + bytes32 salt = _generateSalt( + "AtomicMerkleAllowlist", + type(MerkleAllowlist).creationCode, + abi.encode(address(_auctionHouse), permissions), + "90" + ); + + vm.broadcast(); + _allowlist = new MerkleAllowlist{salt: salt}(address(_auctionHouse), permissions); + + _merkleProof.push( + bytes32(0x16db2e4b9f8dc120de98f8491964203ba76de27b27b29c2d25f85a325cd37477) + ); // Corresponds to _BUYER + } + + modifier givenAtomicOnCreate() { + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(_MERKLE_ROOT) + ); + _; + } + + modifier givenAtomicOnCreateMerkleRootZero() { + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, _SELLER, _BASE_TOKEN, _QUOTE_TOKEN, _LOT_CAPACITY, false, abi.encode(bytes32(0)) + ); + _; + } + + function _onPurchase(uint96 lotId_, address buyer_, uint256 amount_) internal { + vm.prank(address(_auctionHouse)); + _allowlist.onPurchase(lotId_, buyer_, amount_, 0, false, abi.encode(_merkleProof)); + } + + // onCreate + // [X] when the allowlist parameters are in an incorrect format + // [X] it reverts + // [X] if the caller is not the auction house + // [X] it reverts + // [X] if the merkle root is zero + // [X] it sets the merkle root to zero + // [X] if the seller is not the seller for the allowlist + // [X] it sets the merkle root + // [X] if the lot is already registered + // [X] it reverts + // [X] it sets the merkle root + // [X] it sets the lot admin to the seller + + function test_onCreate_allowlistParametersIncorrectFormat_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(_MERKLE_ROOT, uint256(20)) + ); + } + + function test_onCreate_callerNotAuctionHouse_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(_MERKLE_ROOT) + ); + } + + function test_onCreate_sellerNotSeller() public { + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER_TWO, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(_MERKLE_ROOT) + ); + + assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + } + + function test_onCreate_alreadyRegistered_reverts() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(_MERKLE_ROOT) + ); + } + + function test_onCreate_merkleRootZero() public { + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, _SELLER, _BASE_TOKEN, _QUOTE_TOKEN, _LOT_CAPACITY, false, abi.encode(bytes32(0)) + ); + + // Assert + assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); + assertEq(_allowlist.lotMerkleRoot(_lotId), bytes32(0), "lotMerkleRoot"); + } + + function test_onCreate() public givenAtomicOnCreate { + assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + assertEq(_allowlist.lotAdmin(_lotId), _SELLER, "lotAdmin"); + } + + // onPurchase + // [X] if the caller is not the auction house + // [X] it reverts + // [X] if the lot is not registered + // [X] it reverts + // [X] if the merkle root is zero + // [X] it succeeds for any buyer + // [X] if the buyer is not in the merkle tree + // [X] it reverts + // [X] it succeeds + + function test_onPurchase_callerNotAuctionHouse_reverts() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.onPurchase(_lotId, _BUYER, 1e18, 0, false, ""); + } + + function test_onPurchase_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _onPurchase(_lotId, _BUYER, 1e18); + } + + function test_onPurchase_buyerNotInMerkleTree_reverts() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _onPurchase(_lotId, _BUYER_THREE, 1e18); + } + + function test_onPurchase_merkleRootZero( + address buyer_ + ) public givenAtomicOnCreateMerkleRootZero { + vm.assume(buyer_ != _BUYER && buyer_ != _BUYER_TWO); + + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onPurchase(_lotId, buyer_, 1e18, 0, false, ""); + } + + function test_onPurchase( + uint256 amount_ + ) public givenAtomicOnCreate { + uint256 amount = bound(amount_, 1, 1e18); + + _onPurchase(_lotId, _BUYER, amount); + } + + // setLotAdmin + // [X] when the caller is not the lot admin + // [X] it reverts + // [X] given the lot is not registered + // [X] it reverts + // [X] when the new admin is the zero address + // [X] it reverts + // [X] it sets the lot admin + + function test_setLotAdmin_callerNotAdmin() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_newAdminZeroAddress_reverts() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, address(0)); + } + + function test_setLotAdmin() public givenAtomicOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + assertEq(_allowlist.lotAdmin(_lotId), _SELLER_TWO, "lotAdmin"); + } + + // setMerkleRoot + // [X] when the caller is not the lot admin + // [X] it reverts + // [X] given the lot is not registered + // [X] it reverts + // [X] given the lot admin has been changed + // [X] the merkle root is updated + // [X] it sets the merkle root + + function test_setMerkleRoot_callerNotAdmin() public givenAtomicOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + } + + function test_setMerkleRoot_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + } + + function test_setMerkleRoot_lotAdminChanged() public givenAtomicOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + vm.prank(_SELLER_TWO); + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + } + + function test_setMerkleRoot() public givenAtomicOnCreate { + vm.prank(_SELLER); + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + } +} diff --git a/test/callbacks/MerkleAllowlistBatch.t.sol b/test/callbacks/MerkleAllowlistBatch.t.sol new file mode 100644 index 00000000..77c702c2 --- /dev/null +++ b/test/callbacks/MerkleAllowlistBatch.t.sol @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Test} from "@forge-std-1.9.1/Test.sol"; +import {Callbacks} from "@axis-core-1.0.4/lib/Callbacks.sol"; +import {Permit2User} from "@axis-core-1.0.4-test/lib/permit2/Permit2User.sol"; + +import {BatchAuctionHouse} from "@axis-core-1.0.4/BatchAuctionHouse.sol"; + +import {BaseCallback} from "@axis-core-1.0.4/bases/BaseCallback.sol"; + +import {MerkleAllowlist} from "../../src/callbacks/allowlists/MerkleAllowlist.sol"; + +import {WithSalts} from "../../script/salts/WithSalts.s.sol"; +import {TestConstants} from "../Constants.sol"; + +contract MerkleAllowlistBatchTest is Test, Permit2User, WithSalts, TestConstants { + using Callbacks for MerkleAllowlist; + + address internal constant _PROTOCOL = address(0x3); + address internal constant _BUYER = address(0x4); + address internal constant _BUYER_TWO = address(0x5); + address internal constant _BASE_TOKEN = address(0x6); + address internal constant _QUOTE_TOKEN = address(0x7); + address internal constant _SELLER_TWO = address(0x8); + address internal constant _BUYER_THREE = address(0x9); + + uint256 internal constant _LOT_CAPACITY = 10e18; + + uint96 internal _lotId = 1; + + BatchAuctionHouse internal _auctionHouse; + MerkleAllowlist internal _allowlist; + + // Includes _BUYER, _BUYER_TWO but not _BUYER_THREE + bytes32 internal constant _MERKLE_ROOT = + 0xc92348ba87c65979cc4f264810321a35efa64e795075908af2c507a22d4da472; + bytes32[] internal _merkleProof; + + function setUp() public { + // Create an AuctionHouse at a deterministic address, since it is used as input to callbacks + BatchAuctionHouse auctionHouse = new BatchAuctionHouse(_OWNER, _PROTOCOL, _permit2Address); + _auctionHouse = BatchAuctionHouse(address(0x000000000000000000000000000000000000000A)); + vm.etch(address(_auctionHouse), address(auctionHouse).code); + vm.store(address(_auctionHouse), bytes32(uint256(0)), bytes32(abi.encode(_OWNER))); // Owner + vm.store(address(_auctionHouse), bytes32(uint256(6)), bytes32(abi.encode(1))); // Reentrancy + vm.store(address(_auctionHouse), bytes32(uint256(10)), bytes32(abi.encode(_PROTOCOL))); // Protocol + + // Get the salt + Callbacks.Permissions memory permissions = Callbacks.Permissions({ + onCreate: true, + onCancel: false, + onCurate: false, + onPurchase: false, + onBid: true, + onSettle: false, + receiveQuoteTokens: false, + sendBaseTokens: false + }); + bytes32 salt = _generateSalt( + "BatchMerkleAllowlist", + type(MerkleAllowlist).creationCode, + abi.encode(address(_auctionHouse), permissions), + "88" + ); + + vm.broadcast(); + _allowlist = new MerkleAllowlist{salt: salt}(address(_auctionHouse), permissions); + + _merkleProof.push( + bytes32(0x16db2e4b9f8dc120de98f8491964203ba76de27b27b29c2d25f85a325cd37477) + ); // Corresponds to _BUYER + } + + modifier givenBatchOnCreate() { + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(_MERKLE_ROOT) + ); + _; + } + + modifier givenBatchOnCreateMerkleRootZero() { + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, _SELLER, _BASE_TOKEN, _QUOTE_TOKEN, _LOT_CAPACITY, false, abi.encode(bytes32(0)) + ); + _; + } + + function _onBid(uint96 lotId_, address buyer_, uint256 amount_) internal { + vm.prank(address(_auctionHouse)); + _allowlist.onBid(lotId_, 1, buyer_, amount_, abi.encode(_merkleProof)); + } + + // onCreate + // [X] when the allowlist parameters are in an incorrect format + // [X] it reverts + // [X] if the caller is not the auction house + // [X] it reverts + // [X] if the merkle root is zero + // [X] it sets the merkle root to zero + // [X] if the seller is not the seller for the allowlist + // [X] it sets the merkle root + // [X] if the lot is already registered + // [X] it reverts + // [X] it sets the merkle root + // [X] it sets the lot admin to the seller + + function test_onCreate_allowlistParametersIncorrectFormat_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(_MERKLE_ROOT, uint256(20)) + ); + } + + function test_onCreate_callerNotAuctionHouse_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(_MERKLE_ROOT) + ); + } + + function test_onCreate_sellerNotSeller() public { + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER_TWO, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(_MERKLE_ROOT) + ); + + assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + } + + function test_onCreate_alreadyRegistered_reverts() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, + _SELLER, + _BASE_TOKEN, + _QUOTE_TOKEN, + _LOT_CAPACITY, + false, + abi.encode(_MERKLE_ROOT) + ); + } + + function test_onCreate_merkleRootZero() public { + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onCreate( + _lotId, _SELLER, _BASE_TOKEN, _QUOTE_TOKEN, _LOT_CAPACITY, false, abi.encode(bytes32(0)) + ); + + // Assert + assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); + assertEq(_allowlist.lotMerkleRoot(_lotId), bytes32(0), "lotMerkleRoot"); + } + + function test_onCreate() public givenBatchOnCreate { + assertEq(_allowlist.lotIdRegistered(_lotId), true, "lotIdRegistered"); + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + assertEq(_allowlist.lotAdmin(_lotId), _SELLER, "lotAdmin"); + } + + // onBid + // [X] if the caller is not the auction house + // [X] it reverts + // [X] if the lot is not registered + // [X] it reverts + // [X] if the merkle root is zero + // [X] it succeeds for any buyer + // [X] if the buyer is not in the merkle tree + // [X] it reverts + // [X] it succeeds + + function test_onBid_callerNotAuctionHouse_reverts() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.onBid(_lotId, 1, _BUYER, 1e18, ""); + } + + function test_onBid_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _onBid(_lotId, _BUYER, 1e18); + } + + function test_onBid_buyerNotInMerkleTree_reverts() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _onBid(_lotId, _BUYER_THREE, 1e18); + } + + function test_onBid_merkleRootZero( + address buyer_ + ) public givenBatchOnCreateMerkleRootZero { + vm.assume(buyer_ != _BUYER && buyer_ != _BUYER_TWO); + + // Call function + vm.prank(address(_auctionHouse)); + _allowlist.onBid(_lotId, 1, buyer_, 1e18, ""); + } + + function test_onBid( + uint256 amount_ + ) public givenBatchOnCreate { + uint256 amount = bound(amount_, 1, 1e18); + + _onBid(_lotId, _BUYER, amount); + } + + // setLotAdmin + // [X] when the caller is not the lot admin + // [X] it reverts + // [X] given the lot is not registered + // [X] it reverts + // [X] when the new admin is the zero address + // [X] it reverts + // [X] it sets the lot admin + + function test_setLotAdmin_callerNotAdmin() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + } + + function test_setLotAdmin_newAdminZeroAddress_reverts() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_InvalidParams.selector); + vm.expectRevert(err); + + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, address(0)); + } + + function test_setLotAdmin() public givenBatchOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + assertEq(_allowlist.lotAdmin(_lotId), _SELLER_TWO, "lotAdmin"); + } + + // setMerkleRoot + // [X] when the caller is not the lot admin + // [X] it reverts + // [X] given the lot is not registered + // [X] it reverts + // [X] given the lot admin has been changed + // [X] the merkle root is updated + // [X] it sets the merkle root + + function test_setMerkleRoot_callerNotAdmin() public givenBatchOnCreate { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + } + + function test_setMerkleRoot_lotNotRegistered_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(BaseCallback.Callback_NotAuthorized.selector); + vm.expectRevert(err); + + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + } + + function test_setMerkleRoot_lotAdminChanged() public givenBatchOnCreate { + vm.prank(_SELLER); + _allowlist.setLotAdmin(_lotId, _SELLER_TWO); + + vm.prank(_SELLER_TWO); + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + } + + function test_setMerkleRoot() public givenBatchOnCreate { + vm.prank(_SELLER); + _allowlist.setMerkleRoot(_lotId, _MERKLE_ROOT); + + assertEq(_allowlist.lotMerkleRoot(_lotId), _MERKLE_ROOT, "lotMerkleRoot"); + } +}