diff --git a/.gas-snapshot b/.gas-snapshot index 4df7cca..a15ddcf 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -9,48 +9,51 @@ ACLManagerTest:testGrantRole() (gas: 23547) ACLManagerTest:testRenounceRole() (gas: 27841) ACLManagerTest:testRoles() (gas: 15393) ACLManagerTest:testTransferRole() (gas: 21528) -BillboardTest:testAddToWhitelist() (gas: 252415) -BillboardTest:testApproveAndTransfer() (gas: 253043) -BillboardTest:testCalculateTax() (gas: 537542) -BillboardTest:testCannnotWithdrawTaxIfZero() (gas: 20183) -BillboardTest:testCannotAddToWhitelistByAttacker() (gas: 225988) -BillboardTest:testCannotApproveByAttacker() (gas: 221891) -BillboardTest:testCannotCalculateTax() (gas: 216404) -BillboardTest:testCannotClearAuctionIfAuctionNotEnded() (gas: 248674) -BillboardTest:testCannotClearAuctionIfNoBid() (gas: 256039) -BillboardTest:testCannotGetBlockFromEpoch() (gas: 8688) -BillboardTest:testCannotGetEpochFromBlock() (gas: 15590) -BillboardTest:testCannotPlaceBidByAttacker() (gas: 460155) -BillboardTest:testCannotPlaceBidIfAuctionEnded() (gas: 271390) -BillboardTest:testCannotRemoveToWhitelistByAttacker() (gas: 226118) -BillboardTest:testCannotSafeTransferByAttacker() (gas: 219003) -BillboardTest:testCannotSetBoardByAttacker() (gas: 227087) -BillboardTest:testCannotSetBoardByOwner() (gas: 359460) -BillboardTest:testCannotTransferByOperator() (gas: 224137) -BillboardTest:testCannotTransferToZeroAddress() (gas: 219713) -BillboardTest:testCannotUpgradeRegistryByAttacker() (gas: 9039) -BillboardTest:testCannotWithdrawBidIfAuctionNotEndedOrCleared(uint96) (runs: 256, μ: 600791, ~: 600791) -BillboardTest:testCannotWithdrawBidIfNotFound(uint96) (runs: 256, μ: 715717, ~: 715717) -BillboardTest:testCannotWithdrawBidIfWon(uint96) (runs: 256, μ: 966732, ~: 966732) -BillboardTest:testCannotWithdrawBidTwice(uint96) (runs: 256, μ: 1050826, ~: 1050826) -BillboardTest:testClearAuction(uint96) (runs: 256, μ: 697518, ~: 697518) -BillboardTest:testClearAuctions() (gas: 1248982) -BillboardTest:testGetBids(uint8,uint8,uint8) (runs: 256, μ: 5071994, ~: 2128035) -BillboardTest:testGetBlockFromEpoch() (gas: 14680) -BillboardTest:testGetEpochFromBlock() (gas: 15789) -BillboardTest:testGetTokenURI() (gas: 389096) -BillboardTest:testMintBoard() (gas: 580688) -BillboardTest:testPlaceBid(uint96) (runs: 256, μ: 781546, ~: 783879) -BillboardTest:testPlaceBidWithHigherPrice(uint96) (runs: 256, μ: 965751, ~: 965756) -BillboardTest:testPlaceBidWithSamePrices(uint96) (runs: 256, μ: 849576, ~: 853075) -BillboardTest:testPlaceBidZeroPrice() (gas: 401269) -BillboardTest:testRemoveToWhitelist() (gas: 239143) +BillboardTest:testApproveAndTransfer() (gas: 253021) +BillboardTest:testCalculateTax() (gas: 537366) +BillboardTest:testCannnotWithdrawTaxIfZero() (gas: 20184) +BillboardTest:testCannotApproveByAttacker() (gas: 221803) +BillboardTest:testCannotCalculateTax() (gas: 216449) +BillboardTest:testCannotClearAuctionIfAuctionNotEnded() (gas: 253188) +BillboardTest:testCannotClearAuctionIfClosed() (gas: 250314) +BillboardTest:testCannotClearAuctionIfNoBid() (gas: 258354) +BillboardTest:testCannotGetBlockFromEpoch() (gas: 8666) +BillboardTest:testCannotGetEpochFromBlock() (gas: 15634) +BillboardTest:testCannotPlaceBidByAttacker() (gas: 461915) +BillboardTest:testCannotPlaceBidIfAuctionEnded() (gas: 276192) +BillboardTest:testCannotPlaceBidIfClosed() (gas: 250190) +BillboardTest:testCannotSafeTransferByAttacker() (gas: 219025) +BillboardTest:testCannotSetBoardByAttacker() (gas: 227022) +BillboardTest:testCannotSetBoardByOwner() (gas: 359240) +BillboardTest:testCannotSetClosedByAttacker() (gas: 225976) +BillboardTest:testCannotSetWhitelistByAttacker() (gas: 226186) +BillboardTest:testCannotTransferByOperator() (gas: 224159) +BillboardTest:testCannotTransferToZeroAddress() (gas: 219669) +BillboardTest:testCannotUpgradeRegistryByAttacker() (gas: 9105) +BillboardTest:testCannotWithdrawBidIfAuctionNotEndedOrCleared(uint96) (runs: 256, μ: 603933, ~: 603933) +BillboardTest:testCannotWithdrawBidIfNotFound(uint96) (runs: 256, μ: 718940, ~: 718940) +BillboardTest:testCannotWithdrawBidIfWon(uint96) (runs: 256, μ: 970407, ~: 970407) +BillboardTest:testCannotWithdrawBidTwice(uint96) (runs: 256, μ: 1054869, ~: 1054869) +BillboardTest:testClearAuction(uint96) (runs: 256, μ: 700310, ~: 700310) +BillboardTest:testClearAuctions() (gas: 1254674) +BillboardTest:testGetBids(uint8,uint8,uint8) (runs: 256, μ: 4921861, ~: 2132068) +BillboardTest:testGetBlockFromEpoch() (gas: 14900) +BillboardTest:testGetEpochFromBlock() (gas: 15987) +BillboardTest:testGetTokenURI() (gas: 388974) +BillboardTest:testMintBoard() (gas: 580732) +BillboardTest:testPlaceBid(uint96) (runs: 256, μ: 784616, ~: 786949) +BillboardTest:testPlaceBidWithHigherPrice(uint96) (runs: 256, μ: 968453, ~: 968458) +BillboardTest:testPlaceBidWithSamePrices(uint96) (runs: 256, μ: 852569, ~: 856068) +BillboardTest:testPlaceBidZeroPrice() (gas: 403633) BillboardTest:testSafeTransferByOperator() (gas: 232823) -BillboardTest:testSetBidURIs() (gas: 628156) -BillboardTest:testSetBoardByCreator() (gas: 339913) -BillboardTest:testUpgradeRegistry() (gas: 3411474) -BillboardTest:testWithdrawBid(uint96) (runs: 256, μ: 1039157, ~: 1039157) -BillboardTest:testWithdrawTax(uint96) (runs: 256, μ: 702873, ~: 702873) +BillboardTest:testSetBidURIs() (gas: 630718) +BillboardTest:testSetBoardByCreator() (gas: 339694) +BillboardTest:testSetClosed() (gas: 238386) +BillboardTest:testSetWhitelist() (gas: 242793) +BillboardTest:testUpgradeRegistry() (gas: 3494596) +BillboardTest:testWithdrawBid(uint96) (runs: 256, μ: 1042812, ~: 1042812) +BillboardTest:testWithdrawBidIfClosed(uint96) (runs: 256, μ: 667875, ~: 667875) +BillboardTest:testWithdrawTax(uint96) (runs: 256, μ: 705598, ~: 705598) CurationTest:testCannotCurateERC20CurateZeroAmount() (gas: 12194) CurationTest:testCannotCurateERC20EmptyURI() (gas: 15797) CurationTest:testCannotCurateERC20IfNotApproval() (gas: 21624) diff --git a/src/Billboard/Billboard.sol b/src/Billboard/Billboard.sol index 6378945..7328a6d 100644 --- a/src/Billboard/Billboard.sol +++ b/src/Billboard/Billboard.sol @@ -16,6 +16,9 @@ contract Billboard is IBillboard { // tokenId => address => whitelisted mapping(uint256 => mapping(address => bool)) public whitelist; + // tokenId => closed + mapping(uint256 => bool) public closed; + constructor(address token_, address payable registry_, address admin_, string memory name_, string memory symbol_) { require(admin_ != address(0), "Zero address"); admin = admin_; @@ -44,6 +47,11 @@ contract Billboard is IBillboard { _; } + modifier isNotClosed(uint256 tokenId_) { + require(closed[tokenId_] != true, "Closed"); + _; + } + modifier isFromCreator(uint256 tokenId_) { IBillboardRegistry.Board memory _board = registry.getBoard(tokenId_); require(_board.creator == msg.sender, "Creator"); @@ -69,13 +77,13 @@ contract Billboard is IBillboard { ////////////////////////////// /// @inheritdoc IBillboard - function addToWhitelist(uint256 tokenId_, address account_) external isFromCreator(tokenId_) { - whitelist[tokenId_][account_] = true; + function setWhitelist(uint256 tokenId_, address account_, bool whitelisted) external isFromCreator(tokenId_) { + whitelist[tokenId_][account_] = whitelisted; } /// @inheritdoc IBillboard - function removeFromWhitelist(uint256 tokenId_, address account_) external isFromCreator(tokenId_) { - whitelist[tokenId_][account_] = false; + function setClosed(uint256 tokenId_, bool closed_) external isFromCreator(tokenId_) { + closed[tokenId_] = closed_; } ////////////////////////////// @@ -121,7 +129,11 @@ contract Billboard is IBillboard { ////////////////////////////// /// @inheritdoc IBillboard - function placeBid(uint256 tokenId_, uint256 epoch_, uint256 price_) external payable isFromWhitelist(tokenId_) { + function placeBid( + uint256 tokenId_, + uint256 epoch_, + uint256 price_ + ) external payable isNotClosed(tokenId_) isFromWhitelist(tokenId_) { _placeBid(tokenId_, epoch_, price_, "", "", false); } @@ -132,7 +144,7 @@ contract Billboard is IBillboard { uint256 price_, string calldata contentURI_, string calldata redirectURI_ - ) external payable isFromWhitelist(tokenId_) { + ) external payable isNotClosed(tokenId_) isFromWhitelist(tokenId_) { _placeBid(tokenId_, epoch_, price_, contentURI_, redirectURI_, true); } @@ -193,7 +205,7 @@ contract Billboard is IBillboard { function clearAuction( uint256 tokenId_, uint256 epoch_ - ) public returns (address highestBidder, uint256 price, uint256 tax) { + ) public isNotClosed(tokenId_) returns (address highestBidder, uint256 price, uint256 tax) { // revert if board not found IBillboardRegistry.Board memory _board = this.getBoard(tokenId_); require(_board.creator != address(0), "Board not found"); @@ -294,18 +306,20 @@ contract Billboard is IBillboard { /// @inheritdoc IBillboard function withdrawBid(uint256 tokenId_, uint256 epoch_, address bidder_) external { + bool _isClosed = closed[tokenId_]; + // revert if board not found IBillboardRegistry.Board memory _board = this.getBoard(tokenId_); require(_board.creator != address(0), "Board not found"); // revert if auction is not ended uint256 _endedAt = this.getBlockFromEpoch(_board.startedAt, epoch_ + 1, _board.epochInterval); - require(block.number >= _endedAt, "Auction not ended"); + require(_isClosed || block.number >= _endedAt, "Auction not ended"); // revert if auction is not cleared address _highestBidder = registry.highestBidder(tokenId_, epoch_); IBillboardRegistry.Bid memory _highestBid = registry.getBid(tokenId_, epoch_, _highestBidder); - require(_highestBid.isWon, "Auction not cleared"); + require(_isClosed || _highestBid.isWon, "Auction not cleared"); IBillboardRegistry.Bid memory _bid = registry.getBid(tokenId_, epoch_, bidder_); uint256 amount = _bid.price + _bid.tax; diff --git a/src/Billboard/IBillboard.sol b/src/Billboard/IBillboard.sol index 581d767..44f0ed8 100644 --- a/src/Billboard/IBillboard.sol +++ b/src/Billboard/IBillboard.sol @@ -43,20 +43,21 @@ interface IBillboard { ////////////////////////////// /** - * @notice Add address to whitelist. + * @notice Add or remove whitelist address. * * @param tokenId_ Token ID. * @param account_ Address of user will be added into whitelist. + * @param whitelisted Whitelisted or not. */ - function addToWhitelist(uint256 tokenId_, address account_) external; + function setWhitelist(uint256 tokenId_, address account_, bool whitelisted) external; /** - * @notice Remove address from whitelist. + * @notice Open or close a board. * * @param tokenId_ Token ID. - * @param account_ Address of user will be removed from whitelist. + * @param closed Closed or not. */ - function removeFromWhitelist(uint256 tokenId_, address account_) external; + function setClosed(uint256 tokenId_, bool closed) external; ////////////////////////////// /// Board diff --git a/src/test/Billboard/BillboardTest.t.sol b/src/test/Billboard/BillboardTest.t.sol index 7714f38..d6c5038 100644 --- a/src/test/Billboard/BillboardTest.t.sol +++ b/src/test/Billboard/BillboardTest.t.sol @@ -37,45 +37,51 @@ contract BillboardTest is BillboardTestBase { /// Access control ////////////////////////////// - function testAddToWhitelist() public { + function testSetWhitelist() public { (uint256 _tokenId, ) = _mintBoard(); vm.startPrank(ADMIN); - operator.addToWhitelist(_tokenId, USER_A); + // add to whitelist + operator.setWhitelist(_tokenId, USER_A, true); assertEq(operator.whitelist(_tokenId, USER_A), true); - assertEq(operator.whitelist(_tokenId, USER_B), false); + + // remove from whitelist + operator.setWhitelist(_tokenId, USER_A, false); + assertEq(operator.whitelist(_tokenId, USER_A), false); } - function testCannotAddToWhitelistByAttacker() public { + function testCannotSetWhitelistByAttacker() public { (uint256 _tokenId, ) = _mintBoard(); vm.startPrank(ATTACKER); vm.expectRevert("Creator"); - operator.addToWhitelist(_tokenId, USER_A); + operator.setWhitelist(_tokenId, USER_B, false); } - function testRemoveToWhitelist() public { + function testSetClosed() public { (uint256 _tokenId, ) = _mintBoard(); vm.startPrank(ADMIN); - operator.addToWhitelist(_tokenId, USER_A); - assertEq(operator.whitelist(_tokenId, USER_A), true); + // set closed + operator.setClosed(_tokenId, true); + assertEq(operator.closed(_tokenId), true); - operator.removeFromWhitelist(_tokenId, USER_A); - assertEq(operator.whitelist(_tokenId, USER_A), false); + // set open + operator.setClosed(_tokenId, false); + assertEq(operator.closed(_tokenId), false); } - function testCannotRemoveToWhitelistByAttacker() public { + function testCannotSetClosedByAttacker() public { (uint256 _tokenId, ) = _mintBoard(); vm.startPrank(ATTACKER); vm.expectRevert("Creator"); - operator.removeFromWhitelist(_tokenId, USER_B); + operator.setClosed(_tokenId, true); } ////////////////////////////// @@ -198,8 +204,8 @@ contract BillboardTest is BillboardTestBase { uint256 _prevRegistryBalance = usdt.balanceOf(address(registry)); vm.startPrank(ADMIN); - operator.addToWhitelist(_tokenId, USER_A); - operator.addToWhitelist(_tokenId, USER_B); + operator.setWhitelist(_tokenId, USER_A, true); + operator.setWhitelist(_tokenId, USER_B, true); vm.stopPrank(); vm.expectEmit(true, true, true, true); @@ -322,6 +328,17 @@ contract BillboardTest is BillboardTestBase { assertEq(_bid.isWon, false); } + function testCannotPlaceBidIfClosed() public { + (uint256 _tokenId, ) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(block.number, block.number, 1); + + vm.startPrank(ADMIN); + operator.setClosed(_tokenId, true); + + vm.expectRevert("Closed"); + operator.placeBid(_tokenId, _epoch, 1 ether); + } + function testCannotPlaceBidIfAuctionEnded() public { (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); @@ -330,7 +347,7 @@ contract BillboardTest is BillboardTestBase { uint256 _endedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); vm.prank(ADMIN); - operator.addToWhitelist(_tokenId, USER_A); + operator.setWhitelist(_tokenId, USER_A, true); vm.startPrank(USER_A); @@ -454,6 +471,17 @@ contract BillboardTest is BillboardTestBase { assertEq(_bid2.isWon, true); } + function testCannotClearAuctionIfClosed() public { + (uint256 _tokenId, ) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(block.number, block.number, 1); + + vm.startPrank(ADMIN); + operator.setClosed(_tokenId, true); + + vm.expectRevert("Closed"); + operator.clearAuction(_tokenId, _epoch); + } + function testCannotClearAuctionIfAuctionNotEnded() public { (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); @@ -490,7 +518,7 @@ contract BillboardTest is BillboardTestBase { address _bidder = address(uint160(2000 + i)); vm.prank(ADMIN); - operator.addToWhitelist(_tokenId, _bidder); + operator.setWhitelist(_tokenId, _bidder, true); uint256 _price = 1 ether + i; uint256 _tax = operator.calculateTax(_tokenId, _price); @@ -559,6 +587,36 @@ contract BillboardTest is BillboardTestBase { assertEq(usdt.balanceOf(USER_B), _total); } + function testWithdrawBidIfClosed(uint96 _price) public { + vm.assume(_price > 0.001 ether); + + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _tax = operator.calculateTax(_tokenId, _price); + uint256 _total = _price + _tax; + + // bid with USER_A + _placeBid(_tokenId, _epoch, USER_A, _price); + + // set closed + vm.prank(ADMIN); + operator.setClosed(_tokenId, true); + + // withdraw bid + vm.expectEmit(true, true, true, false); + emit IBillboardRegistry.BidWithdrawn(_tokenId, _epoch, USER_A); + + vm.prank(USER_A); + operator.withdrawBid(_tokenId, _epoch, USER_A); + + // check balances + assertEq(usdt.balanceOf(USER_A), _total); + + // check bid + IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _epoch, USER_A); + assertEq(_bid.isWithdrawn, true); + } + function testCannotWithdrawBidTwice(uint96 _price) public { vm.assume(_price > 0.001 ether); diff --git a/src/test/Billboard/BillboardTestBase.t.sol b/src/test/Billboard/BillboardTestBase.t.sol index c7ab1c3..ba03350 100644 --- a/src/test/Billboard/BillboardTestBase.t.sol +++ b/src/test/Billboard/BillboardTestBase.t.sol @@ -70,7 +70,7 @@ contract BillboardTestBase is Test { deal(address(usdt), _bidder, _total); vm.prank(ADMIN); - operator.addToWhitelist(_tokenId, _bidder); + operator.setWhitelist(_tokenId, _bidder, true); vm.prank(_bidder); operator.placeBid(_tokenId, _epoch, _price);