From c81ca5bcccbb29b709f83812b9afba9f32041026 Mon Sep 17 00:00:00 2001 From: Krishang Date: Tue, 24 Oct 2023 21:16:25 +0530 Subject: [PATCH 01/12] btt tests: createAuction --- .../createAuction/createAuction.t.sol | 283 ++++++++++++++++++ .../createAuction/createAuction.tree | 13 + 2 files changed, 296 insertions(+) create mode 100644 src/test/marketplace/english-auctions/createAuction/createAuction.t.sol create mode 100644 src/test/marketplace/english-auctions/createAuction/createAuction.tree diff --git a/src/test/marketplace/english-auctions/createAuction/createAuction.t.sol b/src/test/marketplace/english-auctions/createAuction/createAuction.t.sol new file mode 100644 index 000000000..f6dd2cdee --- /dev/null +++ b/src/test/marketplace/english-auctions/createAuction/createAuction.t.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract CreateAuctionTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + /// @dev Emitted when a new auction is created. + event NewAuction( + address indexed auctionCreator, + uint256 indexed auctionId, + address indexed assetContract, + IEnglishAuctions.Auction auction + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_createAuction_whenCallerDoesntHaveListerRole() public { + assertEq(Permissions(marketplace).hasRole(keccak256("LISTER_ROLE"), seller), false); + + vm.prank(seller); + vm.expectRevert("!LISTER_ROLE"); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenCallerHasListerRole() { + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + _; + } + + function test_createAuction_whenAssetDoesnHaveAssetRole() public whenCallerHasListerRole { + assertEq(Permissions(marketplace).hasRole(keccak256("ASSET_ROLE"), address(erc721)), false); + + vm.prank(seller); + vm.expectRevert("!ASSET_ROLE"); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenAssetHasAssetRole() { + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + _; + } + + function test_createAuction_whenAuctionParamsAreInvalid() public whenCallerHasListerRole whenAssetHasAssetRole { + // This is one way for params to be invalid. `_validateNewAuction` has its own tests. + auctionParams.quantity = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioning zero quantity."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenAuctionParamsAreValid() { + _; + } + + function test_createAuction_whenAuctionParamsAreValid() + public + whenCallerHasListerRole + whenAssetHasAssetRole + whenAuctionParamsAreValid + { + uint256 expectedAuctionId = 0; + + assertEq(EnglishAuctionsLogic(marketplace).totalAuctions(), 0); + assertEq(erc721.ownerOf(0), seller); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).assetContract, address(0)); + + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + IEnglishAuctions.Auction memory dummyAuction; + + vm.prank(seller); + vm.expectEmit(true, true, true, false); + emit NewAuction(seller, expectedAuctionId, auctionParams.assetContract, dummyAuction); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + assertEq(EnglishAuctionsLogic(marketplace).totalAuctions(), 1); + assertEq(erc721.ownerOf(0), marketplace); + assertEq(EnglishAuctionsLogic(marketplace).getAllAuctions(0, 0).length, 1); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).auctionId, expectedAuctionId); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).assetContract, + auctionParams.assetContract + ); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).tokenId, auctionParams.tokenId); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).minimumBidAmount, + auctionParams.minimumBidAmount + ); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).buyoutBidAmount, + auctionParams.buyoutBidAmount + ); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).timeBufferInSeconds, + auctionParams.timeBufferInSeconds + ); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).bidBufferBps, + auctionParams.bidBufferBps + ); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).startTimestamp, + auctionParams.startTimestamp + ); + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).endTimestamp, + auctionParams.endTimestamp + ); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).auctionCreator, seller); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).currency, auctionParams.currency); + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).tokenType), + uint256(IEnglishAuctions.TokenType.ERC721) + ); + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } +} diff --git a/src/test/marketplace/english-auctions/createAuction/createAuction.tree b/src/test/marketplace/english-auctions/createAuction/createAuction.tree new file mode 100644 index 000000000..644a98b7a --- /dev/null +++ b/src/test/marketplace/english-auctions/createAuction/createAuction.tree @@ -0,0 +1,13 @@ +function createAuction(AuctionParameters calldata _params) +├── when the caller does not have LISTER_ROLE +│ └── it should revert +└── when the caller has LISTER_ROLE + ├── when the asset does not have ASSET_ROLE + │ └── it should revert + └── when the asset has ASSET_ROLE + ├── when the auction params are invalid + │ └── it should revert + └── when the auction params are valid + ├── it should create the intended auction + ├── it should escrow asset to auction + └── it should emit an AuctionCreated event with auction creator, auction ID, asset contract, auction data \ No newline at end of file From 749289c7d0f9d8929bed9dfb7b207b783aa7698c Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 01:06:54 +0530 Subject: [PATCH 02/12] btt tests: bidInAuction --- .../bidInAuction/bidInAuction.t.sol | 664 ++++++++++++++++++ .../bidInAuction/bidInAuction.tree | 52 ++ 2 files changed, 716 insertions(+) create mode 100644 src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol create mode 100644 src/test/marketplace/english-auctions/bidInAuction/bidInAuction.tree diff --git a/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol new file mode 100644 index 000000000..c52a18cec --- /dev/null +++ b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol @@ -0,0 +1,664 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + uint256 auctionId = 0; + uint256 bidAmount = 10 ether; + EnglishAuctionsLogic(msg.sender).bidInAuction(auctionId, bidAmount); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract BidInAuctionTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + uint256 internal auctionId; + uint256 internal bidAmount; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event NewBid( + uint256 indexed auctionId, + address indexed bidder, + address indexed assetContract, + uint256 bidAmount, + IEnglishAuctions.Auction auction + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Set bidAmount + bidAmount = auctionParams.minimumBidAmount; + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_bidInAuction_callIsReentrant() public { + vm.warp(auctionParams.startTimestamp + 1); + address reentrantRecipient = address(new ReentrantRecipient()); + + erc20.mint(reentrantRecipient, 100 ether); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), reentrantRecipient); + + vm.startPrank(reentrantRecipient); + erc20.approve(marketplace, 100 ether); + vm.expectRevert(); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.buyoutBidAmount); + vm.stopPrank(); + } + + modifier whenCallIsNotReentrant() { + _; + } + + function test_bidInAuction_whenAuctionDoesNotExist() public whenCallIsNotReentrant { + vm.prank(buyer); + vm.expectRevert("Marketplace: invalid auction."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId + 1, bidAmount); + } + + modifier whenAuctionExists() { + _; + } + + function test_bidInAuction_whenAuctionIsNotActive() public whenCallIsNotReentrant whenAuctionExists { + vm.prank(buyer); + vm.expectRevert("Marketplace: inactive auction."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + } + + modifier whenAuctionIsActive() { + vm.warp(auctionParams.startTimestamp + 1); + _; + } + + function test_bidInAuction_whenBidAmountIsZero() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + { + vm.prank(buyer); + vm.expectRevert("Marketplace: Bidding with zero amount."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, 0); + } + + modifier whenBidAmountIsNotZero() { + bidAmount = auctionParams.minimumBidAmount; + _; + } + + function test_bidInAuction_whenBidAmountIsGtBuyoutPrice() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + { + vm.prank(buyer); + vm.expectRevert("Marketplace: Bidding above buyout price."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.buyoutBidAmount + 1); + } + + modifier whenBidAmountLtBuyoutPrice() { + bidAmount = auctionParams.buyoutBidAmount - 1; + _; + } + + modifier whenBidAmountEqBuyoutPrice() { + bidAmount = auctionParams.buyoutBidAmount; + _; + } + + modifier whenCurrentWinningBid() { + // Existing winning bid. + erc20.mint(winningBidder, 100 ether); + + vm.prank(winningBidder); + erc20.approve(marketplace, 100 ether); + + vm.prank(winningBidder); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.minimumBidAmount + 1); + _; + } + + modifier whenNoCurrentWinningBid() { + _; + } + + function test_bidInAuction_buyoutAndExistingWinningBid() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountEqBuyoutPrice + whenCurrentWinningBid + { + uint256 winningBidderBal = erc20.balanceOf(winningBidder); + + assertEq(erc20.balanceOf(marketplace), auctionParams.minimumBidAmount + 1); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + + assertEq(erc20.balanceOf(winningBidder), winningBidderBal + auctionParams.minimumBidAmount + 1); + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + // Auction is marked CLOSED in auction state when creator collected payout + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } + + function test_bidInAuction_buyoutAndNoWinningBid() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountEqBuyoutPrice + whenNoCurrentWinningBid + { + assertEq(erc20.balanceOf(marketplace), 0); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + // Auction is marked CLOSED in auction state when creator collected payout + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } + + function test_bidInAuction_whenBidIsNotNewWinningBid() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenCurrentWinningBid + whenBidAmountIsNotZero + whenBidAmountLtBuyoutPrice + { + vm.prank(buyer); + vm.expectRevert("Marketplace: not winning bid."); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.minimumBidAmount); + } + + modifier whenBidIsNewWinningBig() { + bidAmount = auctionParams.buyoutBidAmount - 1; + _; + } + + modifier whenBidWithinTimeBuffer() { + vm.warp(auctionParams.endTimestamp - auctionParams.timeBufferInSeconds); + _; + } + + function test_bidInAuction_noBuyoutAndExistingWinningBid() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountLtBuyoutPrice + whenBidIsNewWinningBig + whenCurrentWinningBid + { + uint256 winningBidderBal = erc20.balanceOf(winningBidder); + + assertEq(erc20.balanceOf(marketplace), auctionParams.minimumBidAmount + 1); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + (address bidderBefore, address currencyBefore, uint256 bidAmountBefore) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderBefore, winningBidder); + assertEq(currencyBefore, address(erc20)); + assertEq(bidAmountBefore, auctionParams.minimumBidAmount + 1); + + vm.startPrank(buyer); + vm.expectEmit(true, true, true, false); + emit NewBid( + auctionId, + buyer, + address(erc1155), + bidAmount, + EnglishAuctionsLogic(marketplace).getAuction(auctionId) + ); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + vm.stopPrank(); + + (address bidderAfter, address currencyAfter, uint256 bidAmountAfter) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderAfter, buyer); + assertEq(currencyAfter, address(erc20)); + assertEq(bidAmountAfter, bidAmount); + + assertEq(erc20.balanceOf(winningBidder), winningBidderBal + auctionParams.minimumBidAmount + 1); + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + // Auction is marked CLOSED in auction state when creator collected payout + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } + + function test_bidInAuction_noBuyoutAndNoWinningBid() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountLtBuyoutPrice + whenBidIsNewWinningBig + whenNoCurrentWinningBid + { + assertEq(erc20.balanceOf(marketplace), 0); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + (address bidderBefore, address currencyBefore, uint256 bidAmountBefore) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderBefore, address(0)); + assertEq(currencyBefore, address(erc20)); + assertEq(bidAmountBefore, 0); + + vm.startPrank(buyer); + vm.expectEmit(true, true, true, false); + emit NewBid( + auctionId, + buyer, + address(erc1155), + bidAmount, + EnglishAuctionsLogic(marketplace).getAuction(auctionId) + ); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + vm.stopPrank(); + + (address bidderAfter, address currencyAfter, uint256 bidAmountAfter) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderAfter, buyer); + assertEq(currencyAfter, address(erc20)); + assertEq(bidAmountAfter, bidAmount); + + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } + + function test_bidInAuction_noBuyoutAndExistingWinningBid_withinTimeBuffer() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountLtBuyoutPrice + whenBidIsNewWinningBig + whenCurrentWinningBid + whenBidWithinTimeBuffer + { + uint256 winningBidderBal = erc20.balanceOf(winningBidder); + + assertEq(erc20.balanceOf(marketplace), auctionParams.minimumBidAmount + 1); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + (address bidderBefore, address currencyBefore, uint256 bidAmountBefore) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderBefore, winningBidder); + assertEq(currencyBefore, address(erc20)); + assertEq(bidAmountBefore, auctionParams.minimumBidAmount + 1); + + assertEq(EnglishAuctionsLogic(marketplace).getAuction(auctionId).endTimestamp, auctionParams.endTimestamp); + + vm.startPrank(buyer); + vm.expectEmit(true, true, true, false); + emit NewBid( + auctionId, + buyer, + address(erc1155), + bidAmount, + EnglishAuctionsLogic(marketplace).getAuction(auctionId) + ); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + vm.stopPrank(); + + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(auctionId).endTimestamp, + auctionParams.endTimestamp + auctionParams.timeBufferInSeconds + ); + + (address bidderAfter, address currencyAfter, uint256 bidAmountAfter) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderAfter, buyer); + assertEq(currencyAfter, address(erc20)); + assertEq(bidAmountAfter, bidAmount); + + assertEq(erc20.balanceOf(winningBidder), winningBidderBal + auctionParams.minimumBidAmount + 1); + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + // Auction is marked CLOSED in auction state when creator collected payout + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } + + function test_bidInAuction_noBuyoutAndNoWinningBid_withinTimeBuffer() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + whenBidAmountLtBuyoutPrice + whenBidIsNewWinningBig + whenBidWithinTimeBuffer + whenNoCurrentWinningBid + { + assertEq(erc20.balanceOf(marketplace), 0); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + (address bidderBefore, address currencyBefore, uint256 bidAmountBefore) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderBefore, address(0)); + assertEq(currencyBefore, address(erc20)); + assertEq(bidAmountBefore, 0); + + assertEq(EnglishAuctionsLogic(marketplace).getAuction(auctionId).endTimestamp, auctionParams.endTimestamp); + + vm.startPrank(buyer); + vm.expectEmit(true, true, true, false); + emit NewBid( + auctionId, + buyer, + address(erc1155), + bidAmount, + EnglishAuctionsLogic(marketplace).getAuction(auctionId) + ); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + vm.stopPrank(); + + assertEq( + EnglishAuctionsLogic(marketplace).getAuction(auctionId).endTimestamp, + auctionParams.endTimestamp + auctionParams.timeBufferInSeconds + ); + + (address bidderAfter, address currencyAfter, uint256 bidAmountAfter) = EnglishAuctionsLogic(marketplace) + .getWinningBid(auctionId); + + assertEq(bidderAfter, buyer); + assertEq(currencyAfter, address(erc20)); + assertEq(bidAmountAfter, bidAmount); + + assertEq(erc20.balanceOf(marketplace), bidAmount); + + assertEq(erc1155.balanceOf(marketplace, auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(seller, auctionParams.tokenId), 99); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + } +} diff --git a/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.tree b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.tree new file mode 100644 index 000000000..bcb182035 --- /dev/null +++ b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.tree @@ -0,0 +1,52 @@ +function bidInAuction(uint256 _auctionId, uint256 _bidAmount) +├── when the call is reentrant +│ └── it should revert ✅ +└── when the call is not reentrant + ├── when the auction does not exist + │ └── it should revert ✅ + └── when the auction exists + ├── when the auction is not active + │ └── it should revert ✅ + └── when the auction is active + ├── when the bid amount is zero + │ └── it should revert ✅ + └── when the bid amount is not zero + ├── when the bid amount is greater than buyout price + │ └── it should revert ✅ + └── when the bid amount is less than or equal to buyout price + ├── when the bid amount is equal to buyout price + │ ├── when there is a current winning bid ✅ + │ │ ├── it should transfer previous winning bid back to previous winning bidder + │ │ ├── it should transfer auctioned tokens to bidder + │ │ ├── it should escrow incoming bid + │ │ └── it should emit a NewBid event + │ └── when there is no current winning bid ✅ + │ ├── it should transfer auctioned tokens to bidder + │ ├── it should escrow incoming bid + │ └── it should emit a NewBid event + └── when the bid amount is less than buyout price + ├── when the bid is not a new winning bid + │ └── it should revert ✅ + └── when the bid is a new winning bid + ├── when the remaining auction duration is less than time buffer + │ ├── when there is a current winning bid ✅ + │ │ ├── it should add time buffer to auction duration + │ │ ├── it should transfer previous winning bid back to previous winning bidder + │ │ ├── it should escrow incoming bid + │ │ └── it should emit a NewBid event + │ │ └── it set auction status as completed + │ └── when there is no current winning bid ✅ + │ ├── it should add time buffer to auction duration + │ ├── it should escrow incoming bid + │ └── it should emit a NewBid event + │ └── it set auction status as completed + └── when the remaining auction duration is not less than time buffer + ├── when there is a current winning bid ✅ + │ ├── it should transfer previous winning bid back to previous winning bidder + │ ├── it should escrow incoming bid + │ └── it should emit a NewBid event + │ └── it set auction status as completed + └── when there is no current winning bid ✅ + ├── it should escrow incoming bid + └── it should emit a NewBid event + └── it set auction status as completed \ No newline at end of file From 5381479531520a045d007ac3ee2ab773932601a8 Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 01:07:31 +0530 Subject: [PATCH 03/12] checkmark btt tests createAuction --- .../english-auctions/createAuction/createAuction.tree | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/marketplace/english-auctions/createAuction/createAuction.tree b/src/test/marketplace/english-auctions/createAuction/createAuction.tree index 644a98b7a..dcfa71e4c 100644 --- a/src/test/marketplace/english-auctions/createAuction/createAuction.tree +++ b/src/test/marketplace/english-auctions/createAuction/createAuction.tree @@ -1,13 +1,13 @@ function createAuction(AuctionParameters calldata _params) ├── when the caller does not have LISTER_ROLE -│ └── it should revert +│ └── it should revert ✅ └── when the caller has LISTER_ROLE ├── when the asset does not have ASSET_ROLE - │ └── it should revert + │ └── it should revert ✅ └── when the asset has ASSET_ROLE ├── when the auction params are invalid - │ └── it should revert - └── when the auction params are valid + │ └── it should revert ✅ + └── when the auction params are valid ✅ ├── it should create the intended auction ├── it should escrow asset to auction └── it should emit an AuctionCreated event with auction creator, auction ID, asset contract, auction data \ No newline at end of file From 7daf390ba9aedd09232ad5e19cdb775db4b8de52 Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 14:12:44 +0530 Subject: [PATCH 04/12] btt tests: collectAuctionTokens --- .../collectAuctionTokens.t.sol | 298 ++++++++++++++++++ .../collectAuctionTokens.tree | 18 ++ 2 files changed, 316 insertions(+) create mode 100644 src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol create mode 100644 src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.tree diff --git a/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol b/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol new file mode 100644 index 000000000..89c249ab6 --- /dev/null +++ b/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + uint256 auctionId = 0; + uint256 bidAmount = 10 ether; + EnglishAuctionsLogic(msg.sender).bidInAuction(auctionId, bidAmount); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract CollectAuctionPayoutTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + uint256 internal auctionId; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event AuctionClosed( + uint256 indexed auctionId, + address indexed assetContract, + address indexed closer, + uint256 tokenId, + address auctionCreator, + address winningBidder + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_collectAuctionTokens_whenAuctionIsCancelled() public { + vm.prank(seller); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + + vm.prank(buyer); + vm.expectRevert("Marketplace: invalid auction."); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + } + + modifier whenAuctionNotCancelled() { + _; + } + + function test_collectAuctionTokens_whenAuctionIsActive() public whenAuctionNotCancelled { + vm.warp(auctionParams.startTimestamp + 1); + + vm.prank(buyer); + vm.expectRevert("Marketplace: auction still active."); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + } + + modifier whenAuctionHasEnded() { + vm.warp(auctionParams.endTimestamp + 1); + _; + } + + function test_collectAuctionTokens_whenNoWinningBid() public whenAuctionNotCancelled whenAuctionHasEnded { + vm.warp(auctionParams.endTimestamp + 1); + + vm.prank(buyer); + vm.expectRevert("Marketplace: no bids were made."); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + } + + modifier whenAuctionHasWinningBid() { + vm.warp(auctionParams.startTimestamp + 1); + + // Bid in auction + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.minimumBidAmount); + _; + } + + function test_collectAuctionTokens_whenAuctionTokensAlreadyPaidOut() + public + whenAuctionNotCancelled + whenAuctionHasWinningBid + whenAuctionHasEnded + { + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + + vm.prank(buyer); + vm.expectRevert("Marketplace: payout already completed."); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + } + + modifier whenAuctionTokensNotPaidOut() { + _; + } + + function test_collectAuctionTokens_whenAuctionTokensNotYetPaidOut() + public + whenAuctionNotCancelled + whenAuctionHasWinningBid + whenAuctionHasEnded + whenAuctionTokensNotPaidOut + { + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + assertEq(erc1155.balanceOf(address(marketplace), auctionParams.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 0); + + vm.prank(buyer); + vm.expectEmit(true, true, true, true); + emit AuctionClosed(auctionId, address(erc1155), buyer, auctionParams.tokenId, seller, buyer); + EnglishAuctionsLogic(marketplace).collectAuctionTokens(auctionId); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.COMPLETED) + ); + assertEq(EnglishAuctionsLogic(marketplace).getAuction(auctionId).endTimestamp, uint64(block.timestamp)); + + assertEq(erc1155.balanceOf(address(marketplace), auctionParams.tokenId), 0); + assertEq(erc1155.balanceOf(buyer, auctionParams.tokenId), 1); + } +} diff --git a/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.tree b/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.tree new file mode 100644 index 000000000..d54bd1ba3 --- /dev/null +++ b/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.tree @@ -0,0 +1,18 @@ +function collectAuctionTokens(uint256 _auctionId) external +. +├── when the auction cancelled +│ └── it reverts ✅ +└── when the auction is not cancelled + ├── when the auction is still active + │ └── it should reverts ✅ + └── when the auction is not active + ├── when the auction has no wining bid + │ └── it should reverts ✅ + └── when the auction has a wining bid + ├── when auction bidder has already been paid out tokens + │ └── it should reverts ✅ + └── when auction creator has not been paid out tokens ✅ + ├── it should set auction timestamp to block timestamp + ├── it should set auction state to completed + ├── it should transfer auction tokens to bidder + └── it should emit AuctionClosed event with auction ID, asset contract, caller, tokenId, creator, bidder \ No newline at end of file From 44084a9f076d92daa2e914d1aa06a64beb3e51bf Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 14:29:52 +0530 Subject: [PATCH 05/12] btt tests: collectAuctionPayout --- .../collectAuctionPayout.t.sol | 298 ++++++++++++++++++ .../collectAuctionPayout.tree | 17 + .../collectAuctionTokens.t.sol | 2 +- 3 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.t.sol create mode 100644 src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.tree diff --git a/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.t.sol b/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.t.sol new file mode 100644 index 000000000..0ea873384 --- /dev/null +++ b/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.t.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + uint256 auctionId = 0; + uint256 bidAmount = 10 ether; + EnglishAuctionsLogic(msg.sender).bidInAuction(auctionId, bidAmount); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract CollectAuctionPayoutTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + uint256 internal auctionId; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event AuctionClosed( + uint256 indexed auctionId, + address indexed assetContract, + address indexed closer, + uint256 tokenId, + address auctionCreator, + address winningBidder + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_collectAuctionPayout_whenAuctionIsCancelled() public { + vm.prank(seller); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid auction."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + modifier whenAuctionNotCancelled() { + _; + } + + function test_collectAuctionPayout_whenAuctionIsActive() public whenAuctionNotCancelled { + vm.warp(auctionParams.startTimestamp + 1); + + vm.prank(seller); + vm.expectRevert("Marketplace: auction still active."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + modifier whenAuctionHasEnded() { + vm.warp(auctionParams.endTimestamp + 1); + _; + } + + function test_collectAuctionPayout_whenNoWinningBid() public whenAuctionNotCancelled whenAuctionHasEnded { + vm.warp(auctionParams.endTimestamp + 1); + + vm.prank(seller); + vm.expectRevert("Marketplace: no bids were made."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + modifier whenAuctionHasWinningBid() { + vm.warp(auctionParams.startTimestamp + 1); + + // Bid in auction + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.minimumBidAmount); + _; + } + + function test_collectAuctionPayout_whenAuctionTokensAlreadyPaidOut() + public + whenAuctionNotCancelled + whenAuctionHasWinningBid + whenAuctionHasEnded + { + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + vm.prank(seller); + vm.expectRevert("Marketplace: payout already completed."); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + modifier whenAuctionTokensNotPaidOut() { + _; + } + + function test_collectAuctionPayout_whenAuctionTokensNotYetPaidOut() + public + whenAuctionNotCancelled + whenAuctionHasWinningBid + whenAuctionHasEnded + whenAuctionTokensNotPaidOut + { + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + + uint256 marketplaceBal = erc20.balanceOf(address(marketplace)); + assertEq(marketplaceBal, auctionParams.minimumBidAmount); + assertEq(erc20.balanceOf(seller), 0); + + vm.prank(seller); + vm.expectEmit(true, true, true, true); + emit AuctionClosed(auctionId, address(erc1155), seller, auctionParams.tokenId, seller, buyer); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.COMPLETED) + ); + + assertEq(erc20.balanceOf(address(marketplace)), 0); + assertEq(erc20.balanceOf(seller), marketplaceBal); + } +} diff --git a/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.tree b/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.tree new file mode 100644 index 000000000..571fd4a61 --- /dev/null +++ b/src/test/marketplace/english-auctions/collectAuctionPayout/collectAuctionPayout.tree @@ -0,0 +1,17 @@ +function collectAuctionPayout(uint256 _auctionId) external +. +├── when auction is cancelled +│ └── it should revert ✅ +└── when auction is not cancelled + ├── when auction has not ended + │ └── it should revert ✅ + └── when auction has ended + ├── when there is no winning bid + │ └── it should revert ✅ + └── when there is a winning bid + ├── when creator already paid out + │ └── it should revert ✅ + └── when creator not already paid out ✅ + ├── it should set auction status to completed + ├── it should pay the auction winning bid to creator + └── it should emit AuctionClosed event diff --git a/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol b/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol index 89c249ab6..11b451d67 100644 --- a/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol +++ b/src/test/marketplace/english-auctions/collectAuctionTokens/collectAuctionTokens.t.sol @@ -30,7 +30,7 @@ contract ReentrantRecipient is ERC1155Holder { } } -contract CollectAuctionPayoutTest is BaseTest, IExtension { +contract CollectAuctionTokensTest is BaseTest, IExtension { // Target contract address public marketplace; From af0aa68aeac31b80b301ec97ed79931a2c9a8fd7 Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 15:13:08 +0530 Subject: [PATCH 06/12] btt tests: cancelAuction --- .../cancelAuction/cancelAuction.t.sol | 261 ++++++++++++++++++ .../cancelAuction/cancelAuction.tree | 14 + 2 files changed, 275 insertions(+) create mode 100644 src/test/marketplace/english-auctions/cancelAuction/cancelAuction.t.sol create mode 100644 src/test/marketplace/english-auctions/cancelAuction/cancelAuction.tree diff --git a/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.t.sol b/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.t.sol new file mode 100644 index 000000000..aa69aacb2 --- /dev/null +++ b/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + uint256 auctionId = 0; + uint256 bidAmount = 10 ether; + EnglishAuctionsLogic(msg.sender).bidInAuction(auctionId, bidAmount); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract CancelAuctionTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + uint256 internal auctionId; + uint256 internal bidAmount; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event CancelledAuction(address indexed auctionCreator, uint256 indexed auctionId); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Set bidAmount + bidAmount = auctionParams.minimumBidAmount; + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_cancelAuction_whenAuctionDoesntExist() public { + vm.prank(seller); + vm.expectRevert("Marketplace: invalid auction."); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId + 100); + } + + modifier whenAuctionExists() { + _; + } + + function test_cancelAuction_whenCallerNotCreator() public whenAuctionExists { + vm.prank(buyer); + vm.expectRevert("Marketplace: not auction creator."); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + } + + modifier whenCallerIsCreator() { + _; + } + + function test_cancelAuction_whenWinningBid() public whenAuctionExists whenCallerIsCreator { + vm.warp(auctionParams.startTimestamp + 1); + + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, bidAmount); + + vm.prank(seller); + vm.expectRevert("Marketplace: bids already made."); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + } + + modifier whenNoWinningBid() { + _; + } + + function test_cancelAuction_whenNoWinningBid() public whenAuctionExists whenCallerIsCreator whenNoWinningBid { + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CREATED) + ); + assertEq(erc1155.balanceOf(address(marketplace), 0), 1); + assertEq(erc1155.balanceOf(seller, 0), 99); + + vm.prank(seller); + vm.expectEmit(true, true, true, true); + emit CancelledAuction(seller, auctionId); + EnglishAuctionsLogic(marketplace).cancelAuction(auctionId); + + assertEq( + uint256(EnglishAuctionsLogic(marketplace).getAuction(auctionId).status), + uint256(IEnglishAuctions.Status.CANCELLED) + ); + + assertEq(erc1155.balanceOf(address(marketplace), 0), 0); + assertEq(erc1155.balanceOf(seller, 0), 100); + } +} diff --git a/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.tree b/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.tree new file mode 100644 index 000000000..397d6bc6d --- /dev/null +++ b/src/test/marketplace/english-auctions/cancelAuction/cancelAuction.tree @@ -0,0 +1,14 @@ +function cancelAuction(uint256 _auctionId) external +. +├── when auction does not exist +│ └── it should revert ✅ +└── when auction exists + ├── when the caller is not auction creator + │ └── it should revert ✅ + └── when the caller is auction creator + ├── when there is a winning bidder + │ └── it should revert ✅ + └── when there is no winning bidder ✅ + ├── it should set auction status as cancelled + ├── it should transfer auction tokens back to creator + └── it should emit CancelledAuction event From 6002d9b0468a3f252d86ec8e815a17bca5b49366 Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 15:57:15 +0530 Subject: [PATCH 07/12] btt tests: payout --- .../english-auctions/_payout/_payout.t.sol | 297 ++++++++++++++++++ .../english-auctions/_payout/_payout.tree | 17 + 2 files changed, 314 insertions(+) create mode 100644 src/test/marketplace/english-auctions/_payout/_payout.t.sol create mode 100644 src/test/marketplace/english-auctions/_payout/_payout.tree diff --git a/src/test/marketplace/english-auctions/_payout/_payout.t.sol b/src/test/marketplace/english-auctions/_payout/_payout.t.sol new file mode 100644 index 000000000..a0ebbfda9 --- /dev/null +++ b/src/test/marketplace/english-auctions/_payout/_payout.t.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; +import { MockRoyaltyEngineV1 } from "../../../mocks/MockRoyaltyEngineV1.sol"; +import { PlatformFee } from "contracts/extension/PlatformFee.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract ReentrantRecipient is ERC1155Holder { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes memory data + ) public virtual override returns (bytes4) { + uint256 auctionId = 0; + uint256 bidAmount = 10 ether; + EnglishAuctionsLogic(msg.sender).bidInAuction(auctionId, bidAmount); + return super.onERC1155Received(operator, from, id, value, data); + } +} + +contract EnglishAuctionsPayoutTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + uint256 internal auctionId; + uint256 internal bidAmount; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event NewBid( + uint256 indexed auctionId, + address indexed bidder, + address indexed assetContract, + uint256 bidAmount, + IEnglishAuctions.Auction auction + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), platformFeeRecipient, uint16(platformFeeBps)) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 100 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Set bidAmount + bidAmount = auctionParams.minimumBidAmount; + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + auctionId = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + address payable[] internal mockRecipients; + uint256[] internal mockAmounts; + MockRoyaltyEngineV1 internal royaltyEngine; + + function _setupRoyaltyEngine() private { + mockRecipients.push(payable(address(0x12345))); + mockRecipients.push(payable(address(0x56789))); + + mockAmounts.push(10 ether); + mockAmounts.push(15 ether); + + royaltyEngine = new MockRoyaltyEngineV1(mockRecipients, mockAmounts); + } + + function test_payout_whenZeroRoyaltyRecipients() public { + vm.warp(auctionParams.startTimestamp); + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.buyoutBidAmount); + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + uint256 totalPrice = auctionParams.buyoutBidAmount; + + uint256 platformFees = (totalPrice * platformFeeBps) / 10_000; + + { + // Platform fee recipient receives correct amount + assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFees); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq(address(erc20), seller, totalPrice - platformFees); + } + } + + modifier whenNonZeroRoyaltyRecipients() { + _setupRoyaltyEngine(); + + // Add RoyaltyEngine to marketplace + vm.prank(marketplaceDeployer); + RoyaltyPaymentsLogic(marketplace).setRoyaltyEngine(address(royaltyEngine)); + + _; + } + + function test_payout_whenInsufficientFundsToPayRoyaltyAfterPlatformFeePayout() public whenNonZeroRoyaltyRecipients { + vm.prank(marketplaceDeployer); + PlatformFee(marketplace).setPlatformFeeInfo(platformFeeRecipient, 9999); // 99.99% fees; + + // Buy tokens from listing. + vm.warp(auctionParams.startTimestamp); + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.buyoutBidAmount); + + vm.prank(seller); + vm.expectRevert("fees exceed the price"); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + } + + function test_payout_whenSufficientFundsToPayRoyaltyAfterPlatformFeePayout() public whenNonZeroRoyaltyRecipients { + assertEq(RoyaltyPaymentsLogic(marketplace).getRoyaltyEngineAddress(), address(royaltyEngine)); + + vm.warp(auctionParams.startTimestamp); + vm.prank(buyer); + EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.buyoutBidAmount); + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).collectAuctionPayout(auctionId); + + uint256 totalPrice = auctionParams.buyoutBidAmount; + uint256 platformFees = (totalPrice * platformFeeBps) / 10_000; + + { + // Royalty recipients receive correct amounts + assertBalERC20Eq(address(erc20), mockRecipients[0], mockAmounts[0]); + assertBalERC20Eq(address(erc20), mockRecipients[1], mockAmounts[1]); + + // Platform fee recipient receives correct amount + assertBalERC20Eq(address(erc20), platformFeeRecipient, platformFees); + + // Seller gets total price minus royalty amounts + assertBalERC20Eq(address(erc20), seller, totalPrice - mockAmounts[0] - mockAmounts[1] - platformFees); + } + } +} diff --git a/src/test/marketplace/english-auctions/_payout/_payout.tree b/src/test/marketplace/english-auctions/_payout/_payout.tree new file mode 100644 index 000000000..36b930e11 --- /dev/null +++ b/src/test/marketplace/english-auctions/_payout/_payout.tree @@ -0,0 +1,17 @@ +function _payout( + address _payer, + address _payee, + address _currencyToUse, + uint256 _totalPayoutAmount, + Auction memory _targetAuction +) +├── when there are zero royalty recipients ✅ +│ ├── it should transfer platform fee from payer to platform fee recipient +│ └── it should transfer remainder of currency from payer to payee +└── when there are non-zero royalty recipients + ├── when the total royalty payout exceeds remainder payout after having paid platform fee + │ └── it should revert ✅ + └── when the total royalty payout does not exceed remainder payout after having paid platform fee ✅ + ├── it should transfer platform fee from payer to platform fee recipient + ├── it should transfer royalty fee from payer to royalty recipients + └── it should transfer remainder of currency from payer to payeew \ No newline at end of file From aebac56d3e8a9e9873d335979bbfb8870f537696 Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 16:52:20 +0530 Subject: [PATCH 08/12] btt tests: transferAuctionTokens --- .../_transferAuctionTokens.t.sol | 202 ++++++++++++++++++ .../_transferAuctionTokens.tree | 9 + 2 files changed, 211 insertions(+) create mode 100644 src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.t.sol create mode 100644 src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.tree diff --git a/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.t.sol b/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.t.sol new file mode 100644 index 000000000..5b3418335 --- /dev/null +++ b/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.t.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; +import { MockRoyaltyEngineV1 } from "../../../mocks/MockRoyaltyEngineV1.sol"; +import { PlatformFee } from "contracts/extension/PlatformFee.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract MockTransferAuctionTokens is EnglishAuctionsLogic { + constructor(address _nativeTokenWrapper) EnglishAuctionsLogic(_nativeTokenWrapper) {} + + function transferAuctionTokens( + address _from, + address _to, + Auction memory _auction + ) external { + _transferAuctionTokens(_from, _to, _auction); + } +} + +contract TransferAuctionTokensTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + uint256 internal auctionId_erc1155; + uint256 internal auctionId_erc721; + uint256 internal bidAmount; + address internal winningBidder = address(0x123); + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + event NewBid( + uint256 indexed auctionId, + address indexed bidder, + address indexed assetContract, + uint256 bidAmount, + IEnglishAuctions.Auction auction + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), platformFeeRecipient, uint16(platformFeeBps)) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc1155); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 100 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100 minutes; + uint64 endTimestamp = 200 minutes; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Set bidAmount + bidAmount = auctionParams.minimumBidAmount; + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + + // Create auction + vm.startPrank(seller); + erc721.setApprovalForAll(marketplace, true); + erc1155.setApprovalForAll(marketplace, true); + + auctionId_erc1155 = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + auctionParams.assetContract = address(erc721); + auctionId_erc721 = EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + vm.stopPrank(); + + // Mint currency to bidder. + erc20.mint(buyer, 10_000 ether); + + vm.prank(buyer); + erc20.approve(marketplace, 100 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new MockTransferAuctionTokens(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](3); + extension_englishAuctions.functions[0] = ExtensionFunction( + MockTransferAuctionTokens.transferAuctionTokens.selector, + "transferAuctionTokens(address,address,(uint256,uint256,uint256,uint256,uint256,uint64,uint64,uint64,uint64,address,address,address,uint8,uint8))" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_transferAuctionTokens_erc1155() public { + IEnglishAuctions.Auction memory auction = EnglishAuctionsLogic(marketplace).getAuction(auctionId_erc1155); + + assertEq(erc1155.balanceOf(address(marketplace), auction.tokenId), 1); + assertEq(erc1155.balanceOf(buyer, auction.tokenId), 0); + + MockTransferAuctionTokens(marketplace).transferAuctionTokens(address(marketplace), buyer, auction); + + assertEq(erc1155.balanceOf(address(marketplace), auction.tokenId), 0); + assertEq(erc1155.balanceOf(buyer, auction.tokenId), 1); + } + + function test_transferAuctionTokens_erc721() public { + IEnglishAuctions.Auction memory auction = EnglishAuctionsLogic(marketplace).getAuction(auctionId_erc721); + + assertEq(erc721.ownerOf(auction.tokenId), address(marketplace)); + + MockTransferAuctionTokens(marketplace).transferAuctionTokens(address(marketplace), buyer, auction); + + assertEq(erc721.ownerOf(auction.tokenId), buyer); + } +} diff --git a/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.tree b/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.tree new file mode 100644 index 000000000..c44dc8c55 --- /dev/null +++ b/src/test/marketplace/english-auctions/_transferAuctionTokens/_transferAuctionTokens.tree @@ -0,0 +1,9 @@ +function _transferAuctionTokens( + address _from, + address _to, + Auction memory _auction +) +├── when the token is ERC1155 +│ └── it should transfer ERC1155 tokens from the specified owner to recipient +└── when the token is ERC721 + └── it should transfer ERC721 tokens from the specified owner to recipient \ No newline at end of file From 91dc6ccea6d2efef97aceff16f7d3358dac26bf2 Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 17:43:12 +0530 Subject: [PATCH 09/12] Update createAuction and bidInAuction tests --- .../bidInAuction/bidInAuction.t.sol | 5 +++ .../createAuction/createAuction.t.sol | 37 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol index c52a18cec..9f1134af0 100644 --- a/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol +++ b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol @@ -387,6 +387,8 @@ contract BidInAuctionTest is BaseTest, IExtension { whenBidAmountIsNotZero whenBidAmountLtBuyoutPrice { + assertEq(EnglishAuctionsLogic(marketplace).isNewWinningBid(auctionId, auctionParams.minimumBidAmount), false); + vm.prank(buyer); vm.expectRevert("Marketplace: not winning bid."); EnglishAuctionsLogic(marketplace).bidInAuction(auctionId, auctionParams.minimumBidAmount); @@ -394,6 +396,7 @@ contract BidInAuctionTest is BaseTest, IExtension { modifier whenBidIsNewWinningBig() { bidAmount = auctionParams.buyoutBidAmount - 1; + assertEq(EnglishAuctionsLogic(marketplace).isNewWinningBid(auctionId, bidAmount), true); _; } @@ -432,6 +435,8 @@ contract BidInAuctionTest is BaseTest, IExtension { assertEq(currencyBefore, address(erc20)); assertEq(bidAmountBefore, auctionParams.minimumBidAmount + 1); + assertEq(EnglishAuctionsLogic(marketplace).isNewWinningBid(auctionId, bidAmount), true); + vm.startPrank(buyer); vm.expectEmit(true, true, true, false); emit NewBid( diff --git a/src/test/marketplace/english-auctions/createAuction/createAuction.t.sol b/src/test/marketplace/english-auctions/createAuction/createAuction.t.sol index f6dd2cdee..8bc759b31 100644 --- a/src/test/marketplace/english-auctions/createAuction/createAuction.t.sol +++ b/src/test/marketplace/english-auctions/createAuction/createAuction.t.sol @@ -15,6 +15,12 @@ import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.s import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; +contract InvalidToken { + function supportsInterface(bytes4) public pure returns (bool) { + return false; + } +} + contract CreateAuctionTest is BaseTest, IExtension { // Target contract address public marketplace; @@ -201,7 +207,29 @@ contract CreateAuctionTest is BaseTest, IExtension { _; } - function test_createAuction_whenAuctionParamsAreInvalid() public whenCallerHasListerRole whenAssetHasAssetRole { + function test_createAuction_whenTokenIsInvalid() public whenCallerHasListerRole whenAssetHasAssetRole { + address newToken = address(new InvalidToken()); + + vm.prank(marketplaceDeployer); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), newToken); + + auctionParams.assetContract = newToken; + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioned token must be ERC1155 or ERC721."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenTokenIsValid() { + _; + } + + function test_createAuction_whenAuctionParamsAreInvalid() + public + whenCallerHasListerRole + whenAssetHasAssetRole + whenTokenIsValid + { // This is one way for params to be invalid. `_validateNewAuction` has its own tests. auctionParams.quantity = 0; @@ -218,6 +246,7 @@ contract CreateAuctionTest is BaseTest, IExtension { public whenCallerHasListerRole whenAssetHasAssetRole + whenTokenIsValid whenAuctionParamsAreValid { uint256 expectedAuctionId = 0; @@ -238,6 +267,12 @@ contract CreateAuctionTest is BaseTest, IExtension { assertEq(EnglishAuctionsLogic(marketplace).totalAuctions(), 1); assertEq(erc721.ownerOf(0), marketplace); + assertEq(EnglishAuctionsLogic(marketplace).getAllAuctions(0, 0).length, 1); + + assertEq(EnglishAuctionsLogic(marketplace).getAllValidAuctions(0, 0).length, 0); + vm.warp(auctionParams.startTimestamp); + assertEq(EnglishAuctionsLogic(marketplace).getAllValidAuctions(0, 0).length, 1); + assertEq(EnglishAuctionsLogic(marketplace).getAllAuctions(0, 0).length, 1); assertEq(EnglishAuctionsLogic(marketplace).getAuction(expectedAuctionId).auctionId, expectedAuctionId); assertEq( From eca3e4e01547a2f774cc54ab4afb47953c28e1fc Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 18:47:50 +0530 Subject: [PATCH 10/12] btt tests: _validateNewAuction --- .../_validateNewAuction.t.sol | 304 ++++++++++++++++++ .../_validateNewAuction.tree | 20 ++ 2 files changed, 324 insertions(+) create mode 100644 src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.t.sol create mode 100644 src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree diff --git a/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.t.sol b/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.t.sol new file mode 100644 index 000000000..5f716875f --- /dev/null +++ b/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// Test helper imports +import "../../../utils/BaseTest.sol"; + +// Test contracts and interfaces +import { RoyaltyPaymentsLogic } from "contracts/extension/plugin/RoyaltyPayments.sol"; +import { MarketplaceV3, IPlatformFee } from "contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol"; +import { EnglishAuctionsLogic } from "contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { ERC721Base } from "contracts/base/ERC721Base.sol"; + +import { IEnglishAuctions } from "contracts/prebuilts/marketplace/IMarketplace.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +contract InvalidToken { + function supportsInterface(bytes4) public pure returns (bool) { + return false; + } +} + +contract ValidateNewAuctionTest is BaseTest, IExtension { + // Target contract + address public marketplace; + + // Participants + address public marketplaceDeployer; + address public seller; + address public buyer; + + // Auction parameters + IEnglishAuctions.AuctionParameters internal auctionParams; + + // Events + /// @dev Emitted when a new auction is created. + event NewAuction( + address indexed auctionCreator, + uint256 indexed auctionId, + address indexed assetContract, + IEnglishAuctions.Auction auction + ); + + function setUp() public override { + super.setUp(); + + marketplaceDeployer = getActor(1); + seller = getActor(2); + buyer = getActor(3); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address impl = address( + new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth))) + ); + + vm.prank(marketplaceDeployer); + marketplace = address( + new TWProxy( + impl, + abi.encodeCall( + MarketplaceV3.initialize, + (marketplaceDeployer, "", new address[](0), marketplaceDeployer, 0) + ) + ) + ); + + // Setup roles for seller and assets + vm.startPrank(marketplaceDeployer); + Permissions(marketplace).revokeRole(keccak256("ASSET_ROLE"), address(0)); + Permissions(marketplace).revokeRole(keccak256("LISTER_ROLE"), address(0)); + Permissions(marketplace).grantRole(keccak256("LISTER_ROLE"), seller); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc1155)); + Permissions(marketplace).grantRole(keccak256("ASSET_ROLE"), address(erc721)); + + vm.stopPrank(); + + vm.label(impl, "MarketplaceV3_Impl"); + vm.label(marketplace, "Marketplace"); + vm.label(seller, "Seller"); + vm.label(buyer, "Buyer"); + vm.label(address(erc721), "ERC721_Token"); + vm.label(address(erc1155), "ERC1155_Token"); + + // Sample auction parameters. + address assetContract = address(erc721); + uint256 tokenId = 0; + uint256 quantity = 1; + address currency = address(erc20); + uint256 minimumBidAmount = 1 ether; + uint256 buyoutBidAmount = 10 ether; + uint64 timeBufferInSeconds = 10 seconds; + uint64 bidBufferBps = 1000; + uint64 startTimestamp = 100; + uint64 endTimestamp = 200; + + // Auction tokens. + auctionParams = IEnglishAuctions.AuctionParameters( + assetContract, + tokenId, + quantity, + currency, + minimumBidAmount, + buyoutBidAmount, + timeBufferInSeconds, + bidBufferBps, + startTimestamp, + endTimestamp + ); + + // Mint NFT to seller. + erc721.mint(seller, 1); // to, amount + erc1155.mint(seller, 0, 100); // to, id, amount + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](1); + + // Deploy `EnglishAuctions` + address englishAuctions = address(new EnglishAuctionsLogic(address(weth))); + vm.label(englishAuctions, "EnglishAuctions_Extension"); + + // Extension: EnglishAuctionsLogic + Extension memory extension_englishAuctions; + extension_englishAuctions.metadata = ExtensionMetadata({ + name: "EnglishAuctionsLogic", + metadataURI: "ipfs://EnglishAuctions", + implementation: englishAuctions + }); + + extension_englishAuctions.functions = new ExtensionFunction[](12); + extension_englishAuctions.functions[0] = ExtensionFunction( + EnglishAuctionsLogic.totalAuctions.selector, + "totalAuctions()" + ); + extension_englishAuctions.functions[1] = ExtensionFunction( + EnglishAuctionsLogic.createAuction.selector, + "createAuction((address,uint256,uint256,address,uint256,uint256,uint64,uint64,uint64,uint64))" + ); + extension_englishAuctions.functions[2] = ExtensionFunction( + EnglishAuctionsLogic.cancelAuction.selector, + "cancelAuction(uint256)" + ); + extension_englishAuctions.functions[3] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionPayout.selector, + "collectAuctionPayout(uint256)" + ); + extension_englishAuctions.functions[4] = ExtensionFunction( + EnglishAuctionsLogic.collectAuctionTokens.selector, + "collectAuctionTokens(uint256)" + ); + extension_englishAuctions.functions[5] = ExtensionFunction( + EnglishAuctionsLogic.bidInAuction.selector, + "bidInAuction(uint256,uint256)" + ); + extension_englishAuctions.functions[6] = ExtensionFunction( + EnglishAuctionsLogic.isNewWinningBid.selector, + "isNewWinningBid(uint256,uint256)" + ); + extension_englishAuctions.functions[7] = ExtensionFunction( + EnglishAuctionsLogic.getAuction.selector, + "getAuction(uint256)" + ); + extension_englishAuctions.functions[8] = ExtensionFunction( + EnglishAuctionsLogic.getAllAuctions.selector, + "getAllAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[9] = ExtensionFunction( + EnglishAuctionsLogic.getAllValidAuctions.selector, + "getAllValidAuctions(uint256,uint256)" + ); + extension_englishAuctions.functions[10] = ExtensionFunction( + EnglishAuctionsLogic.getWinningBid.selector, + "getWinningBid(uint256)" + ); + extension_englishAuctions.functions[11] = ExtensionFunction( + EnglishAuctionsLogic.isAuctionExpired.selector, + "isAuctionExpired(uint256)" + ); + + extensions[0] = extension_englishAuctions; + } + + function test_validateNewAuction_whenQuantityIsZero() public { + auctionParams.quantity = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioning zero quantity."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenNonZeroQuantity() { + auctionParams.quantity = 1; + _; + } + + function test_validateNewAuction_whenQuantityGtOneAndAssetERC721() public whenNonZeroQuantity { + auctionParams.quantity = 2; + auctionParams.assetContract = address(erc721); + + vm.prank(seller); + vm.expectRevert("Marketplace: auctioning invalid quantity."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenQtyOneOrAssetERC1155() { + auctionParams.quantity = 1; + auctionParams.assetContract = address(erc721); + _; + } + + function test_validateNewAuction_whenTimeBufferIsZero() public whenNonZeroQuantity whenQtyOneOrAssetERC1155 { + auctionParams.timeBufferInSeconds = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: no time-buffer."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenNonZeroTimeBuffer() { + _; + } + + function test_validateNewAuction_whenBidBufferIsZero() + public + whenNonZeroQuantity + whenQtyOneOrAssetERC1155 + whenNonZeroTimeBuffer + { + auctionParams.bidBufferBps = 0; + + vm.prank(seller); + vm.expectRevert("Marketplace: no bid-buffer."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenNonZeroBidBuffer() { + _; + } + + function test_validateNewAuction_whenInvalidTimestamps() + public + whenNonZeroQuantity + whenQtyOneOrAssetERC1155 + whenNonZeroTimeBuffer + whenNonZeroBidBuffer + { + vm.warp(auctionParams.startTimestamp + 61 minutes); + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid timestamps."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + vm.warp(auctionParams.startTimestamp); + + auctionParams.endTimestamp = auctionParams.startTimestamp - 1; + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid timestamps."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenValidTimestamps() { + _; + } + + function test_validateNewAuction_whenBuyoutLtMinimumBidAmt() + public + whenNonZeroQuantity + whenQtyOneOrAssetERC1155 + whenNonZeroTimeBuffer + whenNonZeroBidBuffer + whenValidTimestamps + { + auctionParams.buyoutBidAmount = auctionParams.minimumBidAmount - 1; + + vm.prank(seller); + vm.expectRevert("Marketplace: invalid bid amounts."); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + } + + modifier whenBuyoutGteMinimumBidAmt() { + _; + } + + function test_validateNewAuction_buyoutGteMinimumBidAmt() + public + whenNonZeroQuantity + whenQtyOneOrAssetERC1155 + whenNonZeroTimeBuffer + whenNonZeroBidBuffer + whenValidTimestamps + whenBuyoutGteMinimumBidAmt + { + vm.prank(seller); + erc721.setApprovalForAll(marketplace, true); + + vm.prank(seller); + EnglishAuctionsLogic(marketplace).createAuction(auctionParams); + + assertEq(EnglishAuctionsLogic(marketplace).totalAuctions(), 1); + } +} diff --git a/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree b/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree new file mode 100644 index 000000000..48f2b55d8 --- /dev/null +++ b/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree @@ -0,0 +1,20 @@ +function _validateNewAuction(AuctionParameters memory _params, TokenType _tokenType) internal view +. +├── when quantity is zero +│ └── it should revert +└── when the quantity is non zero + ├── when the quantity is greater than one and token type is ERC721 + │ └── it should revert + └── when the quantity is one or token type is ERC1155 + ├── when the time buffer is zero + │ └── it should revert + └── when the time buffer is non zero + ├── when the bid buffer is zero + │ └── it should revert + └── when the bid buffer is non zero + ├── when start and end timestamps are invalid + │ └── it should revert + └── when start and end timestamps are valid + ├── when buyout amount is less than minimum bid amount + │ └── it should revert + └── when buyout amount is zero or gte minimum bid amount \ No newline at end of file From 369ff59c8a7649a1691670a7b93c3aa034d14474 Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 19:00:23 +0530 Subject: [PATCH 11/12] Add missing bidInAuction test --- .../bidInAuction/bidInAuction.t.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol index 9f1134af0..ad7041106 100644 --- a/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol +++ b/src/test/marketplace/english-auctions/bidInAuction/bidInAuction.t.sol @@ -265,6 +265,20 @@ contract BidInAuctionTest is BaseTest, IExtension { _; } + function test_bidInAuction_whenAuctionCurrencyIsERC20AndMsgValueSent() + public + whenCallIsNotReentrant + whenAuctionExists + whenAuctionIsActive + whenBidAmountIsNotZero + { + vm.deal(buyer, 1 ether); + + vm.prank(buyer); + vm.expectRevert("Marketplace: invalid native tokens sent."); + EnglishAuctionsLogic(marketplace).bidInAuction{ value: 1 }(auctionId, auctionParams.buyoutBidAmount); + } + function test_bidInAuction_whenBidAmountIsGtBuyoutPrice() public whenCallIsNotReentrant From ed688a958997bf4b6c09c545254a7888ec7e465a Mon Sep 17 00:00:00 2001 From: Krishang Date: Wed, 25 Oct 2023 19:03:12 +0530 Subject: [PATCH 12/12] Add missing checkmarks --- .../_validateNewAuction/_validateNewAuction.tree | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree b/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree index 48f2b55d8..24429d6c5 100644 --- a/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree +++ b/src/test/marketplace/english-auctions/_validateNewAuction/_validateNewAuction.tree @@ -1,20 +1,20 @@ function _validateNewAuction(AuctionParameters memory _params, TokenType _tokenType) internal view . ├── when quantity is zero -│ └── it should revert +│ └── it should revert ✅ └── when the quantity is non zero ├── when the quantity is greater than one and token type is ERC721 - │ └── it should revert + │ └── it should revert ✅ └── when the quantity is one or token type is ERC1155 ├── when the time buffer is zero - │ └── it should revert + │ └── it should revert ✅ └── when the time buffer is non zero ├── when the bid buffer is zero - │ └── it should revert + │ └── it should revert ✅ └── when the bid buffer is non zero ├── when start and end timestamps are invalid - │ └── it should revert + │ └── it should revert ✅ └── when start and end timestamps are valid ├── when buyout amount is less than minimum bid amount - │ └── it should revert - └── when buyout amount is zero or gte minimum bid amount \ No newline at end of file + │ └── it should revert ✅ + └── when buyout amount is zero or gte minimum bid amount ✅ \ No newline at end of file