diff --git a/.env.local.example b/.env.local.example index 149539a..8627d8c 100644 --- a/.env.local.example +++ b/.env.local.example @@ -17,7 +17,6 @@ THESPACE_INCENTIVES_ADDRESS= THESPACE_INCENTIVES_TOKENS= THESPACE_LP_ADDRESS= THESPACE_LP_TOKENS= -BILLBOARD_ERC20_TOKEN= +BILLBOARD_CURRENCY_TOKEN= BILLBOARD_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000000 -BILLBOARD_LEASE_TERM= BILLBOARD_ADMIN_ADDRESS= diff --git a/.env.op-mainnet.example b/.env.op-mainnet.example index 880ce74..85cb5b5 100644 --- a/.env.op-mainnet.example +++ b/.env.op-mainnet.example @@ -18,7 +18,6 @@ THESPACE_INCENTIVES_ADDRESS= THESPACE_INCENTIVES_TOKENS= THESPACE_LP_ADDRESS= THESPACE_LP_TOKENS= -BILLBOARD_ERC20_TOKEN= +BILLBOARD_CURRENCY_TOKEN= BILLBOARD_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000000 -BILLBOARD_LEASE_TERM=900 BILLBOARD_ADMIN_ADDRESS= diff --git a/.env.op-sepolia.example b/.env.op-sepolia.example index 0db906a..09d6453 100644 --- a/.env.op-sepolia.example +++ b/.env.op-sepolia.example @@ -18,7 +18,6 @@ THESPACE_INCENTIVES_ADDRESS= THESPACE_INCENTIVES_TOKENS= THESPACE_LP_ADDRESS= THESPACE_LP_TOKENS= -BILLBOARD_ERC20_TOKEN= +BILLBOARD_CURRENCY_TOKEN= BILLBOARD_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000000 -BILLBOARD_LEASE_TERM=900 BILLBOARD_ADMIN_ADDRESS= diff --git a/.env.polygon-mainnet.example b/.env.polygon-mainnet.example index cf571ce..c30a796 100644 --- a/.env.polygon-mainnet.example +++ b/.env.polygon-mainnet.example @@ -18,7 +18,6 @@ THESPACE_INCENTIVES_ADDRESS= THESPACE_INCENTIVES_TOKENS= THESPACE_LP_ADDRESS= THESPACE_LP_TOKENS= -BILLBOARD_ERC20_TOKEN= +BILLBOARD_CURRENCY_TOKEN= BILLBOARD_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000000 -BILLBOARD_LEASE_TERM= BILLBOARD_ADMIN_ADDRESS= diff --git a/.env.polygon-mumbai.example b/.env.polygon-mumbai.example index e0bfc51..ec2d6de 100644 --- a/.env.polygon-mumbai.example +++ b/.env.polygon-mumbai.example @@ -18,7 +18,6 @@ THESPACE_INCENTIVES_ADDRESS= THESPACE_INCENTIVES_TOKENS= THESPACE_LP_ADDRESS= THESPACE_LP_TOKENS= -BILLBOARD_ERC20_TOKEN=0x0FA8781a83E46826621b3BC094Ea2A0212e71B23 +BILLBOARD_CURRENCY_TOKEN=0x0FA8781a83E46826621b3BC094Ea2A0212e71B23 BILLBOARD_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000000 -BILLBOARD_LEASE_TERM=900 BILLBOARD_ADMIN_ADDRESS= diff --git a/.gas-snapshot b/.gas-snapshot index 30eb59f..52ceb47 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -9,54 +9,57 @@ ACLManagerTest:testGrantRole() (gas: 23547) ACLManagerTest:testRenounceRole() (gas: 27841) ACLManagerTest:testRoles() (gas: 15393) ACLManagerTest:testTransferRole() (gas: 21528) -BillboardTest:testAddToWhitelist() (gas: 35114) -BillboardTest:testApproveAndTransfer() (gas: 162468) -BillboardTest:testCalculateTax() (gas: 21760) -BillboardTest:testCannnotWithdrawTaxIfSmallAmount(uint8) (runs: 256, μ: 521197, ~: 524563) -BillboardTest:testCannnotWithdrawTaxIfZero() (gas: 490384) -BillboardTest:testCannotAddToWhitelistByAttacker() (gas: 9037) -BillboardTest:testCannotApproveByAttacker() (gas: 130281) -BillboardTest:testCannotClearAuctionIfAuctionNotEnded() (gas: 700985) -BillboardTest:testCannotClearAuctionOnNewBoard() (gas: 136261) -BillboardTest:testCannotMintBoardByAttacker() (gas: 13321) -BillboardTest:testCannotPlaceBidByAttacker() (gas: 246293) -BillboardTest:testCannotPlaceBidTwice(uint96) (runs: 256, μ: 748745, ~: 754899) -BillboardTest:testCannotRemoveToWhitelistByAttacker() (gas: 9104) -BillboardTest:testCannotSafeTransferByAttacker() (gas: 127438) -BillboardTest:testCannotSetBoardProprtiesByAttacker() (gas: 157292) -BillboardTest:testCannotSetIsOpenedByAttacker() (gas: 8994) -BillboardTest:testCannotSetTaxRateByAttacker() (gas: 9006) -BillboardTest:testCannotTransferByOperator() (gas: 132771) -BillboardTest:testCannotTransferToZeroAddress() (gas: 128258) -BillboardTest:testCannotUpgradeRegistryByAttacker() (gas: 9128) -BillboardTest:testCannotWithBidTwice(uint96) (runs: 256, μ: 1079907, ~: 1079907) -BillboardTest:testCannotWithdrawBidIfAuctionNotCleared(uint96) (runs: 256, μ: 911089, ~: 911089) -BillboardTest:testCannotWithdrawBidIfAuctionNotEnded(uint96) (runs: 256, μ: 725782, ~: 725782) -BillboardTest:testCannotWithdrawBidIfNotFound() (gas: 428052) -BillboardTest:testCannotWithdrawBidIfWon(uint96) (runs: 256, μ: 834282, ~: 834282) -BillboardTest:testCannotWithdrawTaxByAttacker() (gas: 16687) -BillboardTest:testClearAuctionIfAuctionEnded(uint96) (runs: 256, μ: 837581, ~: 837581) -BillboardTest:testClearAuctionsIfAuctionEnded() (gas: 1379962) -BillboardTest:testGetBids(uint8,uint8,uint8) (runs: 256, μ: 4728587, ~: 2077366) -BillboardTest:testGetTokenURI() (gas: 154936) -BillboardTest:testMintBoard() (gas: 225541) -BillboardTest:testMintBoardByWhitelist() (gas: 154942) -BillboardTest:testMintBoardIfOpened() (gas: 145715) -BillboardTest:testPlaceBidByWhitelist() (gas: 579179) -BillboardTest:testPlaceBidIfAuctionEnded() (gas: 1090700) -BillboardTest:testPlaceBidOnNewBoard(uint96) (runs: 256, μ: 615079, ~: 635089) -BillboardTest:testPlaceBidWithHigherPrice(uint96) (runs: 256, μ: 903459, ~: 913254) -BillboardTest:testPlaceBidWithSamePrices(uint96) (runs: 256, μ: 905690, ~: 918050) -BillboardTest:testPlaceBidZeroPrice() (gas: 376947) -BillboardTest:testRemoveToWhitelist() (gas: 23188) -BillboardTest:testSafeTransferByOperator() (gas: 141193) -BillboardTest:testSetBoardProperties() (gas: 305972) -BillboardTest:testSetBoardPropertiesAfterTransfer() (gas: 335477) -BillboardTest:testSetIsOpened() (gas: 22661) -BillboardTest:testSetTaxRate() (gas: 22887) -BillboardTest:testUpgradeRegistry() (gas: 3132722) -BillboardTest:testWithdrawBid(uint96) (runs: 256, μ: 1081415, ~: 1081415) -BillboardTest:testWithdrawTax(uint96) (runs: 256, μ: 597714, ~: 597714) +BillboardTest:testApproveAndTransfer() (gas: 258132) +BillboardTest:testCalculateTax() (gas: 539822) +BillboardTest:testCannnotWithdrawTaxIfZero() (gas: 20140) +BillboardTest:testCannotApproveByAttacker() (gas: 224303) +BillboardTest:testCannotCalculateTax() (gas: 218883) +BillboardTest:testCannotClearAuctionIfAuctionNotEnded() (gas: 255576) +BillboardTest:testCannotClearAuctionIfClosed() (gas: 252825) +BillboardTest:testCannotClearAuctionIfNoBid() (gas: 260776) +BillboardTest:testCannotGetBlockFromEpoch() (gas: 8600) +BillboardTest:testCannotGetEpochFromBlock() (gas: 18393) +BillboardTest:testCannotPlaceBidIfAuctionEnded() (gas: 282966) +BillboardTest:testCannotPlaceBidIfClosed() (gas: 252745) +BillboardTest:testCannotPlaceBidIfNotWhitelisted() (gas: 465624) +BillboardTest:testCannotSafeTransferByAttacker() (gas: 221525) +BillboardTest:testCannotSetBoardByAttacker() (gas: 229589) +BillboardTest:testCannotSetBoardByOwner() (gas: 362030) +BillboardTest:testCannotSetClosedByAttacker() (gas: 228479) +BillboardTest:testCannotSetWhitelistByAttacker() (gas: 228664) +BillboardTest:testCannotTransferByOperator() (gas: 226771) +BillboardTest:testCannotTransferToZeroAddress() (gas: 222258) +BillboardTest:testCannotUpgradeRegistryByAttacker() (gas: 9017) +BillboardTest:testCannotWithdrawBidIfAuctionNotEndedOrCleared(uint96) (runs: 256, μ: 632850, ~: 632850) +BillboardTest:testCannotWithdrawBidIfNotFound(uint96) (runs: 256, μ: 750346, ~: 750346) +BillboardTest:testCannotWithdrawBidIfWon(uint96) (runs: 256, μ: 1026516, ~: 1026516) +BillboardTest:testCannotWithdrawBidTwice(uint96) (runs: 256, μ: 1111002, ~: 1111002) +BillboardTest:testClearAuction(uint96) (runs: 256, μ: 731815, ~: 731815) +BillboardTest:testClearAuctionIfAlreadyCleared() (gas: 738895) +BillboardTest:testClearAuctions() (gas: 1315317) +BillboardTest:testClearLastAuction(uint96) (runs: 256, μ: 732739, ~: 732739) +BillboardTest:testClearLastAuctions() (gas: 1332619) +BillboardTest:testGetBidderBids(uint8,uint8,uint8) (runs: 256, μ: 1508601, ~: 1140537) +BillboardTest:testGetBids(uint8,uint8,uint8) (runs: 256, μ: 8312589, ~: 6311372) +BillboardTest:testGetBlockFromEpoch() (gas: 16893) +BillboardTest:testGetEpochFromBlock() (gas: 17903) +BillboardTest:testGetTokenURI() (gas: 391345) +BillboardTest:testMintBoard() (gas: 585666) +BillboardTest:testPlaceBid(uint96) (runs: 256, μ: 840080, ~: 840702) +BillboardTest:testPlaceBidIfBoardWhitelistDisabled() (gas: 598166) +BillboardTest:testPlaceBidWithHigherPrice(uint96) (runs: 256, μ: 1011527, ~: 1011532) +BillboardTest:testPlaceBidWithSamePrices(uint96) (runs: 256, μ: 908843, ~: 909776) +BillboardTest:testPlaceBidZeroPrice() (gas: 432705) +BillboardTest:testSafeTransferByOperator() (gas: 235259) +BillboardTest:testSetBidURIs() (gas: 659834) +BillboardTest:testSetBoardByCreator() (gas: 342395) +BillboardTest:testSetBoardWhitelistDisabled() (gas: 244401) +BillboardTest:testSetClosed() (gas: 241110) +BillboardTest:testSetWhitelist() (gas: 245426) +BillboardTest:testUpgradeRegistry() (gas: 3924811) +BillboardTest:testWithdrawBid(uint96) (runs: 256, μ: 1098966, ~: 1098966) +BillboardTest:testWithdrawBidIfClosed(uint96) (runs: 256, μ: 697036, ~: 697036) +BillboardTest:testWithdrawTax(uint96) (runs: 256, μ: 737116, ~: 737116) CurationTest:testCannotCurateERC20CurateZeroAmount() (gas: 12194) CurationTest:testCannotCurateERC20EmptyURI() (gas: 15797) CurationTest:testCannotCurateERC20IfNotApproval() (gas: 21624) @@ -74,8 +77,8 @@ DistributionTest:testCannotClaimIfAlreadyClaimed() (gas: 284835) DistributionTest:testCannotClaimIfInsufficientBalance() (gas: 394264) DistributionTest:testCannotClaimIfInvalidProof() (gas: 245236) DistributionTest:testCannotClaimIfInvalidTreeId() (gas: 243332) -DistributionTest:testCannotDropIfInsufficientAllowance(uint256) (runs: 256, μ: 212266, ~: 212285) -DistributionTest:testCannotDropIfInsufficientBalance(uint256) (runs: 256, μ: 214503, ~: 214742) +DistributionTest:testCannotDropIfInsufficientAllowance(uint256) (runs: 256, μ: 212269, ~: 212284) +DistributionTest:testCannotDropIfInsufficientBalance(uint256) (runs: 256, μ: 214708, ~: 214740) DistributionTest:testCannotDropIfZeroAmount() (gas: 148793) DistributionTest:testCannotDropTwiceWithSameTreeId() (gas: 307260) DistributionTest:testCannotSetAdminByAdmin() (gas: 17334) @@ -86,15 +89,15 @@ DistributionTest:testClaim() (gas: 414576) DistributionTest:testDrop() (gas: 568791) DistributionTest:testSetAdmin() (gas: 20239) DistributionTest:testSweep() (gas: 253087) -LogbookNFTSVGTest:testTokenURI(uint8,uint8,uint16) (runs: 256, μ: 2021610, ~: 1310779) +LogbookNFTSVGTest:testTokenURI(uint8,uint8,uint16) (runs: 256, μ: 2613180, ~: 1746428) LogbookTest:testClaim() (gas: 135608) -LogbookTest:testDonate(uint96) (runs: 256, μ: 155485, ~: 156936) -LogbookTest:testDonateWithCommission(uint96,uint96) (runs: 256, μ: 150646, ~: 140444) -LogbookTest:testFork(uint96,string) (runs: 256, μ: 450121, ~: 453928) -LogbookTest:testForkRecursively(uint8,uint96) (runs: 256, μ: 4402585, ~: 1014389) -LogbookTest:testForkWithCommission(uint96,string,uint256) (runs: 256, μ: 485550, ~: 257636) +LogbookTest:testDonate(uint96) (runs: 256, μ: 156549, ~: 156936) +LogbookTest:testDonateWithCommission(uint96,uint96) (runs: 256, μ: 146644, ~: 140444) +LogbookTest:testFork(uint96,string) (runs: 256, μ: 452537, ~: 453928) +LogbookTest:testForkRecursively(uint8,uint96) (runs: 256, μ: 5351224, ~: 1801537) +LogbookTest:testForkWithCommission(uint96,string,uint256) (runs: 256, μ: 342465, ~: 257636) LogbookTest:testMulticall() (gas: 284999) -LogbookTest:testPublicSale() (gas: 204837) +LogbookTest:testPublicSale() (gas: 207337) LogbookTest:testPublish(string) (runs: 256, μ: 264065, ~: 263590) LogbookTest:testPublishEn1000() (gas: 243477) LogbookTest:testPublishEn140() (gas: 221241) @@ -114,7 +117,7 @@ LogbookTest:testPublishZh5000() (gas: 607690) LogbookTest:testSetDescription() (gas: 140760) LogbookTest:testSetForkPrice() (gas: 153925) LogbookTest:testSetTitle() (gas: 168680) -LogbookTest:testSplitRoyalty(uint8,uint8,uint96) (runs: 256, μ: 2005914, ~: 801064) +LogbookTest:testSplitRoyalty(uint8,uint8,uint96) (runs: 256, μ: 1959072, ~: 965338) LogbookTest:testWithdraw() (gas: 7284400) SnapperTest:testCannotInitRegionByNotOwner() (gas: 11365) SnapperTest:testCannotReInitRegion() (gas: 14373) @@ -124,54 +127,54 @@ SnapperTest:testCannotTakeSnapshotWrongLastBlock() (gas: 49242) SnapperTest:testCannotTakeSnapshotWrongSnapshotBlock() (gas: 23899) SnapperTest:testInitRegion(uint256) (runs: 256, μ: 114408, ~: 114408) SnapperTest:testTakeSnapshot() (gas: 47831) -TheSpaceTest:testBatchBid() (gas: 690308) -TheSpaceTest:testBatchSetPixels(uint16,uint8) (runs: 256, μ: 368737, ~: 370338) -TheSpaceTest:testBidDefaultedToken() (gas: 409416) -TheSpaceTest:testBidExistingToken() (gas: 355023) -TheSpaceTest:testBidNewToken() (gas: 301184) -TheSpaceTest:testCanTransferFromIfSettleTax() (gas: 355069) +TheSpaceTest:testBatchBid() (gas: 695308) +TheSpaceTest:testBatchSetPixels(uint16,uint8) (runs: 256, μ: 371399, ~: 372904) +TheSpaceTest:testBidDefaultedToken() (gas: 413399) +TheSpaceTest:testBidExistingToken() (gas: 360023) +TheSpaceTest:testBidNewToken() (gas: 303729) +TheSpaceTest:testCanTransferFromIfSettleTax() (gas: 357547) TheSpaceTest:testCannotBidExceedAllowance() (gas: 60910) TheSpaceTest:testCannotBidOutBoundTokens() (gas: 260482) -TheSpaceTest:testCannotBidPriceTooLow() (gas: 341674) -TheSpaceTest:testCannotGetTaxWithNonExistingToken() (gas: 16401) -TheSpaceTest:testCannotGetTokenURIInLogicContract() (gas: 298473) -TheSpaceTest:testCannotSetColorByAttacker() (gas: 302848) -TheSpaceTest:testCannotSetConfigByAttacker() (gas: 12053) -TheSpaceTest:testCannotSetPixel(uint256) (runs: 256, μ: 312357, ~: 312357) -TheSpaceTest:testCannotSetPriceByNonOwner() (gas: 302924) +TheSpaceTest:testCannotBidPriceTooLow() (gas: 344174) +TheSpaceTest:testCannotGetTaxWithNonExistingToken() (gas: 16379) +TheSpaceTest:testCannotGetTokenURIInLogicContract() (gas: 300973) +TheSpaceTest:testCannotSetColorByAttacker() (gas: 305326) +TheSpaceTest:testCannotSetConfigByAttacker() (gas: 12031) +TheSpaceTest:testCannotSetPixel(uint256) (runs: 256, μ: 314857, ~: 314857) +TheSpaceTest:testCannotSetPriceByNonOwner() (gas: 305424) TheSpaceTest:testCannotSetTokenImageURIByNonACLManager() (gas: 11862) -TheSpaceTest:testCannotSetTotalSupplyByAttacker() (gas: 11858) -TheSpaceTest:testCannotTransferFromIfDefault() (gas: 394147) -TheSpaceTest:testCannotUpgradeByAttacker() (gas: 11539) -TheSpaceTest:testCollectableTax() (gas: 333364) -TheSpaceTest:testDefault() (gas: 390575) +TheSpaceTest:testCannotSetTotalSupplyByAttacker() (gas: 11836) +TheSpaceTest:testCannotTransferFromIfDefault() (gas: 398147) +TheSpaceTest:testCannotUpgradeByAttacker() (gas: 11517) +TheSpaceTest:testCollectableTax() (gas: 335864) +TheSpaceTest:testDefault() (gas: 394548) TheSpaceTest:testGetConfig() (gas: 14302) -TheSpaceTest:testGetExistingPixel() (gas: 309428) -TheSpaceTest:testGetNonExistingPixel() (gas: 60258) +TheSpaceTest:testGetExistingPixel() (gas: 311906) +TheSpaceTest:testGetNonExistingPixel() (gas: 60303) TheSpaceTest:testGetNonExistingPrice() (gas: 19529) -TheSpaceTest:testGetOwner() (gas: 346931) +TheSpaceTest:testGetOwner() (gas: 351931) TheSpaceTest:testGetOwnerOfNonExistingToken() (gas: 13346) -TheSpaceTest:testGetPixelsByOwnerWithNoPixels() (gas: 24283) -TheSpaceTest:testGetPixelsByOwnerWithOnePixel() (gas: 319322) -TheSpaceTest:testGetPixelsPageByOwnerWithPixels() (gas: 585976) -TheSpaceTest:testGetPrice() (gas: 298001) -TheSpaceTest:testGetTax() (gas: 375416) -TheSpaceTest:testGetTokenImageURI() (gas: 14307) -TheSpaceTest:testGetTokenURI() (gas: 330962) -TheSpaceTest:testSetColor() (gas: 328848) -TheSpaceTest:testSetMintTax() (gas: 269237) -TheSpaceTest:testSetPixel(uint256) (runs: 256, μ: 398816, ~: 398816) -TheSpaceTest:testSetPrice(uint256) (runs: 256, μ: 302152, ~: 302152) -TheSpaceTest:testSetPriceByOperator(uint256) (runs: 256, μ: 352105, ~: 352105) -TheSpaceTest:testSetPriceTooHigh() (gas: 312004) -TheSpaceTest:testSetTaxRate() (gas: 345451) -TheSpaceTest:testSetTokenImageURI() (gas: 353313) -TheSpaceTest:testSetTotalSupply(uint256) (runs: 256, μ: 349701, ~: 349708) -TheSpaceTest:testSetTreasuryShare() (gas: 381788) -TheSpaceTest:testSettleTax() (gas: 336965) -TheSpaceTest:testTaxCalculation() (gas: 397405) -TheSpaceTest:testTokenShouldBeDefaulted() (gas: 323029) +TheSpaceTest:testGetPixelsByOwnerWithNoPixels() (gas: 24348) +TheSpaceTest:testGetPixelsByOwnerWithOnePixel() (gas: 321800) +TheSpaceTest:testGetPixelsPageByOwnerWithPixels() (gas: 588454) +TheSpaceTest:testGetPrice() (gas: 300501) +TheSpaceTest:testGetTax() (gas: 380480) +TheSpaceTest:testGetTokenImageURI() (gas: 14285) +TheSpaceTest:testGetTokenURI() (gas: 333462) +TheSpaceTest:testSetColor() (gas: 331348) +TheSpaceTest:testSetMintTax() (gas: 271715) +TheSpaceTest:testSetPixel(uint256) (runs: 256, μ: 403816, ~: 403816) +TheSpaceTest:testSetPrice(uint256) (runs: 256, μ: 304652, ~: 304652) +TheSpaceTest:testSetPriceByOperator(uint96) (runs: 256, μ: 354785, ~: 354785) +TheSpaceTest:testSetPriceTooHigh() (gas: 314504) +TheSpaceTest:testSetTaxRate() (gas: 347951) +TheSpaceTest:testSetTokenImageURI() (gas: 355813) +TheSpaceTest:testSetTotalSupply(uint256) (runs: 256, μ: 352202, ~: 352208) +TheSpaceTest:testSetTreasuryShare() (gas: 384288) +TheSpaceTest:testSettleTax() (gas: 339465) +TheSpaceTest:testTaxCalculation() (gas: 402405) +TheSpaceTest:testTokenShouldBeDefaulted() (gas: 325529) TheSpaceTest:testTotalSupply() (gas: 7613) TheSpaceTest:testUpgradeTo() (gas: 3215197) -TheSpaceTest:testWithdrawTreasury() (gas: 352672) -TheSpaceTest:testWithdrawUBI() (gas: 375819) \ No newline at end of file +TheSpaceTest:testWithdrawTreasury() (gas: 355172) +TheSpaceTest:testWithdrawUBI() (gas: 378319) \ No newline at end of file diff --git a/Makefile b/Makefile index cb91043..0c3d0ec 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ deploy-curation: clean ## Billboard deploy-billboard: clean - @forge create Billboard --rpc-url ${ETH_RPC_URL} --private-key ${DEPLOYER_PRIVATE_KEY} --constructor-args ${BILLBOARD_ERC20_TOKEN} ${BILLBOARD_REGISTRY_ADDRESS} ${BILLBOARD_ADMIN_ADDRESS} 504 ${BILLBOARD_LEASE_TERM} "Billboard" "BLBD" --legacy --verify --etherscan-api-key ${ETHERSCAN_API_KEY} + @forge create Billboard --rpc-url ${ETH_RPC_URL} --private-key ${DEPLOYER_PRIVATE_KEY} --constructor-args ${BILLBOARD_CURRENCY_TOKEN} ${BILLBOARD_REGISTRY_ADDRESS} ${BILLBOARD_ADMIN_ADDRESS} "Billboard" "BLBD" --legacy --verify --etherscan-api-key ${ETHERSCAN_API_KEY} deploy-billboard-distribution: clean - @forge create Distribution --rpc-url ${ETH_RPC_URL} --private-key ${DEPLOYER_PRIVATE_KEY} --constructor-args ${BILLBOARD_ERC20_TOKEN} ${BILLBOARD_ADMIN_ADDRESS} --legacy --verify --etherscan-api-key ${ETHERSCAN_API_KEY} + @forge create Distribution --rpc-url ${ETH_RPC_URL} --private-key ${DEPLOYER_PRIVATE_KEY} --constructor-args ${BILLBOARD_CURRENCY_TOKEN} ${BILLBOARD_ADMIN_ADDRESS} --legacy --verify --etherscan-api-key ${ETHERSCAN_API_KEY} diff --git a/README.md b/README.md index 4b9b47c..cc08eae 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ | Billboard (Operator) | OP Mainnet | [0x88ea16c2a69f50b9bc2e8f7684d425f33f29225f](https://optimistic.etherscan.io/address/0x88ea16c2a69f50b9bc2e8f7684d425f33f29225f) | | Billboard (Registry) | OP Mainnet | [0x031Dc7C68D4A7057D28814dCc8c61e6f83c7DF25](https://optimistic.etherscan.io/address/0x031Dc7C68D4A7057D28814dCc8c61e6f83c7DF25) | | Billboard (Distribution) | OP Mainnet | [0xad5caac6910f5a737ec53847000c13122b09eada](https://optimistic.etherscan.io/address/0xad5caac6910f5a737ec53847000c13122b09eada) | -| Billboard (Operator) | OP Sepolia | [0x6a72820E1CCCba1B1FE02E37881cEa3F9Aa6375C](https://sepolia-optimism.etherscan.io/address/0x6a72820E1CCCba1B1FE02E37881cEa3F9Aa6375C) | -| Billboard (Registry) | OP Sepolia | [0x29822FDFC36247A5C3b5E92a8E26991DC4D74a2a](https://sepolia-optimism.etherscan.io/address/0x29822FDFC36247A5C3b5E92a8E26991DC4D74a2a) | +| Billboard (Operator) | OP Sepolia | [0x2412316A9fA929CA5D476b9160Fd2688C76614Fe](https://sepolia-optimism.etherscan.io/address/0x2412316A9fA929CA5D476b9160Fd2688C76614Fe) | +| Billboard (Registry) | OP Sepolia | [0xF28e390E51ef279170E2Ccbd3Ffcd9c069A9332d](https://sepolia-optimism.etherscan.io/address/0xF28e390E51ef279170E2Ccbd3Ffcd9c069A9332d) | | Billboard (Distribution) | OP Sepolia | [0x32C838d74f8b8f49a5B27c74E71797dEd3CCE8A3](https://sepolia-optimism.etherscan.io/address/0x32C838d74f8b8f49a5B27c74E71797dEd3CCE8A3) | In the "Contract" tab of Polygonscan/Etherscan, you can see the contract code and ABI. diff --git a/src/Billboard/Billboard.sol b/src/Billboard/Billboard.sol index f620c57..5eb0a08 100644 --- a/src/Billboard/Billboard.sol +++ b/src/Billboard/Billboard.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/Base64.sol"; import "./BillboardRegistry.sol"; import "./IBillboard.sol"; @@ -11,21 +12,25 @@ contract Billboard is IBillboard { // access control BillboardRegistry public immutable registry; address public immutable admin; - mapping(address => bool) public whitelist; - bool public isOpened = false; + + // tokenId => address => whitelisted + mapping(uint256 => mapping(address => bool)) public whitelist; + + // tokenId => disabled + mapping(uint256 => bool) public isBoardWhitelistDisabled; + + // tokenId => closed + mapping(uint256 => bool) public closed; constructor( - address token_, + address currency_, address payable registry_, address admin_, - uint256 taxRate_, - uint64 leaseTerm_, string memory name_, string memory symbol_ ) { require(admin_ != address(0), "Zero address"); admin = admin_; - whitelist[admin_] = true; // deploy operator only if (registry_ != address(0)) { @@ -33,7 +38,7 @@ contract Billboard is IBillboard { } // deploy operator and registry else { - registry = new BillboardRegistry(token_, address(this), taxRate_, leaseTerm_, name_, symbol_); + registry = new BillboardRegistry(currency_, address(this), name_, symbol_); } } @@ -46,18 +51,23 @@ contract Billboard is IBillboard { _; } - modifier isFromWhitelist() { - require(whitelist[msg.sender], "Whitelist"); + modifier isFromWhitelist(uint256 tokenId_) { + require(isBoardWhitelistDisabled[tokenId_] || whitelist[tokenId_][msg.sender], "Whitelist"); _; } - modifier isFromBoardCreator(uint256 tokenId_) { + 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"); _; } - modifier isFromBoardTenant(uint256 tokenId_) { + modifier isFromTenant(uint256 tokenId_) { require(msg.sender == registry.ownerOf(tokenId_), "Tenant"); _; } @@ -76,18 +86,18 @@ contract Billboard is IBillboard { ////////////////////////////// /// @inheritdoc IBillboard - function setIsOpened(bool value_) external isFromAdmin { - isOpened = value_; + function setWhitelist(uint256 tokenId_, address account_, bool whitelisted) external isFromCreator(tokenId_) { + whitelist[tokenId_][account_] = whitelisted; } /// @inheritdoc IBillboard - function addToWhitelist(address value_) external isFromAdmin { - whitelist[value_] = true; + function setBoardWhitelistDisabled(uint256 tokenId_, bool disabled) external isFromCreator(tokenId_) { + isBoardWhitelistDisabled[tokenId_] = disabled; } /// @inheritdoc IBillboard - function removeFromWhitelist(address value_) external isFromAdmin { - whitelist[value_] = false; + function setClosed(uint256 tokenId_, bool closed_) external isFromCreator(tokenId_) { + closed[tokenId_] = closed_; } ////////////////////////////// @@ -95,253 +105,356 @@ contract Billboard is IBillboard { ////////////////////////////// /// @inheritdoc IBillboard - function mintBoard(address to_) external returns (uint256 tokenId) { - require(isOpened || whitelist[msg.sender], "Whitelist"); - - tokenId = registry.mintBoard(to_); - } - - /// @inheritdoc IBillboard - function getBoard(uint256 tokenId_) external view returns (IBillboardRegistry.Board memory board) { - return registry.getBoard(tokenId_); - } - - /// @inheritdoc IBillboard - function setBoardName(uint256 tokenId_, string calldata name_) external isFromBoardCreator(tokenId_) { - registry.setBoardName(tokenId_, name_); - } - - /// @inheritdoc IBillboard - function setBoardDescription(uint256 tokenId_, string calldata description_) external isFromBoardCreator(tokenId_) { - registry.setBoardDescription(tokenId_, description_); + function mintBoard(uint256 taxRate_, uint256 epochInterval_) external returns (uint256 tokenId) { + require(epochInterval_ > 0, "Zero epoch interval"); + tokenId = registry.newBoard(msg.sender, taxRate_, epochInterval_, block.number); + whitelist[tokenId][msg.sender] = true; } /// @inheritdoc IBillboard - function setBoardLocation(uint256 tokenId_, string calldata location_) external isFromBoardCreator(tokenId_) { - registry.setBoardLocation(tokenId_, location_); + function mintBoard( + uint256 taxRate_, + uint256 epochInterval_, + uint256 startedAt_ + ) external returns (uint256 tokenId) { + require(epochInterval_ > 0, "Zero epoch interval"); + tokenId = registry.newBoard(msg.sender, taxRate_, epochInterval_, startedAt_); + whitelist[tokenId][msg.sender] = true; } /// @inheritdoc IBillboard - function setBoardContentURI(uint256 tokenId_, string calldata contentURI_) external isFromBoardTenant(tokenId_) { - registry.setBoardContentURI(tokenId_, contentURI_); + function getBoard(uint256 tokenId_) external view returns (IBillboardRegistry.Board memory board) { + return registry.getBoard(tokenId_); } /// @inheritdoc IBillboard - function setBoardRedirectURI(uint256 tokenId_, string calldata redirectURI_) external isFromBoardTenant(tokenId_) { - registry.setBoardRedirectURI(tokenId_, redirectURI_); + function setBoard( + uint256 tokenId_, + string calldata name_, + string calldata description_, + string calldata imageURI_, + string calldata location_ + ) external isFromCreator(tokenId_) { + registry.setBoard(tokenId_, name_, description_, imageURI_, location_); } ////////////////////////////// - /// Auction + /// Auction & Bid ////////////////////////////// /// @inheritdoc IBillboard - function getAuction( + function placeBid( uint256 tokenId_, - uint256 auctionId_ - ) external view returns (IBillboardRegistry.Auction memory auction) { - auction = registry.getAuction(tokenId_, auctionId_); + uint256 epoch_, + uint256 price_ + ) external isNotClosed(tokenId_) isFromWhitelist(tokenId_) { + _placeBid(tokenId_, epoch_, price_, "", "", false); } /// @inheritdoc IBillboard - function clearAuction(uint256 tokenId_) public returns (uint256 price, uint256 tax) { - // revert if board not found + function placeBid( + uint256 tokenId_, + uint256 epoch_, + uint256 price_, + string calldata contentURI_, + string calldata redirectURI_ + ) external isNotClosed(tokenId_) isFromWhitelist(tokenId_) { + _placeBid(tokenId_, epoch_, price_, contentURI_, redirectURI_, true); + } + + function _placeBid( + uint256 tokenId_, + uint256 epoch_, + uint256 price_, + string memory contentURI_, + string memory redirectURI_, + bool hasURIs + ) private { IBillboardRegistry.Board memory _board = registry.getBoard(tokenId_); require(_board.creator != address(0), "Board not found"); - // revert if it's a new board - uint256 _nextAuctionId = registry.nextBoardAuctionId(tokenId_); - require(_nextAuctionId != 0, "Auction not found"); + uint256 _endedAt = this.getBlockFromEpoch(_board.startedAt, epoch_ + 1, _board.epochInterval); + require(block.number < _endedAt, "Auction ended"); - IBillboardRegistry.Auction memory _nextAuction = registry.getAuction(tokenId_, _nextAuctionId); + IBillboardRegistry.Bid memory _bid = registry.getBid(tokenId_, epoch_, msg.sender); - // revert if auction is still running - require(block.number >= _nextAuction.endAt, "Auction not ended"); + uint256 _tax = calculateTax(tokenId_, price_); - // reclaim ownership to board creator if no auction - address _prevOwner = registry.ownerOf(tokenId_); - if (_nextAuction.startAt == 0 && _prevOwner != _board.creator) { - registry.safeTransferByOperator(_prevOwner, _board.creator, tokenId_); - return (0, 0); + // create new bid if no bid exists + if (_bid.placedAt == 0) { + // transfer bid price and tax to the registry + SafeERC20.safeTransferFrom(registry.currency(), msg.sender, address(registry), price_ + _tax); + + // add new bid + registry.newBid(tokenId_, epoch_, msg.sender, price_, _tax, contentURI_, redirectURI_); } + // update bid if exists + else { + require(price_ > _bid.price, "Price too low"); + + // transfer diff amount to the registry + uint256 _priceDiff = price_ - _bid.price; + uint256 _taxDiff = _tax - _bid.tax; + SafeERC20.safeTransferFrom(registry.currency(), msg.sender, address(registry), _priceDiff + _taxDiff); + + if (hasURIs) { + registry.setBid(tokenId_, epoch_, msg.sender, price_, _tax, contentURI_, redirectURI_, true); + } else { + registry.setBid(tokenId_, epoch_, msg.sender, price_, _tax, "", "", false); + } + } + } - return _clearAuction(tokenId_, _board.creator, _nextAuctionId); + /// @inheritdoc IBillboard + function setBidURIs( + uint256 tokenId_, + uint256 epoch_, + string calldata contentURI_, + string calldata redirectURI_ + ) public { + registry.setBidURIs(tokenId_, epoch_, msg.sender, contentURI_, redirectURI_); } /// @inheritdoc IBillboard - function clearAuctions( - uint256[] calldata tokenIds_ - ) external returns (uint256[] memory prices, uint256[] memory taxes) { - uint256 _size = tokenIds_.length; - uint256[] memory _prices = new uint256[](_size); - uint256[] memory _taxes = new uint256[](_size); + function clearAuction( + uint256 tokenId_, + uint256 epoch_ + ) 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"); - for (uint256 i = 0; i < _size; i++) { - (_prices[i], _taxes[i]) = clearAuction(tokenIds_[i]); - } + // revert if auction is still running + uint256 _endedAt = this.getBlockFromEpoch(_board.startedAt, epoch_ + 1, _board.epochInterval); + require(block.number >= _endedAt, "Auction not ended"); - return (_prices, _taxes); - } + address _highestBidder = registry.highestBidder(tokenId_, epoch_); + IBillboardRegistry.Bid memory _highestBid = registry.getBid(tokenId_, epoch_, _highestBidder); - function _clearAuction( - uint256 tokenId_, - address boardCreator_, - uint256 nextAuctionId_ - ) private returns (uint256 price, uint256 tax) { - IBillboardRegistry.Auction memory _nextAuction = registry.getAuction(tokenId_, nextAuctionId_); + // revert if no bid + require(_highestBid.placedAt != 0, "No bid"); // skip if auction is already cleared - if (_nextAuction.leaseEndAt != 0) { - return (0, 0); + if (_highestBid.isWon) { + return (_highestBidder, _highestBid.price, _highestBid.tax); } address _prevOwner = registry.ownerOf(tokenId_); - IBillboardRegistry.Bid memory _highestBid = registry.getBid( - tokenId_, - nextAuctionId_, - _nextAuction.highestBidder - ); - if (_highestBid.price > 0) { // transfer bid price to board owner (previous tenant or creator) - registry.transferAmount(_prevOwner, _highestBid.price); + registry.transferCurrencyByOperator(_prevOwner, _highestBid.price); // transfer bid tax to board creator's tax treasury - (uint256 _taxAccumulated, uint256 _taxWithdrawn) = registry.taxTreasury(boardCreator_); - registry.setTaxTreasury(boardCreator_, _taxAccumulated + _highestBid.tax, _taxWithdrawn); + (uint256 _taxAccumulated, uint256 _taxWithdrawn) = registry.taxTreasury(_board.creator); + registry.setTaxTreasury(_board.creator, _taxAccumulated + _highestBid.tax, _taxWithdrawn); } // transfer ownership - registry.safeTransferByOperator(_prevOwner, _nextAuction.highestBidder, tokenId_); + registry.safeTransferByOperator(_prevOwner, _highestBidder, tokenId_); // mark highest bid as won - registry.setBidWon(tokenId_, nextAuctionId_, _nextAuction.highestBidder, true); - - // set auction lease - uint64 leaseStartAt = uint64(block.number); - uint64 leaseEndAt = uint64(leaseStartAt + registry.leaseTerm()); - registry.setAuctionLease(tokenId_, nextAuctionId_, leaseStartAt, leaseEndAt); + registry.setBidWon(tokenId_, epoch_, _highestBidder, true); // emit AuctionCleared - registry.emitAuctionCleared(tokenId_, nextAuctionId_, _nextAuction.highestBidder, leaseStartAt, leaseEndAt); + registry.emitAuctionCleared(tokenId_, epoch_, _highestBidder); - return (_highestBid.price, _highestBid.tax); + return (_highestBidder, _highestBid.price, _highestBid.tax); } /// @inheritdoc IBillboard - function placeBid(uint256 tokenId_, uint256 amount_) external payable isFromWhitelist { - IBillboardRegistry.Board memory _board = registry.getBoard(tokenId_); - require(_board.creator != address(0), "Board not found"); - - uint256 _nextAuctionId = registry.nextBoardAuctionId(tokenId_); - IBillboardRegistry.Auction memory _nextAuction = registry.getAuction(tokenId_, _nextAuctionId); - - // if it's a new board without next auction, - // create new auction and new bid first, - // then clear auction and transfer ownership to the bidder immediately. - if (_nextAuction.startAt == 0) { - uint256 _auctionId = _newAuctionAndBid(tokenId_, amount_, uint64(block.number)); - _clearAuction(tokenId_, _board.creator, _auctionId); - return; - } + function clearAuctions( + uint256[] calldata tokenIds_, + uint256[] calldata epochs_ + ) external returns (address[] memory highestBidders, uint256[] memory prices, uint256[] memory taxes) { + uint256 _size = tokenIds_.length; + address[] memory _highestBidders = new address[](_size); + uint256[] memory _prices = new uint256[](_size); + uint256[] memory _taxes = new uint256[](_size); - // if next auction is ended, - // clear auction first, - // then create new auction and new bid - if (block.number >= _nextAuction.endAt) { - _clearAuction(tokenId_, _board.creator, _nextAuctionId); - _newAuctionAndBid(tokenId_, amount_, uint64(block.number + registry.leaseTerm())); - return; + for (uint256 i = 0; i < _size; i++) { + (_highestBidders[i], _prices[i], _taxes[i]) = clearAuction(tokenIds_[i], epochs_[i]); } - // if next auction is not ended, - // push new bid to next auction - else { - require(registry.getBid(tokenId_, _nextAuctionId, msg.sender).placedAt == 0, "Bid already placed"); - uint256 _tax = calculateTax(amount_); - registry.newBid(tokenId_, _nextAuctionId, msg.sender, amount_, _tax); - - _lockBidPriceAndTax(amount_ + _tax); - } + return (_highestBidders, _prices, _taxes); } - function _newAuctionAndBid(uint256 tokenId_, uint256 amount_, uint64 endAt_) private returns (uint256 auctionId) { - uint64 _startAt = uint64(block.number); - uint256 _tax = calculateTax(amount_); - - auctionId = registry.newAuction(tokenId_, _startAt, endAt_); + /// @inheritdoc IBillboard + function clearLastAuction(uint256 tokenId_) external returns (address highestBidder, uint256 price, uint256 tax) { + uint256 _lastEpoch = getLatestEpoch(tokenId_) - 1; + return clearAuction(tokenId_, _lastEpoch); + } - registry.newBid(tokenId_, auctionId, msg.sender, amount_, _tax); + /// @inheritdoc IBillboard + function clearLastAuctions( + uint256[] calldata tokenIds_ + ) external returns (address[] memory highestBidders, uint256[] memory prices, uint256[] memory taxes) { + uint256 _size = tokenIds_.length; + address[] memory _highestBidders = new address[](_size); + uint256[] memory _prices = new uint256[](_size); + uint256[] memory _taxes = new uint256[](_size); - _lockBidPriceAndTax(amount_ + _tax); - } + for (uint256 i = 0; i < _size; i++) { + (_highestBidders[i], _prices[i], _taxes[i]) = this.clearLastAuction(tokenIds_[i]); + } - function _lockBidPriceAndTax(uint256 amount_) private { - // transfer bid price and tax to the registry - SafeERC20.safeTransferFrom(registry.token(), msg.sender, address(registry), amount_); + return (_highestBidders, _prices, _taxes); } /// @inheritdoc IBillboard function getBid( uint256 tokenId_, - uint256 auctionId_, + uint256 epoch_, address bidder_ ) external view returns (IBillboardRegistry.Bid memory bid) { - return registry.getBid(tokenId_, auctionId_, bidder_); + return registry.getBid(tokenId_, epoch_, bidder_); } /// @inheritdoc IBillboard function getBids( uint256 tokenId_, - uint256 auctionId_, + uint256 epoch_, uint256 limit_, uint256 offset_ ) external view returns (uint256 total, uint256 limit, uint256 offset, IBillboardRegistry.Bid[] memory bids) { - uint256 _total = registry.getBidCount(tokenId_, auctionId_); + uint256 _total = registry.getBidCount(tokenId_, epoch_); - if (limit_ == 0) { + if (limit_ == 0 || offset_ >= _total) { return (_total, limit_, offset_, new IBillboardRegistry.Bid[](0)); } - if (offset_ >= _total) { - return (_total, limit_, offset_, new IBillboardRegistry.Bid[](0)); + uint256 _left = _total - offset_; + uint256 _size = _left > limit_ ? limit_ : _left; + + bids = new IBillboardRegistry.Bid[](_size); + + for (uint256 i = 0; i < _size; i++) { + address _bidder = registry.bidders(tokenId_, epoch_, offset_ + i); + bids[i] = registry.getBid(tokenId_, epoch_, _bidder); + } + + return (_total, limit_, offset_, bids); + } + + /// @inheritdoc IBillboard + function getBidderBids( + uint256 tokenId_, + address bidder_, + uint256 limit_, + uint256 offset_ + ) + external + view + returns ( + uint256 total, + uint256 limit, + uint256 offset, + IBillboardRegistry.Bid[] memory bids, + uint256[] memory epochs + ) + { + uint256 _total = registry.getBidderBidCount(tokenId_, bidder_); + + if (limit_ == 0 || offset_ >= _total) { + return (_total, limit_, offset_, new IBillboardRegistry.Bid[](0), new uint256[](0)); } uint256 _left = _total - offset_; uint256 _size = _left > limit_ ? limit_ : _left; - IBillboardRegistry.Bid[] memory _bids = new IBillboardRegistry.Bid[](_size); + (bids, epochs) = _getBidsAndEpochs(tokenId_, bidder_, offset_, _size); - for (uint256 i = 0; i < _size; i++) { - address _bidder = registry.auctionBidders(tokenId_, auctionId_, offset_ + i); - _bids[i] = registry.getBid(tokenId_, auctionId_, _bidder); + return (_total, limit_, offset_, bids, epochs); + } + + function _getBidsAndEpochs( + uint256 tokenId_, + address bidder_, + uint256 offset_, + uint256 size_ + ) internal view returns (IBillboardRegistry.Bid[] memory bids, uint256[] memory epochs) { + bids = new IBillboardRegistry.Bid[](size_); + epochs = new uint256[](size_); + + for (uint256 i = 0; i < size_; i++) { + uint256 _epoch = registry.bidderBids(tokenId_, bidder_, offset_ + i); + bids[i] = registry.getBid(tokenId_, _epoch, bidder_); + epochs[i] = _epoch; } + } + + /// @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(_isClosed || block.number >= _endedAt, "Auction not ended"); - return (_total, limit_, offset_, _bids); + // revert if auction is not cleared + address _highestBidder = registry.highestBidder(tokenId_, epoch_); + IBillboardRegistry.Bid memory _highestBid = registry.getBid(tokenId_, epoch_, _highestBidder); + require(_isClosed || _highestBid.isWon, "Auction not cleared"); + + IBillboardRegistry.Bid memory _bid = registry.getBid(tokenId_, epoch_, bidder_); + uint256 amount = _bid.price + _bid.tax; + + require(_bid.placedAt != 0, "Bid not found"); + require(!_bid.isWithdrawn, "Bid already withdrawn"); + require(!_bid.isWon, "Bid already won"); + require(amount > 0, "Zero amount"); + + // set bid.isWithdrawn to true first to prevent reentrancy + registry.setBidWithdrawn(tokenId_, epoch_, bidder_, true); + + // transfer bid price and tax back to the bidder + registry.transferCurrencyByOperator(bidder_, amount); } - ////////////////////////////// - /// Tax & Withdraw - ////////////////////////////// + /// @inheritdoc IBillboard + function getEpochFromBlock( + uint256 startedAt_, + uint256 block_, + uint256 epochInterval_ + ) public pure returns (uint256 epoch) { + return (block_ - startedAt_) / epochInterval_; + } /// @inheritdoc IBillboard - function getTaxRate() external view returns (uint256 taxRate) { - return registry.taxRate(); + function getBlockFromEpoch( + uint256 startedAt_, + uint256 epoch_, + uint256 epochInterval_ + ) public pure returns (uint256 blockNumber) { + return startedAt_ + (epoch_ * epochInterval_); + } + + /// @inheritdoc IBillboard + function getLatestEpoch(uint256 tokenId_) public view returns (uint256 epoch) { + IBillboardRegistry.Board memory _board = registry.getBoard(tokenId_); + return this.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); } + ////////////////////////////// + /// Tax & Withdraw + ////////////////////////////// + /// @inheritdoc IBillboard - function setTaxRate(uint256 taxRate_) external isFromAdmin { - registry.setTaxRate(taxRate_); + function getTaxRate(uint256 tokenId_) external view returns (uint256 taxRate) { + return registry.getBoard(tokenId_).taxRate; } - function calculateTax(uint256 amount_) public view returns (uint256 tax) { - tax = (amount_ * registry.taxRate()) / 1000; + function calculateTax(uint256 tokenId_, uint256 amount_) public view returns (uint256 tax) { + tax = (amount_ * this.getTaxRate(tokenId_)) / 1000; } /// @inheritdoc IBillboard - function withdrawTax() external returns (uint256 tax) { - (uint256 _taxAccumulated, uint256 _taxWithdrawn) = registry.taxTreasury(msg.sender); + function withdrawTax(address creator_) external returns (uint256 tax) { + (uint256 _taxAccumulated, uint256 _taxWithdrawn) = registry.taxTreasury(creator_); uint256 amount = _taxAccumulated - _taxWithdrawn; @@ -349,41 +462,48 @@ contract Billboard is IBillboard { // set taxTreasury.withdrawn to taxTreasury.accumulated first // to prevent reentrancy - registry.setTaxTreasury(msg.sender, _taxAccumulated, _taxAccumulated); + registry.setTaxTreasury(creator_, _taxAccumulated, _taxAccumulated); // transfer tax to the owner - registry.transferAmount(msg.sender, amount); + registry.transferCurrencyByOperator(creator_, amount); // emit TaxWithdrawn - registry.emitTaxWithdrawn(msg.sender, amount); + registry.emitTaxWithdrawn(creator_, amount); return amount; } - /// @inheritdoc IBillboard - function withdrawBid(uint256 tokenId_, uint256 auctionId_) external { - // revert if auction is still running - IBillboardRegistry.Auction memory _auction = registry.getAuction(tokenId_, auctionId_); - require(block.number >= _auction.endAt, "Auction not ended"); - - // revert if auction is not cleared - require(_auction.leaseEndAt != 0, "Auction not cleared"); - - IBillboardRegistry.Bid memory _bid = registry.getBid(tokenId_, auctionId_, msg.sender); - uint256 amount = _bid.price + _bid.tax; + ////////////////////////////// + /// ERC721 related + ////////////////////////////// - require(_bid.placedAt != 0, "Bid not found"); - require(!_bid.isWithdrawn, "Bid already withdrawn"); - require(!_bid.isWon, "Bid already won"); - require(amount > 0, "Zero amount"); + /// @inheritdoc IBillboard + function _tokenURI(uint256 tokenId_) external view returns (string memory uri) { + require(msg.sender == address(registry), "Unauthorized"); + require(registry.exists(tokenId_), "Token not found"); - // set bid.isWithdrawn to true first to prevent reentrancy - registry.setBidWithdrawn(tokenId_, auctionId_, msg.sender, true); + IBillboardRegistry.Board memory _board = registry.getBoard(tokenId_); - // transfer bid price and tax back to the bidder - registry.transferAmount(msg.sender, amount); + string memory tokenName = string(abi.encodePacked(registry.name(), " #", Strings.toString(tokenId_))); + + string memory json = Base64.encode( + bytes( + string( + abi.encodePacked( + '{"name": "', + tokenName, + '", "description": "', + _board.description, + '", "location": "', + _board.location, + '", "image": "', + _board.imageURI, + '"}' + ) + ) + ) + ); - // emit BidWithdrawn - registry.emitBidWithdrawn(tokenId_, auctionId_, msg.sender, _bid.price, _bid.tax); + uri = string(abi.encodePacked("data:application/json;base64,", json)); } } diff --git a/src/Billboard/BillboardRegistry.sol b/src/Billboard/BillboardRegistry.sol index ce48fb5..5b3c6d2 100644 --- a/src/Billboard/BillboardRegistry.sol +++ b/src/Billboard/BillboardRegistry.sol @@ -5,59 +5,54 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; +import "./IBillboard.sol"; import "./IBillboardRegistry.sol"; contract BillboardRegistry is IBillboardRegistry, ERC721 { using Counters for Counters.Counter; + Counters.Counter public lastTokenId; - // access control address public operator; - Counters.Counter public lastTokenId; - - IERC20 public immutable token; - uint256 public taxRate; - uint64 public leaseTerm; + // currency to be used for auction + IERC20 public immutable currency; // tokenId => Board mapping(uint256 => Board) public boards; - // tokenId => auctionId => Auction - mapping(uint256 => mapping(uint256 => Auction)) public boardAuctions; + // tokenId => epoch => bidder + mapping(uint256 => mapping(uint256 => address)) public highestBidder; - // tokenId => nextAuctionId (start from 1 if exists) - mapping(uint256 => uint256) public nextBoardAuctionId; + // tokenId => epoch => bidders + mapping(uint256 => mapping(uint256 => address[])) public bidders; - // tokenId => auctionId => bidders - mapping(uint256 => mapping(uint256 => address[])) public auctionBidders; + // tokenId => epoch => bidder => Bid + mapping(uint256 => mapping(uint256 => mapping(address => Bid))) public bids; - // tokenId => auctionId => bidder => Bid - mapping(uint256 => mapping(uint256 => mapping(address => Bid))) public auctionBids; + // tokenId => address => epoches + mapping(uint256 => mapping(address => uint256[])) public bidderBids; // board creator => TaxTreasury mapping(address => TaxTreasury) public taxTreasury; + ////////////////////////////// + /// Constructor + ////////////////////////////// constructor( - address token_, + address currency_, address operator_, - uint256 taxRate_, - uint64 leaseTerm_, string memory name_, string memory symbol_ ) ERC721(name_, symbol_) { require(operator_ != address(0), "Zero address"); - require(token_ != address(0), "Zero address"); - - token = IERC20(token_); + require(currency_ != address(0), "Zero address"); operator = operator_; - taxRate = taxRate_; - leaseTerm = leaseTerm_; + currency = IERC20(currency_); } ////////////////////////////// /// Modifier ////////////////////////////// - modifier isFromOperator() { require(msg.sender == operator, "Operator"); _; @@ -80,7 +75,17 @@ contract BillboardRegistry is IBillboardRegistry, ERC721 { ////////////////////////////// /// @inheritdoc IBillboardRegistry - function mintBoard(address to_) external isFromOperator returns (uint256 tokenId) { + function getBoard(uint256 tokenId_) external view returns (Board memory board) { + board = boards[tokenId_]; + } + + /// @inheritdoc IBillboardRegistry + function newBoard( + address to_, + uint256 taxRate_, + uint256 epochInterval_, + uint256 startedAt_ + ) external isFromOperator returns (uint256 tokenId) { lastTokenId.increment(); tokenId = lastTokenId.current(); @@ -90,166 +95,163 @@ contract BillboardRegistry is IBillboardRegistry, ERC721 { creator: to_, name: "", description: "", + imageURI: "", location: "", - contentURI: "", - redirectURI: "" + taxRate: taxRate_, + epochInterval: epochInterval_, + startedAt: startedAt_ }); - } - /// @inheritdoc IBillboardRegistry - function safeTransferByOperator(address from_, address to_, uint256 tokenId_) external isFromOperator { - _safeTransfer(from_, to_, tokenId_, ""); - } - - /// @inheritdoc IBillboardRegistry - function getBoard(uint256 tokenId_) external view returns (Board memory board) { - board = boards[tokenId_]; + emit BoardCreated(tokenId, to_, taxRate_, epochInterval_); } /// @inheritdoc IBillboardRegistry - function setBoardName(uint256 tokenId_, string calldata name_) external isFromOperator { + function setBoard( + uint256 tokenId_, + string calldata name_, + string calldata description_, + string calldata imageURI_, + string calldata location_ + ) external isFromOperator { boards[tokenId_].name = name_; - emit BoardNameUpdated(tokenId_, name_); - } - - /// @inheritdoc IBillboardRegistry - function setBoardDescription(uint256 tokenId_, string calldata description_) external isFromOperator { boards[tokenId_].description = description_; - emit BoardDescriptionUpdated(tokenId_, description_); - } - - /// @inheritdoc IBillboardRegistry - function setBoardLocation(uint256 tokenId_, string calldata location_) external isFromOperator { + boards[tokenId_].imageURI = imageURI_; boards[tokenId_].location = location_; - emit BoardLocationUpdated(tokenId_, location_); + emit BoardUpdated(tokenId_, name_, description_, imageURI_, location_); } + ////////////////////////////// + /// Auction & Bid + ////////////////////////////// + /// @inheritdoc IBillboardRegistry - function setBoardContentURI(uint256 tokenId_, string calldata contentURI_) external isFromOperator { - boards[tokenId_].contentURI = contentURI_; - emit BoardContentURIUpdated(tokenId_, contentURI_); + function getBid(uint256 tokenId_, uint256 auctionId_, address bidder_) external view returns (Bid memory bid) { + bid = bids[tokenId_][auctionId_][bidder_]; } - function setBoardRedirectURI(uint256 tokenId_, string calldata redirectURI_) external isFromOperator { - boards[tokenId_].redirectURI = redirectURI_; - emit BoardRedirectURIUpdated(tokenId_, redirectURI_); + /// @inheritdoc IBillboardRegistry + function getBidCount(uint256 tokenId_, uint256 epoch_) external view returns (uint256 count) { + count = bidders[tokenId_][epoch_].length; } - ////////////////////////////// - /// Auction - ////////////////////////////// - /// @inheritdoc IBillboardRegistry - function getAuction(uint256 tokenId_, uint256 auctionId_) external view returns (Auction memory auction) { - auction = boardAuctions[tokenId_][auctionId_]; + function getBidderBidCount(uint256 tokenId_, address bidder_) external view returns (uint256 count) { + count = bidderBids[tokenId_][bidder_].length; } /// @inheritdoc IBillboardRegistry - function newAuction( + function newBid( uint256 tokenId_, - uint64 startAt_, - uint64 endAt_ - ) external isFromOperator returns (uint256 newAuctionId) { - nextBoardAuctionId[tokenId_]++; - - newAuctionId = nextBoardAuctionId[tokenId_]; - - boardAuctions[tokenId_][newAuctionId] = Auction({ - startAt: startAt_, - endAt: endAt_, - leaseStartAt: 0, - leaseEndAt: 0, - highestBidder: address(0) + uint256 epoch_, + address bidder_, + uint256 price_, + uint256 tax_, + string calldata contentURI_, + string calldata redirectURI_ + ) external isFromOperator { + Bid memory _bid = Bid({ + price: price_, + tax: tax_, + contentURI: contentURI_, + redirectURI: redirectURI_, + placedAt: block.number, + updatedAt: block.number, + isWithdrawn: false, + isWon: false }); - emit AuctionCreated(tokenId_, newAuctionId, startAt_, endAt_); + // add to auction bids + bids[tokenId_][epoch_][bidder_] = _bid; + + // add to bidder's bids + bidderBids[tokenId_][bidder_].push(epoch_); + + // add to auction bidders if new bid + bidders[tokenId_][epoch_].push(bidder_); + + _sethighestBidder(tokenId_, epoch_, price_, bidder_); + + emit BidUpdated(tokenId_, epoch_, bidder_, price_, tax_, contentURI_, redirectURI_); } /// @inheritdoc IBillboardRegistry - function setAuctionLease( + function setBid( uint256 tokenId_, - uint256 auctionId_, - uint64 leaseStartAt_, - uint64 leaseEndAt_ + uint256 epoch_, + address bidder_, + uint256 price_, + uint256 tax_, + string calldata contentURI_, + string calldata redirectURI_, + bool hasURIs ) external isFromOperator { - boardAuctions[tokenId_][auctionId_].leaseStartAt = leaseStartAt_; - boardAuctions[tokenId_][auctionId_].leaseEndAt = leaseEndAt_; - } + Bid storage _bid = bids[tokenId_][epoch_][bidder_]; + require(_bid.placedAt != 0, "Bid not found"); - /// @inheritdoc IBillboardRegistry - function getBidCount(uint256 tokenId_, uint256 auctionId_) external view returns (uint256 count) { - count = auctionBidders[tokenId_][auctionId_].length; + _bid.price = price_; + _bid.tax = tax_; + _bid.updatedAt = block.number; + + if (hasURIs) { + _bid.contentURI = contentURI_; + _bid.redirectURI = redirectURI_; + } + + _sethighestBidder(tokenId_, epoch_, price_, bidder_); + + emit BidUpdated(tokenId_, epoch_, bidder_, price_, tax_, contentURI_, redirectURI_); } - /// @inheritdoc IBillboardRegistry - function getBid(uint256 tokenId_, uint256 auctionId_, address bidder_) external view returns (Bid memory bid) { - bid = auctionBids[tokenId_][auctionId_][bidder_]; + // Set auction highest bidder if no highest bidder or price is higher. + // + // Note: for same price, the first bidder will always be + // the highest bidder since the block.number is always greater. + function _sethighestBidder(uint256 tokenId_, uint256 epoch_, uint256 price_, address bidder_) internal { + address _highestBidder = highestBidder[tokenId_][epoch_]; + Bid storage highestBid = bids[tokenId_][epoch_][_highestBidder]; + if (_highestBidder == address(0) || price_ > highestBid.price) { + highestBidder[tokenId_][epoch_] = bidder_; + } } /// @inheritdoc IBillboardRegistry - function newBid( + function setBidURIs( uint256 tokenId_, - uint256 auctionId_, + uint256 epoch_, address bidder_, - uint256 price_, - uint256 tax_ + string calldata contentURI_, + string calldata redirectURI_ ) external isFromOperator { - Bid memory _bid = Bid({price: price_, tax: tax_, placedAt: block.number, isWithdrawn: false, isWon: false}); + Bid storage _bid = bids[tokenId_][epoch_][bidder_]; + require(_bid.placedAt != 0, "Bid not found"); - // add to auction bids - auctionBids[tokenId_][auctionId_][bidder_] = _bid; - - // add to auction bidders - auctionBidders[tokenId_][auctionId_].push(bidder_); - - // set auction highest bidder if no highest bidder or price is higher. - // - // Note: for same price, the first bidder will always be - // the highest bidder since the block.number is always greater. - address highestBidder = boardAuctions[tokenId_][auctionId_].highestBidder; - Bid memory highestBid = auctionBids[tokenId_][auctionId_][highestBidder]; - if (highestBidder == address(0) || price_ > highestBid.price) { - boardAuctions[tokenId_][auctionId_].highestBidder = bidder_; - } + _bid.contentURI = contentURI_; + _bid.redirectURI = redirectURI_; - emit BidCreated(tokenId_, auctionId_, bidder_, price_, tax_); + emit BidUpdated(tokenId_, epoch_, bidder_, _bid.price, _bid.tax, contentURI_, redirectURI_); } /// @inheritdoc IBillboardRegistry - function setBidWon(uint256 tokenId_, uint256 auctionId_, address bidder_, bool isWon_) external isFromOperator { - auctionBids[tokenId_][auctionId_][bidder_].isWon = isWon_; - - emit BidWon(tokenId_, auctionId_, bidder_); + function setBidWon(uint256 tokenId_, uint256 epoch_, address bidder_, bool isWon_) external isFromOperator { + bids[tokenId_][epoch_][bidder_].isWon = isWon_; + emit BidWon(tokenId_, epoch_, bidder_); } /// @inheritdoc IBillboardRegistry function setBidWithdrawn( uint256 tokenId_, - uint256 auctionId_, + uint256 epoch_, address bidder_, bool isWithdrawn_ ) external isFromOperator { - auctionBids[tokenId_][auctionId_][bidder_].isWithdrawn = isWithdrawn_; - } - - /// @inheritdoc IBillboardRegistry - function transferAmount(address to_, uint256 amount_) external isFromOperator { - require(to_ != address(0), "Zero address"); - - require(token.transfer(to_, amount_), "Failed token transfer"); + bids[tokenId_][epoch_][bidder_].isWithdrawn = isWithdrawn_; + emit BidWithdrawn(tokenId_, epoch_, bidder_); } ////////////////////////////// /// Tax & Withdraw ////////////////////////////// - /// @inheritdoc IBillboardRegistry - function setTaxRate(uint256 taxRate_) external isFromOperator { - taxRate = taxRate_; - - emit TaxRateUpdated(taxRate_); - } - /// @inheritdoc IBillboardRegistry function setTaxTreasury(address owner_, uint256 accumulated_, uint256 withdrawn_) external isFromOperator { taxTreasury[owner_].accumulated = accumulated_; @@ -257,14 +259,30 @@ contract BillboardRegistry is IBillboardRegistry, ERC721 { } ////////////////////////////// - /// ERC721 Overrides + /// ERC20 & ERC721 related ////////////////////////////// + /// @inheritdoc IBillboardRegistry + function safeTransferByOperator(address from_, address to_, uint256 tokenId_) external isFromOperator { + _safeTransfer(from_, to_, tokenId_, ""); + } + + /// @inheritdoc IBillboardRegistry + function transferCurrencyByOperator(address to_, uint256 amount_) external isFromOperator { + require(to_ != address(0), "Zero address"); + require(currency.transfer(to_, amount_), "Failed token transfer"); + } + + /// @inheritdoc IBillboardRegistry + function exists(uint256 tokenId_) external view returns (bool) { + return _exists(tokenId_); + } + /** * @notice See {IERC721-tokenURI}. */ function tokenURI(uint256 tokenId_) public view override(ERC721) returns (string memory uri) { - return boards[tokenId_].contentURI; + uri = IBillboard(operator)._tokenURI(tokenId_); } /** @@ -279,25 +297,8 @@ contract BillboardRegistry is IBillboardRegistry, ERC721 { ////////////////////////////// /// @inheritdoc IBillboardRegistry - function emitAuctionCleared( - uint256 tokenId_, - uint256 auctionId_, - address highestBidder_, - uint64 leaseStartAt_, - uint64 leaseEndAt_ - ) external { - emit AuctionCleared(tokenId_, auctionId_, highestBidder_, leaseStartAt_, leaseEndAt_); - } - - /// @inheritdoc IBillboardRegistry - function emitBidWithdrawn( - uint256 tokenId_, - uint256 auctionId_, - address bidder_, - uint256 price_, - uint256 tax_ - ) external { - emit BidWithdrawn(tokenId_, auctionId_, bidder_, price_, tax_); + function emitAuctionCleared(uint256 tokenId_, uint256 epoch_, address highestBidder_) external { + emit AuctionCleared(tokenId_, epoch_, highestBidder_); } /// @inheritdoc IBillboardRegistry diff --git a/src/Billboard/IBillboard.sol b/src/Billboard/IBillboard.sol index 18e3ffc..8ac161f 100644 --- a/src/Billboard/IBillboard.sol +++ b/src/Billboard/IBillboard.sol @@ -8,19 +8,20 @@ import "./IBillboardRegistry.sol"; * @notice The on-chain billboard system transforms platform attention into NFT billboards based on Harberger tax auctions. Empowering creators with a fair share of tax revenue through quadratic voting. * * ## Billboard - * - User (whitelisted) can mint a billboard: call `mintBoard`. - * - Owner of a billboard can set the AD data of a billboard: call `setBoardName`, `setBoardDescription` and `setBoardLocation`. - * - Tenant of a billboard can set the AD data of a billboard: call `setBoardContentURI` and `setBoardRedirectURI`. + * - User can mint a billboard: `mintBoard`. + * - Creator, who mints the billboard, can set the metadata: `setBoard`. + * - Tenant, who wins the auction, can set the AD data: `setBidURIs`. * - * ## Auction - * - User needs to call `approve` on currency (USDT) contract before starting. - * - User can place a bid on a billboard: call `placeBid`. - * - User can clear auction on a billboard: call `clearAuction`. - * - User can withdraw bid from a billboard: call `withdrawBid`. + * ## Auction & Bid + * - Creator can set a epoch interval when `mintBoard`. + * - User needs to call `approve` on currency (e.g. USDT) contract before starting. + * - User can place a bid on a billboard: `placeBid`. + * - User can clear auction on a billboard: `clearAuction`. + * - User can withdraw a bid: `withdrawBid`. * * ## Tax - * - Admin of this contract can set global tax rate: call `setTaxRate`. - * - Owner of a billbaord can withdraw tax: call `withdrawTax`. + * - Creator can set a tax rate when `mintBoard`. + * - Creator can withdraw tax: `withdrawTax`. * * @dev This contract holds the logic, while read from and write into {BillboardRegistry}, which is the storage contact. * @dev This contract use the {BillboardRegistry} contract for storage, and can be updated by transfering ownership to a new implementation contract. @@ -42,25 +43,29 @@ interface IBillboard { ////////////////////////////// /** - * @notice Toggle for operation access. + * @notice Add or remove whitelist address. * - * @param value_ Value of access state. + * @param tokenId_ Token ID. + * @param account_ Address of user will be added into whitelist. + * @param whitelisted Whitelisted or not. */ - function setIsOpened(bool value_) external; + function setWhitelist(uint256 tokenId_, address account_, bool whitelisted) external; /** - * @notice Add address to white list. + * @notice Enable or disable a board whitelist feature * - * @param value_ Address of user will be added into white list. + * @param tokenId_ Token ID. + * @param disabled Disabled or not. */ - function addToWhitelist(address value_) external; + function setBoardWhitelistDisabled(uint256 tokenId_, bool disabled) external; /** - * @notice Remove address from white list. + * @notice Open or close a board. * - * @param value_ Address of user will be removed from white list. + * @param tokenId_ Token ID. + * @param closed Closed or not. */ - function removeFromWhitelist(address value_) external; + function setClosed(uint256 tokenId_, bool closed) external; ////////////////////////////// /// Board @@ -69,133 +74,262 @@ interface IBillboard { /** * @notice Mint a new board (NFT). * - * @param to_ Address of the new board receiver. + * @param taxRate_ Tax rate per epoch. (e.g. 1024 for 10.24% per epoch) + * @param epochInterval_ Epoch interval in blocks (e.g. 100 for 100 blocks). + * + * @return tokenId Token ID of the new board. */ - function mintBoard(address to_) external returns (uint256 tokenId); + function mintBoard(uint256 taxRate_, uint256 epochInterval_) external returns (uint256 tokenId); /** - * @notice Get a board data. + * @notice Mint a new board (NFT). * - * @param tokenId_ Token ID of a board. + * @param taxRate_ Tax rate per epoch. (e.g. 1024 for 10.24% per epoch) + * @param epochInterval_ Epoch interval in blocks (e.g. 100 for 100 blocks). + * @param startedAt_ Block number when the board starts the first epoch. * - * @return board Board data. + * @return tokenId Token ID of the new board. */ - function getBoard(uint256 tokenId_) external view returns (IBillboardRegistry.Board memory board); + function mintBoard(uint256 taxRate_, uint256 epochInterval_, uint256 startedAt_) external returns (uint256 tokenId); /** - * @notice Set the name of a board by board creator. + * @notice Get metadata of a board . * * @param tokenId_ Token ID of a board. - * @param name_ Board name. + * + * @return board Board metadata. */ - function setBoardName(uint256 tokenId_, string calldata name_) external; + function getBoard(uint256 tokenId_) external view returns (IBillboardRegistry.Board memory board); /** - * @notice Set the name of a board by board creator. + * @notice Set metadata of a board by creator. * * @param tokenId_ Token ID of a board. + * @param name_ Board name. * @param description_ Board description. + * @param imageURI_ Image URI of a board. + * @param location_ Location of a board. */ - function setBoardDescription(uint256 tokenId_, string calldata description_) external; + function setBoard( + uint256 tokenId_, + string calldata name_, + string calldata description_, + string calldata imageURI_, + string calldata location_ + ) external; + + ////////////////////////////// + /// Auction & Bid + ////////////////////////////// /** - * @notice Set the location of a board by board creator. + * @notice Clear an auction by a given epoch. * - * @param tokenId_ Token ID of a board. - * @param location_ Digital address where a board located. + * @param tokenId_ Token ID. + * @param epoch_ Epoch. + * + * @return highestBidder Address of the highest bidder. + * @return price Price of the highest bid. + * @return tax Tax of the highest bid. */ - function setBoardLocation(uint256 tokenId_, string calldata location_) external; + function clearAuction( + uint256 tokenId_, + uint256 epoch_ + ) external returns (address highestBidder, uint256 price, uint256 tax); /** - * @notice Set the content URI and redirect URI of a board by the tenant + * @notice Clear auctions by given epochs. * - * @param tokenId_ Token ID of a board. - * @param contentURI_ Content URI of a board. + * @param tokenIds_ Token IDs of boards. + * @param epochs_ Epochs of auctions. + * + * @return highestBidders Addresses of the highest bidders. + * @return prices Prices of the highest bids. + * @return taxes Taxes of the highest bids. */ - function setBoardContentURI(uint256 tokenId_, string calldata contentURI_) external; + function clearAuctions( + uint256[] calldata tokenIds_, + uint256[] calldata epochs_ + ) external returns (address[] calldata highestBidders, uint256[] calldata prices, uint256[] calldata taxes); /** - * @notice Set the redirect URI and redirect URI of a board by the tenant + * @notice Clear an auction from the last epoch. * - * @param tokenId_ Token ID of a board. - * @param redirectURI_ Redirect URI when users clicking. + * @param tokenId_ Token ID. + * + * @return highestBidder Address of the highest bidder. + * @return price Price of the highest bid. + * @return tax Tax of the highest bid. */ - function setBoardRedirectURI(uint256 tokenId_, string calldata redirectURI_) external; - - ////////////////////////////// - /// Auction - ////////////////////////////// + function clearLastAuction(uint256 tokenId_) external returns (address highestBidder, uint256 price, uint256 tax); /** - * @notice Get auction of a board by auction ID. + * @notice Clear auctions from the last epoch. * - * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of a board. + * @param tokenIds_ Token IDs of boards. + * + * @return highestBidders Addresses of the highest bidders. + * @return prices Prices of the highest bids. + * @return taxes Taxes of the highest bids. */ - function getAuction( - uint256 tokenId_, - uint256 auctionId_ - ) external view returns (IBillboardRegistry.Auction memory auction); + function clearLastAuctions( + uint256[] calldata tokenIds_ + ) external returns (address[] calldata highestBidders, uint256[] calldata prices, uint256[] calldata taxes); /** - * @notice Clear the next auction of a board. + * @notice Place bid on a board auction. * - * @param tokenId_ Token ID of a board. + * @param tokenId_ Token ID. + * @param epoch_ Epoch. + * @param price_ Amount of a bid. */ - function clearAuction(uint256 tokenId_) external returns (uint256 price, uint256 tax); + function placeBid(uint256 tokenId_, uint256 epoch_, uint256 price_) external; /** - * @notice Clear the next auction of mutiple boards. + * @notice Place bid on a board auction. * - * @param tokenIds_ Token IDs of boards. + * @param tokenId_ Token ID. + * @param epoch_ Epoch. + * @param price_ Amount of a bid. + * @param contentURI_ Content URI of a bid. + * @param redirectURI_ Redirect URI of a bid. */ - function clearAuctions( - uint256[] calldata tokenIds_ - ) external returns (uint256[] memory prices, uint256[] memory taxes); + function placeBid( + uint256 tokenId_, + uint256 epoch_, + uint256 price_, + string calldata contentURI_, + string calldata redirectURI_ + ) external; /** - * @notice Place bid for the next auction of a board. + * @notice Set the content URI and redirect URI of a board. * * @param tokenId_ Token ID of a board. - * @param amount_ Amount of a bid. + * @param epoch_ Epoch. + * @param contentURI_ Content URI of a board. + * @param redirectURI_ Redirect URI of a board. */ - function placeBid(uint256 tokenId_, uint256 amount_) external payable; + function setBidURIs( + uint256 tokenId_, + uint256 epoch_, + string calldata contentURI_, + string calldata redirectURI_ + ) external; /** - * @notice Get bid of a board auction by auction ID. + * @notice Get bid of a board auction. * * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of a board. + * @param epoch_ Epoch of an auction. * @param bidder_ Address of a bidder. * * @return bid Bid of a board. */ function getBid( uint256 tokenId_, - uint256 auctionId_, + uint256 epoch_, address bidder_ ) external view returns (IBillboardRegistry.Bid memory bid); /** - * @notice Get bids of a board auction by auction ID. + * @notice Get bids of a board auction. * - * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of a board. + * @param tokenId_ Token ID. + * @param epoch_ Epoch. * @param limit_ Limit of returned bids. * @param offset_ Offset of returned bids. * * @return total Total number of bids. * @return limit Limit of returned bids. * @return offset Offset of returned bids. - * @return bids Bids of a board. + * @return bids Bids. */ function getBids( uint256 tokenId_, - uint256 auctionId_, + uint256 epoch_, uint256 limit_, uint256 offset_ ) external view returns (uint256 total, uint256 limit, uint256 offset, IBillboardRegistry.Bid[] memory bids); + /** + * @notice Get all bids of bidder by token ID. + * + * @param tokenId_ Token ID. + * @param bidder_ Address of bidder. + * @param limit_ Limit of returned bids. + * @param offset_ Offset of returned bids. + * + * @return total Total number of bids. + * @return limit Limit of returned bids. + * @return offset Offset of returned bids. + * @return bids Bids. + * @return epoches Epoches of bids. + */ + function getBidderBids( + uint256 tokenId_, + address bidder_, + uint256 limit_, + uint256 offset_ + ) + external + view + returns ( + uint256 total, + uint256 limit, + uint256 offset, + IBillboardRegistry.Bid[] memory bids, + uint256[] memory epoches + ); + + /** + * @notice Withdraw bid that were not won by auction id; + * + * @param tokenId_ Token ID. + * @param epoch_ Epoch. + * @param bidder_ Address of bidder. + */ + function withdrawBid(uint256 tokenId_, uint256 epoch_, address bidder_) external; + + /** + * @notice Calculate epoch from block number. + * + * @param startedAt_ Started at block number. + * @param block_ Block number. + * @param epochInterval_ Epoch interval. + * + * @return epoch Epoch. + */ + function getEpochFromBlock( + uint256 startedAt_, + uint256 block_, + uint256 epochInterval_ + ) external pure returns (uint256 epoch); + + /** + * @notice Calculate block number from epoch. + * + * @param startedAt_ Started at block number. + * @param epoch_ Epoch. + * @param epochInterval_ Epoch interval. + * + * @return blockNumber Block number. + */ + function getBlockFromEpoch( + uint256 startedAt_, + uint256 epoch_, + uint256 epochInterval_ + ) external pure returns (uint256 blockNumber); + + /** + * @notice Get the latest epoch based on current block number. + * + * @param tokenId_ Token ID. + * + * @return epoch Epoch. + */ + function getLatestEpoch(uint256 tokenId_) external view returns (uint256 epoch); + ////////////////////////////// /// Tax & Withdraw ////////////////////////////// @@ -203,35 +337,41 @@ interface IBillboard { /** * @notice Get the global tax rate. * - * @return taxRate Tax rate. - */ - function getTaxRate() external view returns (uint256 taxRate); - - /** - * @notice Set the global tax rate. + * @param tokenId_ Token ID. * - * @param taxRate_ Tax rate. + * @return taxRate Tax rate. */ - function setTaxRate(uint256 taxRate_) external; + function getTaxRate(uint256 tokenId_) external view returns (uint256 taxRate); /** * @notice Calculate tax of a bid. * + * @param tokenId_ Token ID. * @param amount_ Amount of a bid. + * + * @return tax Tax of a bid. */ - function calculateTax(uint256 amount_) external returns (uint256 tax); + function calculateTax(uint256 tokenId_, uint256 amount_) external returns (uint256 tax); /** - * @notice Withdraw accumulated taxation of a board. + * @notice Withdraw accumulated taxation. + * + * @param creator_ Address of board creator. * */ - function withdrawTax() external returns (uint256 tax); + function withdrawTax(address creator_) external returns (uint256 tax); + + ////////////////////////////// + /// ERC721 related + ////////////////////////////// /** - * @notice Withdraw bid that were not won by auction id; + * @notice Get token URI by registry contract. * - * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of a board. + * @dev Access: only registry. + * + * @param tokenId_ Token id to be transferred. + * @return uri Base64 encoded URI. */ - function withdrawBid(uint256 tokenId_, uint256 auctionId_) external; + function _tokenURI(uint256 tokenId_) external view returns (string memory uri); } diff --git a/src/Billboard/IBillboardRegistry.sol b/src/Billboard/IBillboardRegistry.sol index 4c0a59f..5a270bc 100644 --- a/src/Billboard/IBillboardRegistry.sol +++ b/src/Billboard/IBillboardRegistry.sol @@ -19,121 +19,73 @@ interface IBillboardRegistry is IERC721 { event OperatorUpdated(address indexed operator); /** - * @notice Board name is updated. + * @notice Board is created. * - * @param tokenId Token ID of the board. - * @param name New name of the board. - */ - event BoardNameUpdated(uint256 indexed tokenId, string name); - - /** - * @notice Board description is updated. - * - * @param tokenId Token ID of the board. - * @param description New description of the board. + * @param tokenId Token ID of the board + * @param to Address of the board owner. + * @param taxRate Tax rate of the board. + * @param epochInterval Epoch interval of the board. */ - event BoardDescriptionUpdated(uint256 indexed tokenId, string description); + event BoardCreated(uint256 indexed tokenId, address indexed to, uint256 taxRate, uint256 epochInterval); /** - * @notice Board location is updated. + * @notice Board data is updated. * * @param tokenId Token ID of the board. - * @param location New location of the board. + * @param name Name of the board. + * @param description Description of the board. + * @param imageURI Image URI of the board. + * @param location Location of the board. */ - event BoardLocationUpdated(uint256 indexed tokenId, string location); - - /** - * @notice Board content URI is updated. - * - * @param tokenId Token ID of the board. - * @param contentURI New content URI of the board. - */ - event BoardContentURIUpdated(uint256 indexed tokenId, string contentURI); - - /** - * @notice Board redirect URI is updated. - * - * @param tokenId Token ID of the board. - * @param redirectURI New redirect URI of the board. - */ - event BoardRedirectURIUpdated(uint256 indexed tokenId, string redirectURI); - - /** - * @notice Global tax rate is updated. - * - * @param taxRate New tax rate. - */ - event TaxRateUpdated(uint256 taxRate); - - /** - * @notice Auction is created. - * - * @param tokenId Token ID of the board. - * @param auctionId Auction ID of the auction. - * @param startAt Start time of the auction. - * @param endAt End time of the auction. - */ - event AuctionCreated(uint256 indexed tokenId, uint256 indexed auctionId, uint64 startAt, uint64 endAt); + event BoardUpdated(uint256 indexed tokenId, string name, string description, string imageURI, string location); /** * @notice Auction is cleared. * * @param tokenId Token ID of the board. - * @param auctionId Auction ID of the auction. + * @param epoch Epoch of the auction. * @param highestBidder Highest bidder of the auction. - * @param leaseStartAt Start time of the lease. - * @param leaseEndAt End time of the lease. */ - event AuctionCleared( - uint256 indexed tokenId, - uint256 indexed auctionId, - address indexed highestBidder, - uint64 leaseStartAt, - uint64 leaseEndAt - ); + event AuctionCleared(uint256 indexed tokenId, uint256 indexed epoch, address indexed highestBidder); /** - * @notice Bid is created. + * @notice Bid is created or updated. * * @param tokenId Token ID of the board. - * @param auctionId Auction ID of the auction. + * @param epoch Epoch of the auction. * @param bidder Bidder of the auction. * @param price Price of the bid. * @param tax Tax of the bid. + * @param contentURI Content URI of the bid. + * @param redirectURI Redirect URI of the bid. */ - event BidCreated( + event BidUpdated( uint256 indexed tokenId, - uint256 indexed auctionId, + uint256 indexed epoch, address indexed bidder, uint256 price, - uint256 tax + uint256 tax, + string contentURI, + string redirectURI ); /** * @notice Bid is won. * * @param tokenId Token ID of the board. - * @param auctionId Auction ID of the auction. + * @param epoch Epoch of the auction. * @param bidder Bidder of the auction. */ - event BidWon(uint256 indexed tokenId, uint256 indexed auctionId, address indexed bidder); + event BidWon(uint256 indexed tokenId, uint256 indexed epoch, address indexed bidder); /** * @notice Bid is withdrawn. * * @param tokenId Token ID of the board. - * @param auctionId Auction ID of the auction. + * @param epoch Epoch of the auction. * @param bidder Bidder of the auction. - * @param price Price of the bid. - * @param tax Tax of the bid. */ - event BidWithdrawn( - uint256 indexed tokenId, - uint256 indexed auctionId, - address indexed bidder, - uint256 price, - uint256 tax - ); + event BidWithdrawn(uint256 indexed tokenId, uint256 indexed epoch, address indexed bidder); /** * @notice Tax is withdrawn. @@ -148,26 +100,25 @@ interface IBillboardRegistry is IERC721 { ////////////////////////////// struct Board { + // immutable data address creator; + uint256 taxRate; + uint256 epochInterval; // in blocks + uint256 startedAt; // gensis epoch, block number + // mutable data string name; string description; + string imageURI; string location; - string contentURI; - string redirectURI; - } - - struct Auction { - uint64 startAt; // block number - uint64 endAt; // block number - uint64 leaseStartAt; // block number - uint64 leaseEndAt; // block number - address highestBidder; } struct Bid { uint256 price; uint256 tax; + string contentURI; + string redirectURI; uint256 placedAt; // block number + uint256 updatedAt; // block number bool isWon; bool isWithdrawn; } @@ -188,24 +139,6 @@ interface IBillboardRegistry is IERC721 { /// Board ////////////////////////////// - /** - * @notice Mint a new board (NFT). - * - * @param to_ Address of the new board receiver. - * - * @return tokenId Token ID of the new board. - */ - function mintBoard(address to_) external returns (uint256 tokenId); - - /** - * @notice Transfer a board (NFT) by the operator. - * - * @param from_ Address of the board sender. - * @param to_ Address of the board receiver. - * @param tokenId_ Token ID of the board. - */ - function safeTransferByOperator(address from_, address to_, uint256 tokenId_) external; - /** * @notice Get a board * @@ -214,149 +147,157 @@ interface IBillboardRegistry is IERC721 { function getBoard(uint256 tokenId_) external view returns (Board memory board); /** - * @notice Set the name of a board by board creator. - * - * @param tokenId_ Token ID of a board. - * @param name_ Board name. - */ - function setBoardName(uint256 tokenId_, string calldata name_) external; - - /** - * @notice Set the name of a board by board creator. - * - * @param tokenId_ Token ID of a board. - * @param description_ Board description. - */ - function setBoardDescription(uint256 tokenId_, string calldata description_) external; - - /** - * @notice Set the location of a board by board creator. + * @notice Mint a new board (NFT). * - * @param tokenId_ Token ID of a board. - * @param location_ Digital address where a board located. - */ - function setBoardLocation(uint256 tokenId_, string calldata location_) external; - - /** - * @notice Set the content URI and redirect URI of a board by the tenant + * @param to_ Address of the board owner. + * @param taxRate_ Tax rate of the new board. + * @param epochInterval_ Epoch interval of the new board. + * @param startedAt_ Block number when the board starts the first epoch. * - * @param tokenId_ Token ID of a board. - * @param contentURI_ Content URI of a board. + * @return tokenId Token ID of the new board. */ - function setBoardContentURI(uint256 tokenId_, string calldata contentURI_) external; + function newBoard( + address to_, + uint256 taxRate_, + uint256 epochInterval_, + uint256 startedAt_ + ) external returns (uint256 tokenId); /** - * @notice Set the redirect URI and redirect URI of a board by the tenant + * @notice Set metadata of a board. * * @param tokenId_ Token ID of a board. - * @param redirectURI_ Redirect URI when users clicking. + * @param name_ Board name. + * @param description_ Board description. + * @param imageURI_ Image URI of a board. + * @param location_ Location of a board. */ - function setBoardRedirectURI(uint256 tokenId_, string calldata redirectURI_) external; + function setBoard( + uint256 tokenId_, + string calldata name_, + string calldata description_, + string calldata imageURI_, + string calldata location_ + ) external; ////////////////////////////// - /// Auction + /// Auction & Bid ////////////////////////////// /** - * @notice Get an auction + * @notice Get a bid of an auction * * @param tokenId_ Token ID of a board. - * @param auctionId_ Token ID of a board. + * @param epoch_ Epoch of an auction. + * @param bidder_ Bidder of an auction. */ - function getAuction(uint256 tokenId_, uint256 auctionId_) external view returns (Auction memory auction); + function getBid(uint256 tokenId_, uint256 epoch_, address bidder_) external view returns (Bid memory bid); /** - * @notice Create new auction + * @notice Get bid count of an auction * * @param tokenId_ Token ID of a board. - * @param startAt_ Start time of an auction. - * @param endAt_ End time of an auction. + * @param epoch_ Epoch. + * + * @return count Count of bids. */ - function newAuction(uint256 tokenId_, uint64 startAt_, uint64 endAt_) external returns (uint256 auctionId); + function getBidCount(uint256 tokenId_, uint256 epoch_) external view returns (uint256 count); /** - * @notice Set the data of an auction + * @notice Get the count of bidder bids * * @param tokenId_ Token ID of a board. - * @param auctionId_ Token ID of a board. - * @param leaseStartAt_ Start time of an board lease. - * @param leaseEndAt_ End time of an board lease. + * @param bidder_ Bidder of an auction. + * + * @return count Count of bids. */ - function setAuctionLease(uint256 tokenId_, uint256 auctionId_, uint64 leaseStartAt_, uint64 leaseEndAt_) external; + function getBidderBidCount(uint256 tokenId_, address bidder_) external view returns (uint256 count); /** - * @notice Get bid count of an auction + * @notice Create a bid * * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of an auction. + * @param epoch_ Epoch of an auction. + * @param bidder_ Bidder of an auction. + * @param price_ Price of a bid. + * @param tax_ Tax of a bid. + * @param contentURI_ Content URI of a bid. + * @param redirectURI_ Redirect URI of a bid. */ - function getBidCount(uint256 tokenId_, uint256 auctionId_) external view returns (uint256 count); + function newBid( + uint256 tokenId_, + uint256 epoch_, + address bidder_, + uint256 price_, + uint256 tax_, + string calldata contentURI_, + string calldata redirectURI_ + ) external; /** - * @notice Get a bid of an auction + * @notice Update a bid * * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of an auction. + * @param epoch_ Epoch of an auction. * @param bidder_ Bidder of an auction. + * @param price_ Price of a bid. + * @param tax_ Tax of a bid. + * @param contentURI_ Content URI of a bid. + * @param redirectURI_ Redirect URI of a bid. + * @param hasURIs_ Whether `contentURI_` or `redirectURI_` is provided. */ - function getBid(uint256 tokenId_, uint256 auctionId_, address bidder_) external view returns (Bid memory bid); + function setBid( + uint256 tokenId_, + uint256 epoch_, + address bidder_, + uint256 price_, + uint256 tax_, + string calldata contentURI_, + string calldata redirectURI_, + bool hasURIs_ + ) external; /** - * @notice Create new bid and add it to auction - * - * 1. Create new bid: `new Bid()` - * 2. Add bid to auction: - * - `auction.bids[bidder] = bid` - * - `auction.bidders.push(bidder)` - * - if any `auction.highestBidder = bidder` + * @notice Set the content URI and redirect URI of a board. * * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of an auction. + * @param epoch_ Epoch. * @param bidder_ Bidder of an auction. - * @param price_ Price of a bid. - * @param tax_ Tax of a bid. + * @param contentURI_ Content URI of a board. + * @param redirectURI_ Redirect URI of a board. */ - function newBid(uint256 tokenId_, uint256 auctionId_, address bidder_, uint256 price_, uint256 tax_) external; + function setBidURIs( + uint256 tokenId_, + uint256 epoch_, + address bidder_, + string calldata contentURI_, + string calldata redirectURI_ + ) external; /** * @notice Set isWon of a bid * * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of an auction. + * @param epoch_ Epoch of an auction. * @param bidder_ Bidder of an auction. * @param isWon_ Whether a bid is won. */ - function setBidWon(uint256 tokenId_, uint256 auctionId_, address bidder_, bool isWon_) external; + function setBidWon(uint256 tokenId_, uint256 epoch_, address bidder_, bool isWon_) external; /** * @notice Set isWithdrawn of a bid * * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of an auction. + * @param epoch_ Epoch of an auction. * @param bidder_ Bidder of an auction. * @param isWithdrawn_ Whether a bid is won. */ - function setBidWithdrawn(uint256 tokenId_, uint256 auctionId_, address bidder_, bool isWithdrawn_) external; - - /** - * @notice Transfer amount to a receiver. - * - * @param to_ Address of a receiver. - * @param amount_ Amount. - */ - function transferAmount(address to_, uint256 amount_) external; + function setBidWithdrawn(uint256 tokenId_, uint256 epoch_, address bidder_, bool isWithdrawn_) external; ////////////////////////////// /// Tax & Withdraw ////////////////////////////// - /** - * @notice Set the global tax rate. - * - * @param taxRate_ Tax rate. - */ - function setTaxRate(uint256 taxRate_) external; - /** * @notice Set the tax treasury. * @@ -374,41 +315,42 @@ interface IBillboardRegistry is IERC721 { * @notice Emit `AuctionCleared` event. * * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of an auction. + * @param epoch_ Epoch of an auction. * @param highestBidder_ Highest bidder of an auction. - * @param leaseStartAt_ Start time of an board lease. - * @param leaseEndAt_ End time of an board lease. */ - function emitAuctionCleared( - uint256 tokenId_, - uint256 auctionId_, - address highestBidder_, - uint64 leaseStartAt_, - uint64 leaseEndAt_ - ) external; + function emitAuctionCleared(uint256 tokenId_, uint256 epoch_, address highestBidder_) external; /** - * @notice Emit `BidWithdrawn` event. + * @notice Emit `TaxWithdrawn` event. * - * @param tokenId_ Token ID of a board. - * @param auctionId_ Auction ID of an auction. - * @param bidder_ Bidder of an auction. - * @param price_ Price of a bid. - * @param tax_ Tax of a bid. + * @param owner_ Address of a treasury owner. + * @param amount_ Amount. */ - function emitBidWithdrawn( - uint256 tokenId_, - uint256 auctionId_, - address bidder_, - uint256 price_, - uint256 tax_ - ) external; + function emitTaxWithdrawn(address owner_, uint256 amount_) external; + + ////////////////////////////// + /// ERC20 & ERC721 related + ////////////////////////////// /** - * @notice Emit `TaxWithdrawn` event. + * @notice Transfer a board (NFT). * - * @param owner_ Address of a treasury owner. + * @param from_ Address of the board sender. + * @param to_ Address of the board receiver. + * @param tokenId_ Token ID of the board. + */ + function safeTransferByOperator(address from_, address to_, uint256 tokenId_) external; + + /** + * @notice Transfer amount of token to a receiver. + * + * @param to_ Address of a receiver. * @param amount_ Amount. */ - function emitTaxWithdrawn(address owner_, uint256 amount_) external; + function transferCurrencyByOperator(address to_, uint256 amount_) external; + + /** + * @dev If an ERC721 token has been minted. + */ + function exists(uint256 tokenId_) external view returns (bool); } diff --git a/src/test/Billboard/BillboardTest.t.sol b/src/test/Billboard/BillboardTest.t.sol index 9f7a114..d0ca40f 100644 --- a/src/test/Billboard/BillboardTest.t.sol +++ b/src/test/Billboard/BillboardTest.t.sol @@ -1,8 +1,10 @@ //SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import "./BillboardTestBase.t.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/utils/Base64.sol"; + +import "./BillboardTestBase.t.sol"; contract BillboardTest is BillboardTestBase { ////////////////////////////// @@ -13,15 +15,7 @@ contract BillboardTest is BillboardTestBase { vm.startPrank(ADMIN); // deploy new operator - Billboard newOperator = new Billboard( - address(usdt), - payable(registry), - ADMIN, - TAX_RATE, - LEASE_TERM, - "Billboard2", - "BLBD2" - ); + Billboard newOperator = new Billboard(address(usdt), payable(registry), ADMIN, "Billboard2", "BLBD2"); assertEq(newOperator.admin(), ADMIN); assertEq(registry.name(), "Billboard"); // registry is not changed assertEq(registry.symbol(), "BLBD"); // registry is not changed @@ -43,53 +37,67 @@ contract BillboardTest is BillboardTestBase { /// Access control ////////////////////////////// - function testSetIsOpened() public { + function testSetWhitelist() public { + (uint256 _tokenId, ) = _mintBoard(); + vm.startPrank(ADMIN); - operator.setIsOpened(true); - assertEq(operator.isOpened(), true); + // add to whitelist + operator.setWhitelist(_tokenId, USER_A, true); + assertEq(operator.whitelist(_tokenId, USER_A), true); + assertEq(operator.whitelist(_tokenId, USER_B), false); - operator.setIsOpened(false); - assertEq(operator.isOpened(), false); + // remove from whitelist + operator.setWhitelist(_tokenId, USER_A, false); + assertEq(operator.whitelist(_tokenId, USER_A), false); } - function testCannotSetIsOpenedByAttacker() public { + function testCannotSetWhitelistByAttacker() public { + (uint256 _tokenId, ) = _mintBoard(); + vm.startPrank(ATTACKER); - vm.expectRevert("Admin"); - operator.setIsOpened(true); + vm.expectRevert("Creator"); + operator.setWhitelist(_tokenId, USER_B, false); } - function testAddToWhitelist() public { + function testSetBoardWhitelistDisabled() public { + (uint256 _tokenId, ) = _mintBoard(); + vm.startPrank(ADMIN); - operator.addToWhitelist(USER_A); - assertEq(operator.whitelist(USER_A), true); - assertEq(operator.whitelist(USER_B), false); - } + assertEq(operator.isBoardWhitelistDisabled(_tokenId), false); - function testCannotAddToWhitelistByAttacker() public { - vm.startPrank(ATTACKER); + // disable + operator.setBoardWhitelistDisabled(_tokenId, true); + assertEq(operator.isBoardWhitelistDisabled(_tokenId), true); - vm.expectRevert("Admin"); - operator.addToWhitelist(USER_A); + // enable + operator.setBoardWhitelistDisabled(_tokenId, false); + assertEq(operator.whitelist(_tokenId, USER_A), false); } - function testRemoveToWhitelist() public { + function testSetClosed() public { + (uint256 _tokenId, ) = _mintBoard(); + vm.startPrank(ADMIN); - operator.addToWhitelist(USER_A); - assertEq(operator.whitelist(USER_A), true); + // set closed + operator.setClosed(_tokenId, true); + assertEq(operator.closed(_tokenId), true); - operator.removeFromWhitelist(USER_A); - assertEq(operator.whitelist(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("Admin"); - operator.removeFromWhitelist(USER_B); + vm.expectRevert("Creator"); + operator.setClosed(_tokenId, true); } ////////////////////////////// @@ -102,125 +110,75 @@ contract BillboardTest is BillboardTestBase { // mint vm.expectEmit(true, true, true, true); emit IERC721.Transfer(address(0), ADMIN, 1); - operator.mintBoard(ADMIN); - assertEq(registry.balanceOf(ADMIN), 1); + uint256 _tokenId = operator.mintBoard(TAX_RATE, EPOCH_INTERVAL); // ownership - assertEq(registry.ownerOf(1), ADMIN); - - // get board & check data - IBillboardRegistry.Board memory board = operator.getBoard(1); - assertEq(board.creator, ADMIN); - assertEq(board.name, ""); - assertEq(board.description, ""); - assertEq(board.location, ""); - assertEq(board.contentURI, ""); - assertEq(board.redirectURI, ""); - - // mint again for checking id generator - vm.expectEmit(true, true, true, true); - emit IERC721.Transfer(address(0), ADMIN, 2); - operator.mintBoard(ADMIN); - assertEq(registry.balanceOf(ADMIN), 2); - board = operator.getBoard(2); - assertEq(board.creator, ADMIN); - } - - function testMintBoardIfOpened() public { - vm.startPrank(ADMIN); - operator.setIsOpened(true); + assertEq(registry.balanceOf(ADMIN), 1); + assertEq(registry.ownerOf(_tokenId), ADMIN); + + // data + IBillboardRegistry.Board memory _board = operator.getBoard(_tokenId); + assertEq(_board.creator, ADMIN); + assertEq(_board.name, ""); + assertEq(_board.description, ""); + assertEq(_board.imageURI, ""); + assertEq(_board.location, ""); + assertEq(_board.taxRate, TAX_RATE); + assertEq(_board.epochInterval, EPOCH_INTERVAL); + assertEq(_board.startedAt, block.number); + vm.stopPrank(); vm.startPrank(USER_A); - operator.mintBoard(USER_A); - assertEq(registry.balanceOf(USER_A), 1); - } - - function testMintBoardByWhitelist() public { - vm.prank(USER_A); - vm.expectRevert("Whitelist"); - operator.mintBoard(USER_A); - - vm.prank(ADMIN); - operator.addToWhitelist(USER_A); - vm.prank(USER_A); - operator.mintBoard(USER_A); + // mint by user and check token id + uint256 _newTokenId = 2; + vm.expectEmit(true, true, true, true); + emit IERC721.Transfer(address(0), USER_A, _newTokenId); + uint256 _tokenId2 = operator.mintBoard(TAX_RATE, EPOCH_INTERVAL); + assertEq(_tokenId2, _newTokenId); assertEq(registry.balanceOf(USER_A), 1); + IBillboardRegistry.Board memory _board2 = operator.getBoard(_tokenId2); + assertEq(_board2.creator, USER_A); + + // mint with startedAt + uint256 _startedAt = block.number + 100; + uint256 _tokenId3 = operator.mintBoard(TAX_RATE, EPOCH_INTERVAL, _startedAt); + IBillboardRegistry.Board memory _board3 = operator.getBoard(_tokenId3); + assertEq(_board3.startedAt, _startedAt); } - function testCannotMintBoardByAttacker() public { - vm.startPrank(ATTACKER); + function testSetBoardByCreator() public { + (uint256 _tokenId, ) = _mintBoard(); + string memory _name = "name"; + string memory _description = "description"; + string memory _imageURI = "image URI"; + string memory _location = "location"; - vm.expectRevert("Whitelist"); - operator.mintBoard(ATTACKER); - } - - function testSetBoardProperties() public { - uint256 _tokenId = _mintBoard(); + vm.expectEmit(true, true, true, true); + emit IBillboardRegistry.BoardUpdated(_tokenId, _name, _description, _imageURI, _location); vm.startPrank(ADMIN); + operator.setBoard(_tokenId, _name, _description, _imageURI, _location); - vm.expectEmit(true, true, false, false); - emit IBillboardRegistry.BoardNameUpdated(_tokenId, "name"); - operator.setBoardName(_tokenId, "name"); - - vm.expectEmit(true, true, false, false); - emit IBillboardRegistry.BoardDescriptionUpdated(_tokenId, "description"); - operator.setBoardDescription(_tokenId, "description"); - - vm.expectEmit(true, true, false, false); - emit IBillboardRegistry.BoardLocationUpdated(_tokenId, "location"); - operator.setBoardLocation(_tokenId, "location"); - - vm.expectEmit(true, true, false, false); - emit IBillboardRegistry.BoardContentURIUpdated(_tokenId, "uri"); - operator.setBoardContentURI(_tokenId, "uri"); - - vm.expectEmit(true, true, false, false); - emit IBillboardRegistry.BoardRedirectURIUpdated(_tokenId, "redirect URI"); - operator.setBoardRedirectURI(_tokenId, "redirect URI"); - - IBillboardRegistry.Board memory board = operator.getBoard(1); - assertEq(board.name, "name"); - assertEq(board.description, "description"); - assertEq(board.location, "location"); - assertEq(board.contentURI, "uri"); - assertEq(board.redirectURI, "redirect URI"); + IBillboardRegistry.Board memory board = operator.getBoard(_tokenId); + assertEq(board.name, _name); + assertEq(board.description, _description); + assertEq(board.imageURI, _imageURI); + assertEq(board.location, _location); } - function testCannotSetBoardProprtiesByAttacker() public { - uint256 _tokenId = _mintBoard(); + function testCannotSetBoardByAttacker() public { + (uint256 _tokenId, ) = _mintBoard(); vm.startPrank(ATTACKER); vm.expectRevert("Creator"); - operator.setBoardName(_tokenId, "name"); - - vm.expectRevert("Creator"); - operator.setBoardDescription(_tokenId, "description"); - - vm.expectRevert("Creator"); - operator.setBoardLocation(_tokenId, "location"); - - vm.expectRevert("Tenant"); - operator.setBoardContentURI(_tokenId, "uri"); - - vm.expectRevert("Tenant"); - operator.setBoardRedirectURI(_tokenId, "redirect URI"); + operator.setBoard(_tokenId, "", "", "", ""); } - function testGetTokenURI() public { - uint256 _tokenId = _mintBoard(); - - vm.startPrank(ADMIN); - - operator.setBoardContentURI(_tokenId, "new uri"); - assertEq(registry.tokenURI(_tokenId), "new uri"); - } - - function testSetBoardPropertiesAfterTransfer() public { + function testCannotSetBoardByOwner() public { // mint - uint256 _tokenId = _mintBoard(); + (uint256 _tokenId, ) = _mintBoard(); // transfer vm.startPrank(ADMIN); @@ -231,224 +189,91 @@ contract BillboardTest is BillboardTestBase { assertEq(registry.balanceOf(ADMIN), 0); assertEq(registry.ownerOf(_tokenId), USER_A); - // set board properties + // cannot set board by new owner vm.stopPrank(); vm.startPrank(USER_A); - vm.expectRevert("Creator"); - operator.setBoardName(_tokenId, "name by a"); - - vm.expectRevert("Creator"); - operator.setBoardDescription(_tokenId, "description by a"); - - vm.expectRevert("Creator"); - operator.setBoardLocation(_tokenId, "location by a"); - - operator.setBoardContentURI(_tokenId, "uri by a"); - operator.setBoardRedirectURI(_tokenId, "redirect URI by a"); - - board = operator.getBoard(_tokenId); - assertEq(board.name, ""); - assertEq(board.description, ""); - assertEq(board.location, ""); - assertEq(board.contentURI, "uri by a"); - assertEq(board.redirectURI, "redirect URI by a"); - - // transfer board from user_a to user_b - registry.safeTransferFrom(USER_A, USER_B, 1); - board = operator.getBoard(_tokenId); - assertEq(board.creator, ADMIN); - assertEq(registry.ownerOf(_tokenId), USER_B); + operator.setBoard(_tokenId, "name", "description", "image URI", "location"); + // can set board by creator vm.stopPrank(); - vm.startPrank(USER_B); - - vm.expectRevert("Creator"); - operator.setBoardName(_tokenId, "name by b"); - - vm.expectRevert("Creator"); - operator.setBoardDescription(_tokenId, "description by b"); - - vm.expectRevert("Creator"); - operator.setBoardLocation(_tokenId, "location by b"); - - operator.setBoardContentURI(_tokenId, "uri by b"); - operator.setBoardRedirectURI(_tokenId, "redirect URI by b"); - - board = operator.getBoard(_tokenId); - assertEq(board.name, ""); - assertEq(board.description, ""); - assertEq(board.location, ""); - assertEq(board.contentURI, "uri by b"); - assertEq(board.redirectURI, "redirect URI by b"); - } - - function testCannotTransferToZeroAddress() public { - uint256 _tokenId = _mintBoard(); - vm.startPrank(ADMIN); - - vm.expectRevert("ERC721: transfer to the zero address"); - registry.transferFrom(ADMIN, ZERO_ADDRESS, _tokenId); - } - - function testCannotTransferByOperator() public { - uint256 _tokenId = _mintBoard(); - - vm.startPrank(address(operator)); - - vm.expectRevert("ERC721: caller is not token owner or approved"); - registry.transferFrom(USER_B, USER_C, _tokenId); - } - - function testSafeTransferByOperator() public { - uint256 _tokenId = _mintBoard(); - vm.expectEmit(true, true, true, true); - emit IERC721.Transfer(ADMIN, USER_A, _tokenId); - - vm.startPrank(address(operator)); - registry.safeTransferByOperator(ADMIN, USER_A, _tokenId); - assertEq(registry.ownerOf(_tokenId), USER_A); - } - - function testCannotSafeTransferByAttacker() public { - uint256 _tokenId = _mintBoard(); - - vm.startPrank(ATTACKER); - - vm.expectRevert("Operator"); - registry.safeTransferByOperator(ADMIN, ATTACKER, _tokenId); - } - - function testApproveAndTransfer() public { - uint256 _tokenId = _mintBoard(); - - vm.expectEmit(true, true, true, true); - emit IERC721.Approval(ADMIN, USER_A, _tokenId); - vm.prank(ADMIN); - registry.approve(USER_A, _tokenId); - assertEq(registry.getApproved(_tokenId), USER_A); - - vm.expectEmit(true, true, true, true); - emit IERC721.Transfer(ADMIN, USER_A, _tokenId); - vm.prank(USER_A); - registry.transferFrom(ADMIN, USER_A, _tokenId); - - IBillboardRegistry.Board memory board = operator.getBoard(_tokenId); - assertEq(board.creator, ADMIN); - assertEq(registry.ownerOf(_tokenId), USER_A); - } - - function testCannotApproveByAttacker() public { - uint256 _tokenId = _mintBoard(); - - vm.stopPrank(); - vm.startPrank(ATTACKER); - vm.expectRevert("ERC721: approve caller is not token owner or approved for all"); - registry.approve(USER_A, _tokenId); + emit IBillboardRegistry.BoardUpdated(_tokenId, "name", "description", "image URI", "location"); + operator.setBoard(_tokenId, "name", "description", "image URI", "location"); } ////////////////////////////// - /// Auction + /// Auction & Bid ////////////////////////////// - function testPlaceBidOnNewBoard(uint96 _amount) public { - vm.prank(ADMIN); - operator.addToWhitelist(USER_A); - - vm.expectEmit(true, false, false, false); - emit IERC721.Transfer(address(0), ADMIN, 1); - - uint256 _tokenId = _mintBoard(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _overpaid = 0.1 ether; - uint256 _total = _amount + _tax; - deal(address(usdt), USER_A, _total + _overpaid); + function testPlaceBid(uint96 _price) public { + (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; + deal(address(usdt), USER_A, _total); - uint256 _prevNextActionId = registry.nextBoardAuctionId(_tokenId); uint256 _prevCreatorBalance = usdt.balanceOf(ADMIN); uint256 _prevBidderBalance = usdt.balanceOf(USER_A); uint256 _prevOperatorBalance = usdt.balanceOf(address(operator)); uint256 _prevRegistryBalance = usdt.balanceOf(address(registry)); + vm.startPrank(ADMIN); + operator.setWhitelist(_tokenId, USER_A, true); + operator.setWhitelist(_tokenId, USER_B, true); + vm.stopPrank(); + vm.expectEmit(true, true, true, true); - emit IBillboardRegistry.AuctionCreated( - _tokenId, - _prevNextActionId + 1, - uint64(block.number), - uint64(block.number) - ); - vm.expectEmit(true, true, true, true); - emit IBillboardRegistry.BidCreated(_tokenId, _prevNextActionId + 1, USER_A, _amount, _tax); - vm.expectEmit(true, true, true, true); - emit IBillboardRegistry.BidWon(_tokenId, _prevNextActionId + 1, USER_A); - vm.expectEmit(true, true, true, true); - emit IBillboardRegistry.AuctionCleared( - _tokenId, - _prevNextActionId + 1, - USER_A, - uint64(block.number), - uint64(block.number + registry.leaseTerm()) - ); + emit IBillboardRegistry.BidUpdated(_tokenId, _epoch, USER_A, _price, _tax, "", ""); vm.prank(USER_A); - operator.placeBid(_tokenId, _amount); + operator.placeBid(_tokenId, _epoch, _price); // check balances - assertEq(usdt.balanceOf(ADMIN), _prevCreatorBalance + _amount); + assertEq(usdt.balanceOf(ADMIN), _prevCreatorBalance); assertEq(usdt.balanceOf(USER_A), _prevBidderBalance - _total); assertEq(usdt.balanceOf(address(operator)), _prevOperatorBalance); - assertEq(usdt.balanceOf(address(registry)), _prevRegistryBalance + _tax); - - // check auction - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - IBillboardRegistry.Auction memory _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_prevNextActionId, 0); - assertEq(_nextAuctionId, _prevNextActionId + 1); - assertEq(_auction.startAt, block.number); - assertEq(_auction.endAt, block.number); - assertEq(_auction.leaseStartAt, block.number); - assertEq(_auction.leaseEndAt, block.number + registry.leaseTerm()); - assertEq(_auction.highestBidder, USER_A); + assertEq(usdt.balanceOf(address(registry)), _prevRegistryBalance + _total); // check bid - IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _nextAuctionId, USER_A); - assertEq(_bid.price, _amount); + IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _epoch, USER_A); + assertEq(_bid.price, _price); assertEq(_bid.tax, _tax); assertEq(_bid.placedAt, block.number); - assertEq(_bid.isWon, true); + assertEq(_bid.updatedAt, block.number); + assertEq(_bid.isWon, false); assertEq(_bid.isWithdrawn, false); + + // bid with AD data + string memory _contentURI = "content URI"; + string memory _redirectURI = "redirect URI"; + vm.expectEmit(true, true, true, true); + emit IBillboardRegistry.BidUpdated(_tokenId, _epoch, USER_B, 0, 0, _contentURI, _redirectURI); + + vm.prank(USER_B); + operator.placeBid(_tokenId, _epoch, 0, _contentURI, _redirectURI); } - function testPlaceBidWithSamePrices(uint96 _amount) public { - (uint256 _tokenId, uint256 _prevNextAuctionId) = _mintBoardAndPlaceBid(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + function testPlaceBidWithSamePrices(uint96 _price) public { + (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; - // new auction and new bid with USER_A - deal(address(usdt), USER_A, _total); - vm.prank(USER_A); - operator.placeBid(_tokenId, _amount); - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - assertEq(_nextAuctionId, _prevNextAuctionId + 1); - IBillboardRegistry.Auction memory _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.highestBidder, USER_A); + // bid with USER_A + _placeBid(_tokenId, _epoch, USER_A, _price); + assertEq(registry.highestBidder(_tokenId, _epoch), USER_A); - // new bid with USER_B - deal(address(usdt), USER_B, _total); - vm.prank(USER_B); - operator.placeBid(_tokenId, _amount); - _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - assertEq(_nextAuctionId, _prevNextAuctionId + 1); // still the same auction - _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.highestBidder, USER_A); // USER_A is still the same highest bidder - - // check if bids exist - IBillboardRegistry.Bid memory _bidA = registry.getBid(_tokenId, _nextAuctionId, USER_A); + // bid with USER_B + _placeBid(_tokenId, _epoch, USER_B, _price); + assertEq(registry.highestBidder(_tokenId, _epoch), USER_A); // USER_A is still the same highest bidder + + // check bids + IBillboardRegistry.Bid memory _bidA = registry.getBid(_tokenId, _epoch, USER_A); assertEq(_bidA.placedAt, block.number); assertEq(_bidA.isWon, false); - IBillboardRegistry.Bid memory _bidB = registry.getBid(_tokenId, _nextAuctionId, USER_A); + IBillboardRegistry.Bid memory _bidB = registry.getBid(_tokenId, _epoch, USER_A); assertEq(_bidB.placedAt, block.number); assertEq(_bidB.isWon, false); @@ -456,40 +281,56 @@ contract BillboardTest is BillboardTestBase { assertEq(usdt.balanceOf(address(registry)), _total * 2); } - function testPlaceBidWithHigherPrice(uint96 _amount) public { - vm.assume(_amount > 0); - vm.assume(_amount < type(uint96).max / 2); + function testPlaceBidWithHigherPrice(uint96 _price) public { + vm.assume(_price > 0); + vm.assume(_price < type(uint96).max / 4); + + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _tax = operator.calculateTax(_tokenId, _price); - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + uint256 _placedAt = block.number; + uint256 _updatedAt = block.number + 1; // bid with USER_A - deal(address(usdt), USER_A, _total); - vm.prank(USER_A); - operator.placeBid(_tokenId, _amount); - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - IBillboardRegistry.Auction memory _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.highestBidder, USER_A); + _placeBid(_tokenId, _epoch, USER_A, _price); + assertEq(registry.highestBidder(_tokenId, _epoch), USER_A); // bid with USER_B - _amount = _amount * 2; - _tax = operator.calculateTax(_amount); - _total = _amount + _tax; - deal(address(usdt), USER_B, _total); - vm.startPrank(USER_B); - operator.placeBid(_tokenId, _amount); - _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.highestBidder, USER_B); + uint256 _priceB = _price * 2; + _placeBid(_tokenId, _epoch, USER_B, _priceB); + assertEq(registry.highestBidder(_tokenId, _epoch), USER_B); + + // bid with USER_A + vm.roll(_updatedAt); + uint256 _priceA = _price * 4; + uint256 _taxA = operator.calculateTax(_tokenId, _priceA); + uint256 _totalA = _priceA + _taxA; + _placeBid(_tokenId, _epoch, USER_A, _priceA); + assertEq(registry.highestBidder(_tokenId, _epoch), USER_A); + + // check balance of USER_A + uint256 _priceDiff = _priceA - _price; + uint256 _taxDiff = _taxA - _tax; + uint256 _totalDiff = _priceDiff + _taxDiff; + assertEq(usdt.balanceOf(USER_A), _totalA - _totalDiff); + + // check bid + IBillboardRegistry.Bid memory _bidA = registry.getBid(_tokenId, _epoch, USER_A); + assertEq(_bidA.price, _priceA); + assertEq(_bidA.tax, _taxA); + assertEq(_bidA.placedAt, _placedAt); + assertEq(_bidA.updatedAt, _updatedAt); } function testPlaceBidZeroPrice() public { - uint256 _tokenId = _mintBoard(); - - vm.startPrank(ADMIN); + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); uint256 _prevBalance = usdt.balanceOf(ADMIN); - operator.placeBid(_tokenId, 0); + vm.startPrank(ADMIN); + operator.placeBid(_tokenId, _epoch, 0); + assertEq(registry.highestBidder(_tokenId, _epoch), ADMIN); // check balances uint256 _afterBalance = usdt.balanceOf(ADMIN); @@ -497,254 +338,285 @@ contract BillboardTest is BillboardTestBase { assertEq(usdt.balanceOf(address(operator)), 0); assertEq(usdt.balanceOf(address(registry)), 0); - // check auction - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - IBillboardRegistry.Auction memory _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.highestBidder, ADMIN); - // check bid - IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _nextAuctionId, ADMIN); + IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _epoch, ADMIN); assertEq(_bid.placedAt, block.number); - assertEq(_bid.isWon, true); + assertEq(_bid.isWon, false); } - function testPlaceBidByWhitelist() public { - uint256 _tokenId = _mintBoard(); - uint256 _amount = 1 ether; - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + function testCannotPlaceBidIfClosed() public { + (uint256 _tokenId, ) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(block.number, block.number, 1); - vm.prank(ADMIN); - operator.addToWhitelist(USER_A); + vm.startPrank(ADMIN); + operator.setClosed(_tokenId, true); - deal(address(usdt), USER_A, _total); - vm.prank(USER_A); - operator.placeBid(_tokenId, _amount); - assertEq(usdt.balanceOf(USER_A), 0); + vm.expectRevert("Closed"); + operator.placeBid(_tokenId, _epoch, 1 ether); } - function testPlaceBidIfAuctionEnded() public { - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); - uint256 _amount = 1 ether; - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + function testCannotPlaceBidIfAuctionEnded() public { + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _price = 1 ether; - // place a bid with USER_A - vm.startPrank(USER_A); - deal(address(usdt), USER_A, _total); - operator.placeBid(_tokenId, _amount); - - // check auction - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - IBillboardRegistry.Auction memory _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.highestBidder, USER_A); - assertEq(_auction.endAt, block.number + registry.leaseTerm()); - - // make auction ended - vm.roll(_auction.endAt + 1); - - // place a bid with USER_B - vm.startPrank(USER_B); - deal(address(usdt), USER_B, _total); - operator.placeBid(_tokenId, _amount); - - // check auction - uint256 _newNextAuctionId = registry.nextBoardAuctionId(_tokenId); - IBillboardRegistry.Auction memory _newAuction = registry.getAuction(_tokenId, _newNextAuctionId); - assertEq(_newNextAuctionId, _nextAuctionId + 1); - assertEq(_newAuction.highestBidder, USER_B); - assertEq(_newAuction.endAt, block.number + registry.leaseTerm()); - - // USER_A won the previous auction - IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _nextAuctionId, USER_A); - assertEq(_bid.isWon, true); - - // USER_B's bid is still in a running auction - IBillboardRegistry.Bid memory _newBid = registry.getBid(_tokenId, _newNextAuctionId, USER_B); - assertEq(_newBid.isWon, false); - } + uint256 _endedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); - function testCannotPlaceBidTwice(uint96 _amount) public { - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + vm.prank(ADMIN); + operator.setWhitelist(_tokenId, USER_A, true); vm.startPrank(USER_A); - deal(address(usdt), USER_A, _total); - operator.placeBid(_tokenId, _amount); - assertEq(usdt.balanceOf(USER_A), 0); - deal(address(usdt), USER_A, _total); - vm.expectRevert("Bid already placed"); - operator.placeBid(_tokenId, _amount); + vm.roll(_endedAt); + vm.expectRevert("Auction ended"); + operator.placeBid(_tokenId, _epoch, _price); + + vm.roll(_endedAt + 1); + vm.expectRevert("Auction ended"); + operator.placeBid(_tokenId, _epoch, _price); } - function testCannotPlaceBidByAttacker() public { - uint256 _tokenId = _mintBoard(); - uint256 _amount = 1 ether; - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + function testCannotPlaceBidIfNotWhitelisted() public { + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _price = 1 ether; + uint256 _tax = operator.calculateTax(_tokenId, _price); + uint256 _total = _price + _tax; + deal(address(usdt), USER_A, _total); vm.startPrank(ATTACKER); deal(address(usdt), ATTACKER, _total); vm.expectRevert("Whitelist"); - operator.placeBid(_tokenId, _amount); + operator.placeBid(_tokenId, _epoch, _price); } - function testClearAuctionIfAuctionEnded(uint96 _amount) public { - vm.assume(_amount > 0.001 ether); + function testPlaceBidIfBoardWhitelistDisabled() public { + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + vm.prank(ADMIN); + operator.setBoardWhitelistDisabled(_tokenId, true); - (uint256 _tokenId, uint256 _prevAuctionId) = _mintBoardAndPlaceBid(); - uint64 _placedAt = uint64(block.number); - uint64 _clearedAt = uint64(block.number) + registry.leaseTerm() + 1; + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _price = 1 ether; + uint256 _tax = operator.calculateTax(_tokenId, _price); + uint256 _total = _price + _tax; + deal(address(usdt), USER_A, _total); - // place a bid vm.startPrank(USER_A); - deal(address(usdt), USER_A, _total); - operator.placeBid(_tokenId, _amount); + operator.placeBid(_tokenId, _epoch, _price); + IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _epoch, USER_A); + assertEq(_bid.price, _price); + assertEq(_bid.tax, _tax); + } + + function testSetBidURIs() public { + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + + _placeBid(_tokenId, _epoch, USER_A, 1 ether); + + // check bid + IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _epoch, USER_A); + assertEq(_bid.contentURI, ""); + assertEq(_bid.redirectURI, ""); + + // set bid AD data + string memory _contentURI = "content URI"; + string memory _redirectURI = "redirect URI"; + + vm.prank(USER_A); + operator.setBidURIs(_tokenId, _epoch, _contentURI, _redirectURI); + + IBillboardRegistry.Bid memory _newBid = registry.getBid(_tokenId, _epoch, USER_A); + assertEq(_newBid.contentURI, _contentURI); + assertEq(_newBid.redirectURI, _redirectURI); + } + + function testClearAuction(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 _placedAt = block.number; + uint256 _clearedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); + + // place bid + _placeBid(_tokenId, _epoch, USER_A, _price); // clear auction - vm.expectEmit(true, true, true, true); - emit IBillboardRegistry.AuctionCleared( - _tokenId, - _prevAuctionId + 1, - USER_A, - _clearedAt, - _clearedAt + registry.leaseTerm() - ); + vm.expectEmit(true, true, true, false); + emit IBillboardRegistry.AuctionCleared(_tokenId, _epoch, USER_A); vm.roll(_clearedAt); - (uint256 _price1, uint256 _tax1) = operator.clearAuction(_tokenId); - assertEq(_price1, _amount); - assertEq(_tax1, _tax); + (address _highestBidder, uint256 _price1, uint256 _tax1) = operator.clearAuction(_tokenId, _epoch); - // check auction - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - IBillboardRegistry.Auction memory _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.startAt, _placedAt); - assertEq(_auction.endAt, _placedAt + registry.leaseTerm()); - assertEq(_auction.leaseStartAt, _clearedAt); - assertEq(_auction.leaseEndAt, _clearedAt + registry.leaseTerm()); - assertEq(_auction.highestBidder, USER_A); + assertEq(_price1, _price); + assertEq(_tax1, _tax); + assertEq(_highestBidder, registry.highestBidder(_tokenId, _epoch)); + assertEq(_highestBidder, USER_A); - // check bid - IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _nextAuctionId, USER_A); - assertEq(_bid.price, _amount); + // check auction & bid + IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _epoch, USER_A); + assertEq(_bid.price, _price); assertEq(_bid.tax, _tax); assertEq(_bid.placedAt, _placedAt); assertEq(_bid.isWon, true); assertEq(_bid.isWithdrawn, false); + + // check balances + assertEq(usdt.balanceOf(address(registry)), _tax); + assertEq(usdt.balanceOf(ADMIN), _price); + assertEq(usdt.balanceOf(USER_A), 0); } - function testClearAuctionsIfAuctionEnded() public { - (uint256 _tokenId, uint256 _prevAuctionId) = _mintBoardAndPlaceBid(); - (uint256 _tokenId2, uint256 _prevAuctionId2) = _mintBoardAndPlaceBid(); + function testClearAuctionIfAlreadyCleared() public { + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _clearedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); + uint256 _price = 1 ether; + uint256 _tax = operator.calculateTax(_tokenId, _price); - uint64 _placedAt = uint64(block.number); - uint64 _clearedAt = uint64(block.number) + registry.leaseTerm() + 1; + // place bid + _placeBid(_tokenId, _epoch, USER_A, 1 ether); - // place bids - vm.startPrank(USER_A); - deal(address(usdt), USER_A, 0); - operator.placeBid(_tokenId, 0); + // clear auction + vm.roll(_clearedAt); + (address _highestBidder1, uint256 _price1, uint256 _tax1) = operator.clearAuction(_tokenId, _epoch); + assertEq(_highestBidder1, USER_A); + assertEq(_price1, _price); + assertEq(_tax1, _tax); + + // clear auction again + (address _highestBidder2, uint256 _price2, uint256 _tax2) = operator.clearAuction(_tokenId, _epoch); + assertEq(_highestBidder2, USER_A); + assertEq(_price2, _price); + assertEq(_tax2, _tax); + } - vm.startPrank(USER_B); - deal(address(usdt), USER_B, 0); - operator.placeBid(_tokenId2, 0); + function testClearAuctions() public { + (uint256 _tokenId1, IBillboardRegistry.Board memory _board1) = _mintBoard(); + (uint256 _tokenId2, IBillboardRegistry.Board memory _board2) = _mintBoard(); + uint256 _epoch1 = operator.getEpochFromBlock(_board1.startedAt, block.number, _board1.epochInterval); + uint256 _epoch2 = operator.getEpochFromBlock(_board2.startedAt, block.number, _board2.epochInterval); + _placeBid(_tokenId1, _epoch1, USER_A, 1 ether); + _placeBid(_tokenId2, _epoch2, USER_B, 1 ether); - // clear auction + uint256 _clearedAt = operator.getBlockFromEpoch(_board1.startedAt, _epoch1 + 1, _board1.epochInterval); + + // clear auctions vm.expectEmit(true, true, true, true); - emit IBillboardRegistry.AuctionCleared( - _tokenId, - _prevAuctionId + 1, - USER_A, - _clearedAt, - _clearedAt + registry.leaseTerm() - ); + emit IBillboardRegistry.AuctionCleared(_tokenId1, _epoch1, USER_A); vm.expectEmit(true, true, true, true); - emit IBillboardRegistry.AuctionCleared( - _tokenId2, - _prevAuctionId2 + 1, - USER_B, - _clearedAt, - _clearedAt + registry.leaseTerm() - ); + emit IBillboardRegistry.AuctionCleared(_tokenId2, _epoch2, USER_B); vm.roll(_clearedAt); uint256[] memory _tokenIds = new uint256[](2); - _tokenIds[0] = _tokenId; + uint256[] memory _epochs = new uint256[](2); + _tokenIds[0] = _tokenId1; _tokenIds[1] = _tokenId2; - (uint256[] memory prices, uint256[] memory taxes) = operator.clearAuctions(_tokenIds); - assertEq(prices[0], 0); - assertEq(prices[1], 0); - assertEq(taxes[0], 0); - assertEq(taxes[1], 0); - - // check auction - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - IBillboardRegistry.Auction memory _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.startAt, _placedAt); - assertEq(_auction.endAt, _placedAt + registry.leaseTerm()); - assertEq(_auction.leaseStartAt, _clearedAt); - assertEq(_auction.leaseEndAt, _clearedAt + registry.leaseTerm()); - assertEq(_auction.highestBidder, USER_A); - - uint256 _nextAuctionId2 = registry.nextBoardAuctionId(_tokenId2); - IBillboardRegistry.Auction memory _auction2 = registry.getAuction(_tokenId2, _nextAuctionId2); - assertEq(_auction2.startAt, _placedAt); - assertEq(_auction2.endAt, _placedAt + registry.leaseTerm()); - assertEq(_auction2.leaseStartAt, _clearedAt); - assertEq(_auction2.leaseEndAt, _clearedAt + registry.leaseTerm()); - assertEq(_auction2.highestBidder, USER_B); + _epochs[0] = _epoch1; + _epochs[1] = _epoch2; + (address[] memory highestBidders, , ) = operator.clearAuctions(_tokenIds, _epochs); + assertEq(highestBidders[0], USER_A); + assertEq(highestBidders[1], USER_B); - // check bid - IBillboardRegistry.Bid memory _bid = registry.getBid(_tokenId, _nextAuctionId, USER_A); - assertEq(_bid.price, 0); - assertEq(_bid.tax, 0); - assertEq(_bid.placedAt, _placedAt); - assertEq(_bid.isWon, true); - assertEq(_bid.isWithdrawn, false); + // check auction & bids + IBillboardRegistry.Bid memory _bid1 = registry.getBid(_tokenId1, _epoch1, USER_A); + assertEq(_bid1.isWon, true); - IBillboardRegistry.Bid memory _bid2 = registry.getBid(_tokenId2, _nextAuctionId2, USER_B); - assertEq(_bid2.price, 0); - assertEq(_bid2.tax, 0); - assertEq(_bid2.placedAt, _placedAt); + IBillboardRegistry.Bid memory _bid2 = registry.getBid(_tokenId2, _epoch2, USER_B); assertEq(_bid2.isWon, true); - assertEq(_bid2.isWithdrawn, false); } - function testCannotClearAuctionOnNewBoard() public { - uint256 _mintedAt = block.number; - uint256 _clearedAt = _mintedAt + 1; - uint256 _tokenId = _mintBoard(); + function testCannotClearAuctionIfClosed() public { + (uint256 _tokenId, ) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(block.number, block.number, 1); vm.startPrank(ADMIN); + operator.setClosed(_tokenId, true); - // clear auction - vm.roll(_clearedAt); - vm.expectRevert("Auction not found"); - operator.clearAuction(_tokenId); + vm.expectRevert("Closed"); + operator.clearAuction(_tokenId, _epoch); } function testCannotClearAuctionIfAuctionNotEnded() public { - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _endedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); - // place a bid - vm.startPrank(USER_A); - deal(address(usdt), USER_A, 0); - operator.placeBid(_tokenId, 0); - - // try to clear auction vm.expectRevert("Auction not ended"); - operator.clearAuction(_tokenId); + operator.clearAuction(_tokenId, _epoch); - vm.roll(block.number + registry.leaseTerm() - 1); + vm.roll(_endedAt - 1); vm.expectRevert("Auction not ended"); - operator.clearAuction(_tokenId); + operator.clearAuction(_tokenId, _epoch); + } + + function testCannotClearAuctionIfNoBid() public { + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _clearedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); + + vm.roll(_clearedAt); + vm.expectRevert("No bid"); + operator.clearAuction(_tokenId, _epoch); + } + + function testClearLastAuction(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 _clearedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); + + // place bid + _placeBid(_tokenId, _epoch, USER_A, _price); + + // clear auction + vm.expectEmit(true, true, true, false); + emit IBillboardRegistry.AuctionCleared(_tokenId, _epoch, USER_A); + + vm.roll(_clearedAt); + operator.clearLastAuction(_tokenId); + + // check balances + assertEq(usdt.balanceOf(address(registry)), _tax); + assertEq(usdt.balanceOf(ADMIN), _price); + assertEq(usdt.balanceOf(USER_A), 0); + } + + function testClearLastAuctions() public { + (uint256 _tokenId1, IBillboardRegistry.Board memory _board1) = _mintBoard(); + (uint256 _tokenId2, IBillboardRegistry.Board memory _board2) = _mintBoard(); + uint256 _epoch1 = operator.getEpochFromBlock(_board1.startedAt, block.number, _board1.epochInterval); + uint256 _epoch2 = operator.getEpochFromBlock(_board2.startedAt, block.number, _board2.epochInterval); + _placeBid(_tokenId1, _epoch1, USER_A, 1 ether); + _placeBid(_tokenId2, _epoch2, USER_B, 1 ether); + + uint256 _clearedAt = operator.getBlockFromEpoch(_board1.startedAt, _epoch1 + 1, _board1.epochInterval); + + // clear auctions + vm.expectEmit(true, true, true, true); + emit IBillboardRegistry.AuctionCleared(_tokenId1, _epoch1, USER_A); + vm.expectEmit(true, true, true, true); + emit IBillboardRegistry.AuctionCleared(_tokenId2, _epoch2, USER_B); + + vm.roll(_clearedAt); + + uint256[] memory _tokenIds = new uint256[](2); + _tokenIds[0] = _tokenId1; + _tokenIds[1] = _tokenId2; + operator.clearLastAuctions(_tokenIds); + + // check auction & bids + IBillboardRegistry.Bid memory _bid1 = registry.getBid(_tokenId1, _epoch1, USER_A); + assertEq(_bid1.isWon, true); + + IBillboardRegistry.Bid memory _bid2 = registry.getBid(_tokenId2, _epoch2, USER_B); + assertEq(_bid2.isWon, true); } function testGetBids(uint8 _bidCount, uint8 _limit, uint8 _offset) public { @@ -753,30 +625,30 @@ contract BillboardTest is BillboardTestBase { vm.assume(_limit <= _bidCount); vm.assume(_offset <= _limit); - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); for (uint8 i = 0; i < _bidCount; i++) { address _bidder = address(uint160(2000 + i)); vm.prank(ADMIN); - operator.addToWhitelist(_bidder); + operator.setWhitelist(_tokenId, _bidder, true); - uint256 _amount = 1 ether + i; - uint256 _tax = operator.calculateTax(_amount); - uint256 _totalAmount = _amount + _tax; + uint256 _price = 1 ether + i; + uint256 _tax = operator.calculateTax(_tokenId, _price); + uint256 _totalAmount = _price + _tax; deal(address(usdt), _bidder, _totalAmount); vm.startPrank(_bidder); usdt.approve(address(operator), _totalAmount); - operator.placeBid(_tokenId, _amount); + operator.placeBid(_tokenId, _epoch, _price); vm.stopPrank(); } // get bids - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); (uint256 _t, uint256 _l, uint256 _o, IBillboardRegistry.Bid[] memory _bids) = operator.getBids( _tokenId, - _nextAuctionId, + _epoch, _limit, _offset ); @@ -787,274 +659,486 @@ contract BillboardTest is BillboardTestBase { assertEq(_bids.length, _size); assertEq(_o, _offset); for (uint256 i = 0; i < _size; i++) { - uint256 _amount = 1 ether + _offset + i; - assertEq(_bids[i].price, _amount); + uint256 _price = 1 ether + _offset + i; + assertEq(_bids[i].price, _price); } } - ////////////////////////////// - /// Tax & Withdraw - ////////////////////////////// + // Main function to test getting bidder bids + function testGetBidderBids(uint8 _bidCount, uint8 _limit, uint8 _offset) public { + vm.assume(_bidCount > 0); + vm.assume(_bidCount <= 10); + vm.assume(_limit <= _bidCount); + vm.assume(_offset <= _limit); - function testCalculateTax() public { - uint256 _amount = 100; - uint256 _taxRate = 10; // 10% per lease term + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); - vm.startPrank(ADMIN); - operator.setTaxRate(_taxRate); + _placeBids(_bidCount, _tokenId, _board); + + // Get bidder bids + ( + uint256 totalBids, + uint256 limit, + uint256 offset, + IBillboardRegistry.Bid[] memory bids, + uint256[] memory epoches + ) = operator.getBidderBids(_tokenId, USER_A, _limit, _offset); - uint256 _tax = operator.calculateTax(_amount); - assertEq(_tax, (_amount * _taxRate) / 1000); + _assertBidderBids(_bidCount, _limit, _offset, totalBids, limit, offset, bids, epoches, _board); } - function testSetTaxRate() public { - vm.startPrank(ADMIN); + // Helper function to place bids + function _placeBids(uint8 _bidCount, uint256 _tokenId, IBillboardRegistry.Board memory _board) internal { + for (uint8 i = 0; i < _bidCount; i++) { + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval) + i; - vm.expectEmit(true, true, true, true); - emit IBillboardRegistry.TaxRateUpdated(2); + vm.prank(ADMIN); + operator.setWhitelist(_tokenId, USER_A, true); - operator.setTaxRate(2); - assertEq(operator.getTaxRate(), 2); - } + uint256 _price = 1 ether + i; + uint256 _tax = operator.calculateTax(_tokenId, _price); + uint256 _totalAmount = _price + _tax; - function testCannotSetTaxRateByAttacker() public { - vm.startPrank(ATTACKER); + deal(address(usdt), USER_A, _totalAmount); + vm.startPrank(USER_A); + usdt.approve(address(operator), _totalAmount); + operator.placeBid(_tokenId, _epoch, _price); + vm.stopPrank(); + } + } - vm.expectRevert("Admin"); - operator.setTaxRate(2); + // Helper function to assert bidder bids + function _assertBidderBids( + uint8 _bidCount, + uint8 _limit, + uint8 _offset, + uint256 totalBids, + uint256 limit, + uint256 offset, + IBillboardRegistry.Bid[] memory bids, + uint256[] memory epoches, + IBillboardRegistry.Board memory _board + ) internal { + uint256 remainingBids = totalBids - offset; + uint256 size = remainingBids > limit ? limit : remainingBids; + + assertEq(totalBids, _bidCount); + assertEq(limit, _limit); + assertEq(bids.length, size); + assertEq(offset, _offset); + + for (uint256 i = 0; i < size; i++) { + uint256 expectedPrice = 1 ether + _offset + i; + uint256 expectedEpoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval) + + _offset + + i; + + assertEq(bids[i].price, expectedPrice); + assertEq(epoches[i], expectedEpoch); + } } - function testWithdrawTax(uint96 _amount) public { - vm.assume(_amount > 0.001 ether); + function testWithdrawBid(uint96 _price) public { + vm.assume(_price > 0.001 ether); - uint256 _tokenId = _mintBoard(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + (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; + uint256 _clearedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); - vm.prank(ADMIN); - operator.addToWhitelist(USER_A); + // new bid with USER_A + _placeBid(_tokenId, _epoch, USER_A, _price); - // place a bid and win auction - deal(address(usdt), USER_A, _total); - vm.prank(USER_A); - operator.placeBid(_tokenId, _amount); + // new bid with USER_B + _placeBid(_tokenId, _epoch, USER_B, _price); - uint256 _prevRegistryBalance = usdt.balanceOf(address(registry)); - uint256 _prevAdminBalance = usdt.balanceOf(ADMIN); + // clear auction + vm.roll(_clearedAt); + operator.clearAuction(_tokenId, _epoch); - // withdraw tax - vm.expectEmit(true, true, true, true); - emit IBillboardRegistry.TaxWithdrawn(ADMIN, _tax); + // check bid + IBillboardRegistry.Bid memory _bidA = registry.getBid(_tokenId, _epoch, USER_A); + assertEq(_bidA.isWon, true); + IBillboardRegistry.Bid memory _bidB = registry.getBid(_tokenId, _epoch, USER_B); + assertEq(_bidB.isWon, false); - vm.prank(ADMIN); - operator.withdrawTax(); + // withdraw bid + vm.expectEmit(true, true, true, false); + emit IBillboardRegistry.BidWithdrawn(_tokenId, _epoch, USER_B); + + vm.prank(USER_B); + operator.withdrawBid(_tokenId, _epoch, USER_B); // check balances - assertEq(usdt.balanceOf(address(registry)), _prevRegistryBalance - _tax); - assertEq(usdt.balanceOf(ADMIN), _prevAdminBalance + _tax); + assertEq(usdt.balanceOf(USER_A), 0); + assertEq(usdt.balanceOf(USER_B), _total); } - function testCannnotWithdrawTaxIfZero() public { - uint256 _tokenId = _mintBoard(); + 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.addToWhitelist(USER_A); + operator.setClosed(_tokenId, true); + + // withdraw bid + vm.expectEmit(true, true, true, false); + emit IBillboardRegistry.BidWithdrawn(_tokenId, _epoch, USER_A); - // place a bid and win auction - deal(address(usdt), USER_A, 0); vm.prank(USER_A); - operator.placeBid(_tokenId, 0); + operator.withdrawBid(_tokenId, _epoch, USER_A); - vm.prank(ADMIN); - vm.expectRevert("Zero amount"); - operator.withdrawTax(); + // 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 testCannnotWithdrawTaxIfSmallAmount(uint8 _amount) public { - uint256 _tax = operator.calculateTax(_amount); - vm.assume(_tax <= 0); + function testCannotWithdrawBidTwice(uint96 _price) public { + vm.assume(_price > 0.001 ether); - uint256 _tokenId = _mintBoard(); + (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; + uint256 _clearedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); - vm.prank(ADMIN); - operator.addToWhitelist(USER_A); + // new bid with USER_A + _placeBid(_tokenId, _epoch, USER_A, _price); - // place a bid and win auction - deal(address(usdt), USER_A, _amount); - vm.prank(USER_A); - operator.placeBid(_tokenId, _amount); + // new bid with USER_B + _placeBid(_tokenId, _epoch, USER_B, _price); - vm.prank(ADMIN); - vm.expectRevert("Zero amount"); - operator.withdrawTax(); - } + // clear auction + vm.roll(_clearedAt); + operator.clearAuction(_tokenId, _epoch); - function testCannotWithdrawTaxByAttacker() public { - vm.startPrank(ATTACKER); + // withdraw bid + vm.prank(USER_B); + operator.withdrawBid(_tokenId, _epoch, USER_B); + assertEq(usdt.balanceOf(USER_B), _total); - vm.expectRevert("Zero amount"); - operator.withdrawTax(); + // withdraw bid again + vm.prank(USER_B); + vm.expectRevert("Bid already withdrawn"); + operator.withdrawBid(_tokenId, _epoch, USER_B); } - function testWithdrawBid(uint96 _amount) public { - vm.assume(_amount > 0.001 ether); + function testCannotWithdrawBidIfWon(uint96 _price) public { + vm.assume(_price > 0.001 ether); - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _clearedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); - // new auction and new bid with USER_A - deal(address(usdt), USER_A, _total); - vm.prank(USER_A); - operator.placeBid(_tokenId, _amount); + // new bid with USER_A + _placeBid(_tokenId, _epoch, USER_A, _price); // new bid with USER_B - deal(address(usdt), USER_B, _total); - vm.prank(USER_B); - operator.placeBid(_tokenId, _amount); + _placeBid(_tokenId, _epoch, USER_B, _price); // clear auction - vm.roll(block.number + registry.leaseTerm() + 1); - operator.clearAuction(_tokenId); + vm.roll(_clearedAt); + operator.clearAuction(_tokenId, _epoch); - // check auction - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - IBillboardRegistry.Auction memory _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.highestBidder, USER_A); + // withdraw bid + vm.prank(USER_A); + vm.expectRevert("Bid already won"); + operator.withdrawBid(_tokenId, _epoch, USER_A); + } - // check bid - IBillboardRegistry.Bid memory _bidA = registry.getBid(_tokenId, _nextAuctionId, USER_A); - assertEq(_bidA.isWon, true); - IBillboardRegistry.Bid memory _bidB = registry.getBid(_tokenId, _nextAuctionId, USER_B); - assertEq(_bidB.isWon, false); + function testCannotWithdrawBidIfAuctionNotEndedOrCleared(uint96 _price) public { + vm.assume(_price > 0.001 ether); - // withdraw bid - vm.expectEmit(true, true, true, true); - emit IBillboardRegistry.BidWithdrawn(_tokenId, _nextAuctionId, USER_B, _amount, _tax); + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _clearedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); - vm.prank(USER_B); - operator.withdrawBid(_tokenId, _nextAuctionId); - assertEq(usdt.balanceOf(USER_B), _total); + // new bid with USER_A + _placeBid(_tokenId, _epoch, USER_A, _price); + + // auction is not ended + vm.roll(_clearedAt - 1); + vm.expectRevert("Auction not ended"); + operator.withdrawBid(_tokenId, _epoch, USER_A); + + // auction is ended but not cleared + vm.roll(_clearedAt); + vm.expectRevert("Auction not cleared"); + operator.withdrawBid(_tokenId, _epoch, USER_A); } - function testCannotWithBidTwice(uint96 _amount) public { - vm.assume(_amount > 0.001 ether); + function testCannotWithdrawBidIfNotFound(uint96 _price) public { + vm.assume(_price > 0.001 ether); - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _clearedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); - // new auction and new bid with USER_A - deal(address(usdt), USER_A, _total); - vm.prank(USER_A); - operator.placeBid(_tokenId, _amount); + // new bid with USER_A + _placeBid(_tokenId, _epoch, USER_A, _price); + + // clear auction + vm.roll(_clearedAt); + operator.clearAuction(_tokenId, _epoch); - // new bid with USER_B - deal(address(usdt), USER_B, _total); vm.prank(USER_B); - operator.placeBid(_tokenId, _amount); + vm.expectRevert("Bid not found"); + operator.withdrawBid(_tokenId, _epoch, USER_B); + } - // clear auction - vm.roll(block.number + registry.leaseTerm() + 1); - operator.clearAuction(_tokenId); + function testGetEpochFromBlock() public { + // epoch interval = 1 + assertEq(operator.getEpochFromBlock(0, 0, 1), 0); + assertEq(operator.getEpochFromBlock(0, 1, 1), 1); + assertEq(operator.getEpochFromBlock(100, 100, 1), 0); + assertEq(operator.getEpochFromBlock(100, 101, 1), 1); + + // epoch interval = 101 + assertEq(operator.getEpochFromBlock(0, 0, 101), 0); + assertEq(operator.getEpochFromBlock(0, 1, 101), 0); + assertEq(operator.getEpochFromBlock(0, 100, 101), 0); + assertEq(operator.getEpochFromBlock(0, 101, 101), 1); + assertEq(operator.getEpochFromBlock(0, 203, 101), 2); + + // epoch interval = MAX + assertEq(operator.getEpochFromBlock(0, 0, type(uint256).max), 0); + assertEq(operator.getEpochFromBlock(0, 1, type(uint256).max), 0); + assertEq(operator.getEpochFromBlock(0, type(uint256).max, type(uint256).max), 1); + } - // check auction - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - IBillboardRegistry.Auction memory _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.highestBidder, USER_A); + function testCannotGetEpochFromBlock() public { + // panic: division or modulo by zero + vm.expectRevert(); + operator.getEpochFromBlock(0, 0, 0); + vm.expectRevert(); + operator.getEpochFromBlock(0, 1, 0); + vm.expectRevert(); + operator.getEpochFromBlock(0, 0, type(uint256).min); + + // panic: arithmetic underflow or overflow + vm.expectRevert(); + operator.getEpochFromBlock(100, 99, 1); + vm.expectRevert(); + operator.getEpochFromBlock(0, type(uint256).max + 1, type(uint256).max); + vm.expectRevert(); + operator.getEpochFromBlock(0, type(uint256).min - 1, type(uint256).min); + } - // withdraw bid - vm.prank(USER_B); - operator.withdrawBid(_tokenId, _nextAuctionId); - assertEq(usdt.balanceOf(USER_B), _total); + function testGetBlockFromEpoch() public { + // epoch interval = 1 + assertEq(operator.getBlockFromEpoch(0, 0, 1), 0); + assertEq(operator.getBlockFromEpoch(0, 1, 1), 1); + assertEq(operator.getBlockFromEpoch(100, 0, 1), 100); + assertEq(operator.getBlockFromEpoch(100, 1, 1), 101); + + // epoch interval = 101 + assertEq(operator.getBlockFromEpoch(0, 0, 101), 0); + assertEq(operator.getBlockFromEpoch(0, 1, 101), 101); + assertEq(operator.getBlockFromEpoch(0, 2, 101), 202); + + // epoch interval = MAX + assertEq(operator.getBlockFromEpoch(0, 0, type(uint256).max), 0); + assertEq(operator.getBlockFromEpoch(0, 1, type(uint256).max), type(uint256).max); + + // epoch interval = MIN + assertEq(operator.getBlockFromEpoch(0, 0, type(uint256).min), 0); + assertEq(operator.getBlockFromEpoch(0, 1, type(uint256).min), 0); + } - // withdraw bid again - vm.prank(USER_B); - vm.expectRevert("Bid already withdrawn"); - operator.withdrawBid(_tokenId, _nextAuctionId); + function testCannotGetBlockFromEpoch() public { + // panic: arithmetic underflow or overflow + vm.expectRevert(); + operator.getBlockFromEpoch(0, 2, type(uint256).max); + } + + ////////////////////////////// + /// Tax + ////////////////////////////// + + function testCalculateTax() public { + vm.prank(ADMIN); + uint256 _price = 1000; + uint256 _taxRate = 2; + uint256 _tokenId = operator.mintBoard(_taxRate, EPOCH_INTERVAL); + uint256 _tax = operator.calculateTax(_tokenId, _price); + assertEq(_tax, 2); + + vm.prank(ADMIN); + uint256 _price1 = 1007; + uint256 _taxRate1 = 2; + uint256 _tokenId1 = operator.mintBoard(_taxRate1, EPOCH_INTERVAL); + uint256 _tax1 = operator.calculateTax(_tokenId1, _price1); + assertEq(_tax1, 2); + + vm.prank(ADMIN); + uint256 _price2 = 0; + uint256 _taxRate2 = 100; + uint256 _tokenId2 = operator.mintBoard(_taxRate2, EPOCH_INTERVAL); + uint256 _tax2 = operator.calculateTax(_tokenId2, _price2); + assertEq(_tax2, 0); } - function testCannotWithdrawBidIfWon(uint96 _amount) public { - vm.assume(_amount > 0.001 ether); + function testCannotCalculateTax() public { + vm.prank(ADMIN); + uint256 _price = 1000; + uint256 _taxRate = type(uint256).max; + uint256 _tokenId = operator.mintBoard(_taxRate, EPOCH_INTERVAL); + vm.expectRevert(); + operator.calculateTax(_tokenId, _price); + } - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + function testWithdrawTax(uint96 _price) public { + vm.assume(_price > 0.001 ether); - // new auction and new bid with USER_A - deal(address(usdt), USER_A, _total); - vm.prank(USER_A); - operator.placeBid(_tokenId, _amount); + (uint256 _tokenId, IBillboardRegistry.Board memory _board) = _mintBoard(); + uint256 _epoch = operator.getEpochFromBlock(_board.startedAt, block.number, _board.epochInterval); + uint256 _tax = operator.calculateTax(_tokenId, _price); + uint256 _clearedAt = operator.getBlockFromEpoch(_board.startedAt, _epoch + 1, _board.epochInterval); + + // place bid with USER_A + _placeBid(_tokenId, _epoch, USER_A, _price); // clear auction - vm.roll(block.number + registry.leaseTerm() + 1); - operator.clearAuction(_tokenId); + vm.roll(_clearedAt); + operator.clearAuction(_tokenId, _epoch); - // check auction - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - IBillboardRegistry.Auction memory _auction = registry.getAuction(_tokenId, _nextAuctionId); - assertEq(_auction.highestBidder, USER_A); + uint256 _prevRegistryBalance = usdt.balanceOf(address(registry)); + uint256 _prevAdminBalance = usdt.balanceOf(ADMIN); + (uint256 _taxAccumulated, uint256 _taxWithdrawn) = registry.taxTreasury(ADMIN); + assertEq(_taxAccumulated, _tax); + assertEq(_taxWithdrawn, 0); - // withdraw bid - vm.prank(USER_A); - vm.expectRevert("Bid already won"); - operator.withdrawBid(_tokenId, _nextAuctionId); + // withdraw tax + vm.expectEmit(true, true, true, true); + emit IBillboardRegistry.TaxWithdrawn(ADMIN, _tax); + + vm.prank(ADMIN); + operator.withdrawTax(ADMIN); + + // check balances + assertEq(usdt.balanceOf(address(registry)), _prevRegistryBalance - _tax); + assertEq(usdt.balanceOf(ADMIN), _prevAdminBalance + _tax); } - function testCannotWithdrawBidIfAuctionNotEnded(uint96 _amount) public { - vm.assume(_amount > 0.001 ether); + function testCannnotWithdrawTaxIfZero() public { + (uint256 _taxAccumulated, uint256 _taxWithdrawn) = registry.taxTreasury(ADMIN); + assertEq(_taxAccumulated, 0); + assertEq(_taxWithdrawn, 0); + + vm.prank(ADMIN); + vm.expectRevert("Zero amount"); + operator.withdrawTax(ADMIN); + } - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + ////////////////////////////// + /// ERC20 & ERC721 related + ////////////////////////////// - // new auction and new bid with USER_A - vm.startPrank(USER_A); - deal(address(usdt), USER_A, _total); - operator.placeBid(_tokenId, _amount); + function testCannotTransferToZeroAddress() public { + (uint256 _tokenId, ) = _mintBoard(); - // auction is not ended - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - vm.expectRevert("Auction not ended"); - operator.withdrawBid(_tokenId, _nextAuctionId); + vm.startPrank(ADMIN); - // auction is ended but not cleared - vm.roll(block.number + registry.leaseTerm() + 1); - vm.expectRevert("Auction not cleared"); - operator.withdrawBid(_tokenId, _nextAuctionId); + vm.expectRevert("ERC721: transfer to the zero address"); + registry.transferFrom(ADMIN, ZERO_ADDRESS, _tokenId); } - function testCannotWithdrawBidIfAuctionNotCleared(uint96 _amount) public { - vm.assume(_amount > 0.001 ether); + function testCannotTransferByOperator() public { + (uint256 _tokenId, ) = _mintBoard(); - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); - uint256 _tax = operator.calculateTax(_amount); - uint256 _total = _amount + _tax; + vm.startPrank(address(operator)); - // new auction and new bid with USER_A - deal(address(usdt), USER_A, _total); - vm.prank(USER_A); - operator.placeBid(_tokenId, _amount); + vm.expectRevert("ERC721: caller is not token owner or approved"); + registry.transferFrom(USER_B, USER_C, _tokenId); + } - // new bid with USER_B - deal(address(usdt), USER_B, _total); - vm.prank(USER_B); - operator.placeBid(_tokenId, _amount); + function testSafeTransferByOperator() public { + (uint256 _tokenId, ) = _mintBoard(); - // auction is ended but not cleared - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); - vm.roll(block.number + registry.leaseTerm() + 1); - vm.prank(USER_B); - vm.expectRevert("Auction not cleared"); - operator.withdrawBid(_tokenId, _nextAuctionId); + vm.expectEmit(true, true, true, true); + emit IERC721.Transfer(ADMIN, USER_A, _tokenId); + + vm.startPrank(address(operator)); + registry.safeTransferByOperator(ADMIN, USER_A, _tokenId); + assertEq(registry.ownerOf(_tokenId), USER_A); } - function testCannotWithdrawBidIfNotFound() public { - (uint256 _tokenId, ) = _mintBoardAndPlaceBid(); - uint256 _nextAuctionId = registry.nextBoardAuctionId(_tokenId); + function testCannotSafeTransferByAttacker() public { + (uint256 _tokenId, ) = _mintBoard(); + + vm.startPrank(ATTACKER); + vm.expectRevert("Operator"); + registry.safeTransferByOperator(ADMIN, ATTACKER, _tokenId); + } + + function testApproveAndTransfer() public { + (uint256 _tokenId, ) = _mintBoard(); + + vm.expectEmit(true, true, true, true); + emit IERC721.Approval(ADMIN, USER_A, _tokenId); + vm.prank(ADMIN); + registry.approve(USER_A, _tokenId); + assertEq(registry.getApproved(_tokenId), USER_A); + + vm.expectEmit(true, true, true, true); + emit IERC721.Transfer(ADMIN, USER_A, _tokenId); vm.prank(USER_A); - vm.expectRevert("Bid not found"); - operator.withdrawBid(_tokenId, _nextAuctionId); + registry.transferFrom(ADMIN, USER_A, _tokenId); + + IBillboardRegistry.Board memory board = operator.getBoard(_tokenId); + assertEq(board.creator, ADMIN); + assertEq(registry.ownerOf(_tokenId), USER_A); + } + + function testCannotApproveByAttacker() public { + (uint256 _tokenId, ) = _mintBoard(); + + vm.stopPrank(); + vm.startPrank(ATTACKER); + vm.expectRevert("ERC721: approve caller is not token owner or approved for all"); + registry.approve(USER_A, _tokenId); + } + + function testGetTokenURI() public { + (uint256 _tokenId, ) = _mintBoard(); + + vm.startPrank(ADMIN); + + // new board + string memory json = Base64.encode( + bytes(string(abi.encodePacked('{"name": "Billboard #1", "description": "", "location": "", "image": ""}'))) + ); + assertEq(registry.tokenURI(_tokenId), string(abi.encodePacked("data:application/json;base64,", json))); + + // set board data + string memory _name = "name"; + string memory _description = "description"; + string memory _imageURI = "image URI"; + string memory _location = "location"; + operator.setBoard(_tokenId, _name, _description, _imageURI, _location); + + string memory newJson = Base64.encode( + bytes( + string( + abi.encodePacked( + '{"name": "Billboard #1", "description": "description", "location": "location", "image": "image URI"}' + ) + ) + ) + ); + assertEq(registry.tokenURI(_tokenId), string(abi.encodePacked("data:application/json;base64,", newJson))); } } diff --git a/src/test/Billboard/BillboardTestBase.t.sol b/src/test/Billboard/BillboardTestBase.t.sol index 6ba6849..ba03350 100644 --- a/src/test/Billboard/BillboardTestBase.t.sol +++ b/src/test/Billboard/BillboardTestBase.t.sol @@ -16,8 +16,8 @@ contract BillboardTestBase is Test { BillboardRegistry internal registry; USDT internal usdt; - uint256 constant TAX_RATE = 1; // 1% per lease term - uint64 constant LEASE_TERM = 100; // 100 blocks + uint256 constant TAX_RATE = 1024; // 10.24% per epoch + uint256 constant EPOCH_INTERVAL = 100; // 100 blocks address constant ZERO_ADDRESS = address(0); address constant FAKE_CONTRACT = address(1); @@ -36,7 +36,7 @@ contract BillboardTestBase is Test { usdt = new USDT(ADMIN, 0); // deploy operator & registry - operator = new Billboard(address(usdt), payable(address(0)), ADMIN, TAX_RATE, LEASE_TERM, "Billboard", "BLBD"); + operator = new Billboard(address(usdt), payable(address(0)), ADMIN, "Billboard", "BLBD"); registry = operator.registry(); assertEq(operator.admin(), ADMIN); assertEq(registry.operator(), address(operator)); @@ -57,25 +57,22 @@ contract BillboardTestBase is Test { usdt.approve(address(operator), MAX_ALLOWANCE); } - function _mintBoard() public returns (uint256 tokenId) { + function _mintBoard() public returns (uint256 tokenId, IBillboardRegistry.Board memory board) { vm.prank(ADMIN); - tokenId = operator.mintBoard(ADMIN); + tokenId = operator.mintBoard(TAX_RATE, EPOCH_INTERVAL); + board = registry.getBoard(tokenId); } - function _mintBoardAndPlaceBid() public returns (uint256 tokenId, uint256 _nextAuctionId) { - tokenId = _mintBoard(); + function _placeBid(uint256 _tokenId, uint256 _epoch, address _bidder, uint256 _price) public { + uint256 _tax = operator.calculateTax(_tokenId, _price); + uint256 _total = _price + _tax; - // (new board) ADMIN places first bid and takes the ownership - vm.startPrank(ADMIN); - operator.placeBid(tokenId, 0); - _nextAuctionId = registry.nextBoardAuctionId(tokenId); - IBillboardRegistry.Auction memory _auction = registry.getAuction(tokenId, _nextAuctionId); - assertEq(_nextAuctionId, 1); - assertEq(_auction.highestBidder, ADMIN); - - // add USER_A and USER_B to whitelist - operator.addToWhitelist(USER_A); - operator.addToWhitelist(USER_B); - vm.stopPrank(); + deal(address(usdt), _bidder, _total); + + vm.prank(ADMIN); + operator.setWhitelist(_tokenId, _bidder, true); + + vm.prank(_bidder); + operator.placeBid(_tokenId, _epoch, _price); } } diff --git a/src/test/TheSpace/TheSpace.t.sol b/src/test/TheSpace/TheSpace.t.sol index f62e7d4..d52af61 100644 --- a/src/test/TheSpace/TheSpace.t.sol +++ b/src/test/TheSpace/TheSpace.t.sol @@ -447,7 +447,7 @@ contract TheSpaceTest is BaseTheSpaceTest { assertEq(thespace.getPrice(PIXEL_ID), price); } - function testSetPriceByOperator(uint256 price) public { + function testSetPriceByOperator(uint96 price) public { vm.assume(price <= registry.currency().totalSupply()); vm.assume(price > 0);