From 126f0d80d5f72c0fca2e64c4585ea69c8f08a072 Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 24 Jan 2025 12:57:31 +0800 Subject: [PATCH 1/4] feat: move spotlight ip collection to this repo --- .../SpotlightIPCollection.sol | 96 +++++++ test/SpotlightIPCollection.t.sol | 247 ++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 src/spotlight-ip-collection/SpotlightIPCollection.sol create mode 100644 test/SpotlightIPCollection.t.sol diff --git a/src/spotlight-ip-collection/SpotlightIPCollection.sol b/src/spotlight-ip-collection/SpotlightIPCollection.sol new file mode 100644 index 0000000..99b913b --- /dev/null +++ b/src/spotlight-ip-collection/SpotlightIPCollection.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.13; + +import {ERC721} from "../../lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {Ownable} from "../../lib/openzeppelin-contracts/contracts/access/Ownable.sol"; + +contract SpotlightIPCollection is ERC721, Ownable { + bool private _isTransferEnabled = false; + bool private _isMintEnabled = false; + + uint256 private _nextTokenId; + string private _tokenURI; + + modifier onlyTransferEnabled() { + _checkTransferEnabled(); + _; + } + + modifier onlyMintEnabled() { + _checkMintEnabled(); + _; + } + + constructor() ERC721("Spotlight IP", "SPIP") Ownable(msg.sender) {} + + function totalSupply() external view returns (uint256) { + return _nextTokenId; + } + + function _baseURI() internal view override returns (string memory) { + return _tokenURI; + } + + function tokenURI(uint256 tokenId) public view override returns (string memory) { + _requireOwned(tokenId); + return _tokenURI; + } + + function setTokenURI(string memory tokenURI_) public onlyOwner { + _tokenURI = tokenURI_; + } + + function isMintEnabled() public view returns (bool) { + return _isMintEnabled; + } + + function _checkMintEnabled() private view { + if (isMintEnabled() != true) { + revert("SpotlightIPCollection: mint is disabled"); + } + } + + function setMintEnabled(bool enabled) public onlyOwner { + _isMintEnabled = enabled; + } + + function mint(address to) public onlyOwner onlyMintEnabled returns (uint256) { + uint256 tokenId = _nextTokenId; + _mint(to, tokenId); + _nextTokenId = _nextTokenId + 1; + return tokenId; + } + + function mint() public onlyMintEnabled returns (uint256) { + uint256 tokenId = _nextTokenId; + _mint(msg.sender, tokenId); + _nextTokenId = _nextTokenId + 1; + return tokenId; + } + + function isTransferEnabled() public view returns (bool) { + return _isTransferEnabled; + } + + function _checkTransferEnabled() private view { + if (isTransferEnabled() != true) { + revert("SpotlightIPCollection: transfer is disabled"); + } + } + + function setTransferEnabled(bool enabled) public onlyOwner { + _isTransferEnabled = enabled; + } + + function transferFrom(address from, address to, uint256 tokenId) public override onlyTransferEnabled { + super.transferFrom(from, to, tokenId); + } + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) + public + override + onlyTransferEnabled + { + super.safeTransferFrom(from, to, tokenId, data); + } +} diff --git a/test/SpotlightIPCollection.t.sol b/test/SpotlightIPCollection.t.sol new file mode 100644 index 0000000..f11bde2 --- /dev/null +++ b/test/SpotlightIPCollection.t.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.13; +import "../lib/forge-std/src/Test.sol"; +import {SpotlightIPCollection} from "../src/spotlight-ip-collection/SpotlightIPCollection.sol"; + +abstract contract OwnableError { + error OwnableUnauthorizedAccount(address account); + error OwnableInvalidOwner(address owner); +} + +contract SpotlightIPCollectionTest is Test { + address private _ownerAddr; + + SpotlightIPCollection private _spotlightIPCollection; + address private _spotlightIPCollectionAddr; + + string private originalTokenURI = "https://example.com/original-token/"; + string private newTokenURI = "https://example.com/new-token/"; + + function setUp() public { + _ownerAddr = makeAddr("owner"); + vm.startPrank(_ownerAddr); + _spotlightIPCollection = new SpotlightIPCollection(); + _spotlightIPCollectionAddr = address(_spotlightIPCollection); + _spotlightIPCollection.setTokenURI(originalTokenURI); + vm.stopPrank(); + } + + function test_constructor() public view { + assertEq(_spotlightIPCollection.owner(), _ownerAddr); + assertEq(_spotlightIPCollection.totalSupply(), 0); + assertEq(_spotlightIPCollection.isMintEnabled(), false); + assertEq(_spotlightIPCollection.isTransferEnabled(), false); + assertEq(_spotlightIPCollection.name(), "Spotlight IP"); + assertEq(_spotlightIPCollection.symbol(), "SPIP"); + } + + function test_notOwnerSetMintEnabled() public { + address notOwner = makeAddr("notOwner"); + vm.startPrank(notOwner); + vm.expectRevert(); + _spotlightIPCollection.setMintEnabled(true); + vm.stopPrank(); + } + + function test_setMintEnabled() public { + _enableMint(); + assert(_spotlightIPCollection.isMintEnabled()); + _disableMin(); + assert(!_spotlightIPCollection.isMintEnabled()); + } + + function test_mintBeforeEnabled() public { + address receiver = makeAddr("receiver"); + vm.startPrank(receiver); + vm.expectRevert("SpotlightIPCollection: mint is disabled"); + _spotlightIPCollection.mint(); + vm.stopPrank(); + } + + function test_notOwnerMintTo() public { + address notOwner = makeAddr("notOwner"); + address receiver = makeAddr("receiver"); + vm.startPrank(notOwner); + vm.expectRevert(); + _mintTo(receiver); + vm.stopPrank(); + } + + function test_mintToBeforeEnabled() public { + address receiver = makeAddr("receiver"); + vm.startPrank(_ownerAddr); + vm.expectRevert("SpotlightIPCollection: mint is disabled"); + _spotlightIPCollection.mint(receiver); + vm.stopPrank(); + } + + function test_mint() public { + uint originalTotalSupply = _spotlightIPCollection.totalSupply(); + address receiver = makeAddr("receiver"); + + uint256 tokenId = _mint(receiver); + + assertEq(_spotlightIPCollection.totalSupply(), originalTotalSupply + 1); + assertEq(_spotlightIPCollection.ownerOf(tokenId), receiver); + assertEq(_spotlightIPCollection.tokenURI(tokenId), originalTokenURI); + } + + function test_mintTo() public { + uint originalTotalSupply = _spotlightIPCollection.totalSupply(); + address receiver = makeAddr("receiver"); + + uint256 tokenId = _mintTo(receiver); + + assertEq(_spotlightIPCollection.totalSupply(), originalTotalSupply + 1); + assertEq(_spotlightIPCollection.ownerOf(tokenId), receiver); + assertEq(_spotlightIPCollection.tokenURI(tokenId), originalTokenURI); + } + + function test_notOwnerSetTransferEnabled() public { + address notOwner = makeAddr("notOwner"); + vm.startPrank(notOwner); + vm.expectRevert(); + _spotlightIPCollection.setTransferEnabled(true); + vm.stopPrank(); + } + + function test_setTransferEnabled() public { + _enableTransfer(); + assert(_spotlightIPCollection.isTransferEnabled()); + _disableTransfer(); + assert(!_spotlightIPCollection.isTransferEnabled()); + } + + function test_notOwnerSetTokenURI() public { + address notOwner = makeAddr("notOwner"); + vm.startPrank(notOwner); + vm.expectRevert(); + _spotlightIPCollection.setTokenURI(newTokenURI); + vm.stopPrank(); + } + + function test_setTokenURI() public { + address receiver = makeAddr("receiver"); + uint256 tokenId = _mint(receiver); + + vm.startPrank(_ownerAddr); + _spotlightIPCollection.setTokenURI(newTokenURI); + vm.stopPrank(); + + assertEq(_spotlightIPCollection.tokenURI(tokenId), newTokenURI); + } + + function test_transferFromBeforeEnabled() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + + uint256 tokenId = _mint(user1); + vm.startPrank(user1); + vm.expectRevert("SpotlightIPCollection: transfer is disabled"); + _spotlightIPCollection.transferFrom(user1, user2, tokenId); + vm.stopPrank(); + } + + function test_safeTransferFromBeforeEnabled() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + + uint256 tokenId = _mint(user1); + vm.startPrank(user1); + vm.expectRevert("SpotlightIPCollection: transfer is disabled"); + _spotlightIPCollection.safeTransferFrom(user1, user2, tokenId); + vm.stopPrank(); + } + + function test_safeTransferFromWithDataBeforeEnabled() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + + uint256 tokenId = _mint(user1); + vm.startPrank(user1); + vm.expectRevert("SpotlightIPCollection: transfer is disabled"); + _spotlightIPCollection.safeTransferFrom(user1, user2, tokenId, ""); + vm.stopPrank(); + } + + function test_transferFrom() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + + uint256 tokenId = _mint(user1); + _enableTransfer(); + vm.startPrank(user1); + _spotlightIPCollection.transferFrom(user1, user2, tokenId); + vm.stopPrank(); + + assertEq(_spotlightIPCollection.ownerOf(tokenId), user2); + } + + function test_safeTransferFrom() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + + uint256 tokenId = _mint(user1); + _enableTransfer(); + vm.startPrank(user1); + _spotlightIPCollection.safeTransferFrom(user1, user2, tokenId); + vm.stopPrank(); + + assertEq(_spotlightIPCollection.ownerOf(tokenId), user2); + } + + function test_safeTransferFromWithData() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + + uint256 tokenId = _mint(user1); + _enableTransfer(); + vm.startPrank(user1); + _spotlightIPCollection.safeTransferFrom(user1, user2, tokenId, ""); + vm.stopPrank(); + + assertEq(_spotlightIPCollection.ownerOf(tokenId), user2); + } + + // MARK: - Private functions + + function _enableTransfer() private { + vm.startPrank(_ownerAddr); + _spotlightIPCollection.setTransferEnabled(true); + vm.stopPrank(); + } + + function _disableTransfer() private { + vm.startPrank(_ownerAddr); + _spotlightIPCollection.setTransferEnabled(false); + vm.stopPrank(); + } + + function _enableMint() private { + vm.startPrank(_ownerAddr); + _spotlightIPCollection.setMintEnabled(true); + vm.stopPrank(); + } + + function _disableMin() private { + vm.startPrank(_ownerAddr); + _spotlightIPCollection.setMintEnabled(false); + vm.stopPrank(); + } + + function _mint(address _receiver) private returns (uint256) { + _enableMint(); + vm.startPrank(_receiver); + uint256 tokenId = _spotlightIPCollection.mint(); + vm.stopPrank(); + return tokenId; + } + + function _mintTo(address _receiver) private returns (uint256) { + _enableMint(); + vm.startPrank(_ownerAddr); + uint256 tokenId = _spotlightIPCollection.mint(_receiver); + vm.stopPrank(); + return tokenId; + } +} From 556e97cd64a6777fe130ad490e2c279a04ceb192 Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 24 Jan 2025 13:10:31 +0800 Subject: [PATCH 2/4] feat: add dynamic baseURL --- .../SpotlightIPCollection.sol | 22 +++++++++----- test/SpotlightIPCollection.t.sol | 29 +++++++++++++++---- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/spotlight-ip-collection/SpotlightIPCollection.sol b/src/spotlight-ip-collection/SpotlightIPCollection.sol index 99b913b..d9da62c 100644 --- a/src/spotlight-ip-collection/SpotlightIPCollection.sol +++ b/src/spotlight-ip-collection/SpotlightIPCollection.sol @@ -1,15 +1,19 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.13; -import {ERC721} from "../../lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; -import {Ownable} from "../../lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; contract SpotlightIPCollection is ERC721, Ownable { + using Strings for uint256; + bool private _isTransferEnabled = false; bool private _isMintEnabled = false; uint256 private _nextTokenId; - string private _tokenURI; + string private _defaultTokenURI; + string public __baseURI; modifier onlyTransferEnabled() { _checkTransferEnabled(); @@ -28,16 +32,20 @@ contract SpotlightIPCollection is ERC721, Ownable { } function _baseURI() internal view override returns (string memory) { - return _tokenURI; + return __baseURI; } function tokenURI(uint256 tokenId) public view override returns (string memory) { _requireOwned(tokenId); - return _tokenURI; + return bytes(_baseURI()).length > 0 ? string.concat(_baseURI(), tokenId.toString()) : _defaultTokenURI; + } + + function setBaseURI(string memory baseURI_) public onlyOwner { + __baseURI = baseURI_; } - function setTokenURI(string memory tokenURI_) public onlyOwner { - _tokenURI = tokenURI_; + function setDefaultTokenURI(string memory defaultTokenURI_) public onlyOwner { + _defaultTokenURI = defaultTokenURI_; } function isMintEnabled() public view returns (bool) { diff --git a/test/SpotlightIPCollection.t.sol b/test/SpotlightIPCollection.t.sol index f11bde2..bf2ec20 100644 --- a/test/SpotlightIPCollection.t.sol +++ b/test/SpotlightIPCollection.t.sol @@ -22,7 +22,7 @@ contract SpotlightIPCollectionTest is Test { vm.startPrank(_ownerAddr); _spotlightIPCollection = new SpotlightIPCollection(); _spotlightIPCollectionAddr = address(_spotlightIPCollection); - _spotlightIPCollection.setTokenURI(originalTokenURI); + _spotlightIPCollection.setDefaultTokenURI(originalTokenURI); vm.stopPrank(); } @@ -112,25 +112,44 @@ contract SpotlightIPCollectionTest is Test { assert(!_spotlightIPCollection.isTransferEnabled()); } - function test_notOwnerSetTokenURI() public { + function test_notOwnerSetDefaultTokenURI() public { address notOwner = makeAddr("notOwner"); vm.startPrank(notOwner); vm.expectRevert(); - _spotlightIPCollection.setTokenURI(newTokenURI); + _spotlightIPCollection.setDefaultTokenURI(newTokenURI); vm.stopPrank(); } - function test_setTokenURI() public { + function test_setDefaultTokenURI() public { address receiver = makeAddr("receiver"); uint256 tokenId = _mint(receiver); vm.startPrank(_ownerAddr); - _spotlightIPCollection.setTokenURI(newTokenURI); + _spotlightIPCollection.setDefaultTokenURI(newTokenURI); vm.stopPrank(); assertEq(_spotlightIPCollection.tokenURI(tokenId), newTokenURI); } + function test_notOwnerSetBaseURI() public { + address notOwner = makeAddr("notOwner"); + vm.startPrank(notOwner); + vm.expectRevert(); + _spotlightIPCollection.setBaseURI(newTokenURI); + vm.stopPrank(); + } + + function test_setBaseURI() public { + address receiver = makeAddr("receiver"); + _mint(receiver); + + vm.startPrank(_ownerAddr); + _spotlightIPCollection.setBaseURI(newTokenURI); + vm.stopPrank(); + + assertEq(_spotlightIPCollection.tokenURI(0), "https://example.com/new-token/0"); + } + function test_transferFromBeforeEnabled() public { address user1 = makeAddr("user1"); address user2 = makeAddr("user2"); From bb9c9451b51415dc1e10c1529263273900e1b3b2 Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 24 Jan 2025 13:56:51 +0800 Subject: [PATCH 3/4] build: add deploy ip collection script --- script/DeployIPCollection.s.sol | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 script/DeployIPCollection.s.sol diff --git a/script/DeployIPCollection.s.sol b/script/DeployIPCollection.s.sol new file mode 100644 index 0000000..4c2650d --- /dev/null +++ b/script/DeployIPCollection.s.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.13; + +import "../lib/forge-std/src/Script.sol"; +import {SpotlightIPCollection} from "../src/spotlight-ip-collection/SpotlightIPCollection.sol"; + +/* Deploy and verify with the following command: + forge script script/DeployIPCollection.s.sol:Deploy --broadcast \ + --chain-id 1516 \ + --rpc-url https://odyssey.storyrpc.io \ + --verify \ + --verifier blockscout \ + --verifier-url 'https://odyssey.storyscan.xyz/api/' +*/ + +contract Deploy is Script { + function run() public { + vm.startBroadcast(vm.envUint("PRIVATE_KEY")); + SpotlightIPCollection ipCollection = new SpotlightIPCollection(); + ipCollection.setDefaultTokenURI("ipfs://bafkreifsq7jdvvlj7cabklirodim7syc5xt4yzbrny6i4siie4yrnnqzge"); + vm.stopBroadcast(); + } +} From 26058a3699f75f8fbd61ef4a487abd1e42db40fc Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 24 Jan 2025 14:20:46 +0800 Subject: [PATCH 4/4] refactor: formatting --- test/SpotlightIPCollection.t.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/SpotlightIPCollection.t.sol b/test/SpotlightIPCollection.t.sol index bf2ec20..fc3313c 100644 --- a/test/SpotlightIPCollection.t.sol +++ b/test/SpotlightIPCollection.t.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.13; + import "../lib/forge-std/src/Test.sol"; import {SpotlightIPCollection} from "../src/spotlight-ip-collection/SpotlightIPCollection.sol"; @@ -76,7 +77,7 @@ contract SpotlightIPCollectionTest is Test { } function test_mint() public { - uint originalTotalSupply = _spotlightIPCollection.totalSupply(); + uint256 originalTotalSupply = _spotlightIPCollection.totalSupply(); address receiver = makeAddr("receiver"); uint256 tokenId = _mint(receiver); @@ -87,7 +88,7 @@ contract SpotlightIPCollectionTest is Test { } function test_mintTo() public { - uint originalTotalSupply = _spotlightIPCollection.totalSupply(); + uint256 originalTotalSupply = _spotlightIPCollection.totalSupply(); address receiver = makeAddr("receiver"); uint256 tokenId = _mintTo(receiver);