Skip to content

Commit bad5d7b

Browse files
authored
refactor(story-nft): extract OrgNFT logic to BaseOrgStoryNFT (#109)
* refactor(story-nft): factor out `OrgNFT` logic in `BaseStoryNFT` * fix(story-nft): rm redundant initializer * fix(story-nft): initializer & missing comments
1 parent 8edb5f3 commit bad5d7b

9 files changed

+180
-98
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.26;
3+
4+
import { IStoryNFT } from "./IStoryNFT.sol";
5+
6+
/// @title Organization Story NFT Interface
7+
/// @notice Interface for StoryNFTs with Organization NFT integration.
8+
interface IOrgStoryNFT is IStoryNFT {
9+
/// @notice Initializes the OrgStoryNFT.
10+
/// @param orgTokenId_ The token ID of the organization NFT.
11+
/// @param orgIpId_ The ID of the organization IP.
12+
/// @param initParams The initialization parameters for StoryNFT {see {StoryNftInitParams}}.
13+
function initialize(uint256 orgTokenId_, address orgIpId_, StoryNftInitParams calldata initParams) external;
14+
}

contracts/interfaces/story-nft/IStoryNFT.sol

-6
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@ interface IStoryNFT is IERC721, IERC7572 {
3636
////////////////////////////////////////////////////////////////////////////
3737
// Functions //
3838
////////////////////////////////////////////////////////////////////////////
39-
/// @notice Initializes the StoryNFT.
40-
/// @param orgTokenId_ The token ID of the organization NFT.
41-
/// @param orgIpId_ The ID of the organization IP.
42-
/// @param initParams The initialization parameters for StoryNFT {see {StoryNftInitParams}}.
43-
function initialize(uint256 orgTokenId_, address orgIpId_, StoryNftInitParams calldata initParams) external;
44-
4539
/// @notice Sets the contractURI of the collection (follows OpenSea contract-level metadata standard).
4640
function setContractURI(string memory contractURI) external;
4741

contracts/interfaces/story-nft/IStoryNFTFactory.sol

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ interface IStoryNFTFactory {
3939
error StoryNFTFactory__SignatureAlreadyUsed(bytes signature);
4040

4141
/// @notice BaseStoryNFT is not supported by the StoryNFTFactory.
42-
/// @param tokenContract The address of the token contract that does not implement IStoryNFT.
43-
error StoryNFTFactory__UnsupportedIStoryNFT(address tokenContract);
42+
/// @param tokenContract The address of the token contract that does not implement IOrgStoryNFT.
43+
error StoryNFTFactory__UnsupportedIOrgStoryNFT(address tokenContract);
4444

4545
/// @notice Zero address provided as a param to StoryNFTFactory functions.
4646
error StoryNFTFactory__ZeroAddressParam();
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.26;
3+
4+
import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol";
5+
6+
import { IOrgStoryNFT } from "../interfaces/story-nft/IOrgStoryNFT.sol";
7+
import { BaseStoryNFT } from "./BaseStoryNFT.sol";
8+
9+
/// @title Base Story NFT with OrgNFT integration
10+
/// @notice Base Story NFT which integrates with the OrgNFT and StoryNFTFactory.
11+
abstract contract BaseOrgStoryNFT is IOrgStoryNFT, BaseStoryNFT {
12+
/// @notice Organization NFT address (see {OrgNFT}).
13+
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
14+
address public immutable ORG_NFT;
15+
16+
/// @dev Storage structure for the BaseOrgStoryNFT
17+
/// @param orgTokenId Associated Organization NFT token ID.
18+
/// @param orgIpId Associated Organization IP ID.
19+
/// @custom:storage-location erc7201:story-protocol-periphery.BaseOrgStoryNFT
20+
struct BaseOrgStoryNFTStorage {
21+
uint256 orgTokenId;
22+
address orgIpId;
23+
}
24+
25+
// keccak256(abi.encode(uint256(keccak256("story-protocol-periphery.BaseOrgStoryNFT")) - 1)) & ~bytes32(uint256(0xff));
26+
bytes32 private constant BaseOrgStoryNFTStorageLocation =
27+
0x52eea8b3c549d1bd8b986d98314c387ab153ca0f32b6949d51f32dbd11b07900;
28+
29+
constructor(
30+
address ipAssetRegistry,
31+
address licensingModule,
32+
address orgNft
33+
) BaseStoryNFT(ipAssetRegistry, licensingModule) {
34+
if (orgNft == address(0)) revert StoryNFT__ZeroAddressParam();
35+
ORG_NFT = orgNft;
36+
_disableInitializers();
37+
}
38+
39+
/// @dev External initializer function, to be overridden by the inheriting contracts.
40+
/// @param orgTokenId_ The token ID of the organization NFT.
41+
/// @param orgIpId_ The ID of the organization IP.
42+
/// @param initParams The initialization parameters for StoryNFT {see {IStoryNFT-StoryNftInitParams}}.
43+
function initialize(
44+
uint256 orgTokenId_,
45+
address orgIpId_,
46+
StoryNftInitParams calldata initParams
47+
) external virtual initializer {
48+
__BaseOrgStoryNFT_init(orgTokenId_, orgIpId_, initParams);
49+
}
50+
51+
/// @dev Initialize the BaseOrgStoryNFT
52+
/// @param orgTokenId_ The token ID of the organization NFT.
53+
/// @param orgIpId_ The ID of the organization IP.
54+
/// @param initParams The initialization parameters for StoryNFT {see {IStoryNFT-StoryNftInitParams}}.
55+
function __BaseOrgStoryNFT_init(
56+
uint256 orgTokenId_,
57+
address orgIpId_,
58+
StoryNftInitParams calldata initParams
59+
) internal onlyInitializing {
60+
if (orgIpId_ == address(0)) revert StoryNFT__ZeroAddressParam();
61+
__BaseStoryNFT_init(initParams);
62+
63+
BaseOrgStoryNFTStorage storage $ = _getBaseOrgStoryNFTStorage();
64+
$.orgTokenId = orgTokenId_;
65+
$.orgIpId = orgIpId_;
66+
}
67+
68+
/// @notice Returns the token ID of the associated Organization NFT.
69+
function orgTokenId() public view returns (uint256) {
70+
return _getBaseOrgStoryNFTStorage().orgTokenId;
71+
}
72+
73+
/// @notice Returns the ID of the associated Organization IP.
74+
function orgIpId() public view returns (address) {
75+
return _getBaseOrgStoryNFTStorage().orgIpId;
76+
}
77+
78+
/// @notice IERC165 interface support.
79+
function supportsInterface(bytes4 interfaceId) public view virtual override(BaseStoryNFT, IERC165) returns (bool) {
80+
return interfaceId == type(IOrgStoryNFT).interfaceId || super.supportsInterface(interfaceId);
81+
}
82+
83+
/// @dev Returns the storage struct of BaseOrgStoryNFT.
84+
function _getBaseOrgStoryNFTStorage() private pure returns (BaseOrgStoryNFTStorage storage $) {
85+
assembly {
86+
$.slot := BaseOrgStoryNFTStorageLocation
87+
}
88+
}
89+
}

contracts/story-nft/BaseStoryNFT.sol

+45-65
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
pragma solidity 0.8.26;
33

44
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
5-
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
6-
import { ERC721URIStorage } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
7-
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
8-
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
5+
/* solhint-disable-next-line max-line-length */
6+
import { ERC721URIStorageUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
7+
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
98
import { IIPAssetRegistry } from "@story-protocol/protocol-core/contracts/interfaces/registries/IIPAssetRegistry.sol";
109
/*solhint-disable-next-line max-line-length*/
1110
import { ILicensingModule } from "@story-protocol/protocol-core/contracts/interfaces/modules/licensing/ILicensingModule.sol";
@@ -17,72 +16,56 @@ import { IStoryNFT } from "../interfaces/story-nft/IStoryNFT.sol";
1716
/// To create a new custom StoryNFT, inherit from this contract and override the required functions.
1817
/// Note: the new StoryNFT must be whitelisted in `StoryNFTFactory` by the Story governance in order
1918
/// to use the Story NFT Factory features.
20-
abstract contract BaseStoryNFT is IStoryNFT, ERC721URIStorage, Ownable, Initializable {
19+
abstract contract BaseStoryNFT is IStoryNFT, ERC721URIStorageUpgradeable, OwnableUpgradeable {
2120
/// @notice Story Proof-of-Creativity IP Asset Registry address.
21+
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
2222
IIPAssetRegistry public immutable IP_ASSET_REGISTRY;
2323

2424
/// @notice Story Proof-of-Creativity Licensing Module address.
25+
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
2526
ILicensingModule public immutable LICENSING_MODULE;
2627

27-
/// @notice Organization NFT address (see {OrgNFT}).
28-
address public immutable ORG_NFT;
29-
30-
/// @notice Associated Organization NFT token ID.
31-
uint256 public orgTokenId;
32-
33-
/// @notice Associated Organization IP ID.
34-
address public orgIpId;
35-
36-
/// @dev Name of the collection.
37-
string private _name;
38-
39-
/// @dev Symbol of the collection.
40-
string private _symbol;
41-
42-
/// @dev Contract URI of the collection (follows OpenSea contract-level metadata standard).
43-
string private _contractURI;
44-
45-
/// @dev Base URI of the collection (see {ERC721URIStorage-tokenURI} for how it is used).
46-
string private _baseURI_;
28+
/// @dev Storage structure for the BaseStoryNFT
29+
/// @param contractURI The contract URI of the collection.
30+
/// @param baseURI The base URI of the collection.
31+
/// @param totalSupply The total supply of the collection.
32+
/// @custom:storage-location erc7201:story-protocol-periphery.BaseStoryNFT
33+
struct BaseStoryNFTStorage {
34+
string contractURI;
35+
string baseURI;
36+
uint256 totalSupply;
37+
}
4738

48-
/// @dev Current total supply of the collection.
49-
uint256 private _totalSupply;
39+
// keccak256(abi.encode(uint256(keccak256("story-protocol-periphery.BaseStoryNFT")) - 1)) & ~bytes32(uint256(0xff));
40+
bytes32 private constant BaseStoryNFTStorageLocation =
41+
0x81ed94d7560ff7bef5060a232718049e514c358c346e3254b876807a753c0e00;
5042

51-
constructor(address ipAssetRegistry, address licensingModule, address orgNft) ERC721("", "") Ownable(msg.sender) {
52-
if (ipAssetRegistry == address(0) || licensingModule == address(0) || orgNft == address(0))
53-
revert StoryNFT__ZeroAddressParam();
43+
constructor(address ipAssetRegistry, address licensingModule) {
44+
if (ipAssetRegistry == address(0) || licensingModule == address(0)) revert StoryNFT__ZeroAddressParam();
5445
IP_ASSET_REGISTRY = IIPAssetRegistry(ipAssetRegistry);
5546
LICENSING_MODULE = ILicensingModule(licensingModule);
56-
ORG_NFT = orgNft;
47+
48+
_disableInitializers();
5749
}
5850

5951
/// @notice Initializes the StoryNFT
60-
/// @param orgTokenId_ The token ID of the organization NFT.
61-
/// @param orgIpId_ The ID of the organization IP.
6252
/// @param initParams The initialization parameters for StoryNFT {see {IStoryNFT-StoryNftInitParams}}.
63-
function initialize(
64-
uint256 orgTokenId_,
65-
address orgIpId_,
66-
StoryNftInitParams calldata initParams
67-
) public virtual initializer {
68-
if (initParams.owner == address(0) || orgIpId_ == address(0)) revert StoryNFT__ZeroAddressParam();
69-
70-
orgTokenId = orgTokenId_;
71-
orgIpId = orgIpId_;
72-
73-
_name = initParams.name;
74-
_symbol = initParams.symbol;
75-
_contractURI = initParams.contractURI;
76-
_baseURI_ = initParams.baseURI;
77-
78-
_transferOwnership(initParams.owner);
53+
function __BaseStoryNFT_init(StoryNftInitParams calldata initParams) internal onlyInitializing {
54+
__Ownable_init(initParams.owner);
55+
__ERC721URIStorage_init();
56+
__ERC721_init(initParams.name, initParams.symbol);
57+
58+
BaseStoryNFTStorage storage $ = _getBaseStoryNFTStorage();
59+
$.contractURI = initParams.contractURI;
60+
$.baseURI = initParams.baseURI;
61+
7962
_customize(initParams.customInitData);
8063
}
8164

8265
/// @notice Sets the contractURI of the collection (follows OpenSea contract-level metadata standard).
8366
/// @param contractURI_ The new contractURI of the collection.
8467
function setContractURI(string memory contractURI_) external onlyOwner {
85-
_contractURI = contractURI_;
68+
_getBaseStoryNFTStorage().contractURI = contractURI_;
8669

8770
emit ContractURIUpdated();
8871
}
@@ -104,7 +87,7 @@ abstract contract BaseStoryNFT is IStoryNFT, ERC721URIStorage, Ownable, Initiali
10487
address recipient,
10588
string memory tokenURI_
10689
) internal virtual returns (uint256 tokenId, address ipId) {
107-
tokenId = _totalSupply++;
90+
tokenId = _getBaseStoryNFTStorage().totalSupply++;
10891
_safeMint(recipient, tokenId);
10992
_setTokenURI(tokenId, tokenURI_);
11093
ipId = IP_ASSET_REGISTRY.register(block.chainid, address(this), tokenId);
@@ -138,28 +121,18 @@ abstract contract BaseStoryNFT is IStoryNFT, ERC721URIStorage, Ownable, Initiali
138121
/// @notice IERC165 interface support.
139122
function supportsInterface(
140123
bytes4 interfaceId
141-
) public view virtual override(ERC721URIStorage, IERC165) returns (bool) {
124+
) public view virtual override(ERC721URIStorageUpgradeable, IERC165) returns (bool) {
142125
return interfaceId == type(IStoryNFT).interfaceId || super.supportsInterface(interfaceId);
143126
}
144127

145-
/// @notice Returns the name of the collection.
146-
function name() public view override returns (string memory) {
147-
return _name;
148-
}
149-
150-
/// @notice Returns the symbol of the collection.
151-
function symbol() public view override returns (string memory) {
152-
return _symbol;
153-
}
154-
155128
/// @notice Returns the current total supply of the collection.
156129
function totalSupply() public view returns (uint256) {
157-
return _totalSupply;
130+
return _getBaseStoryNFTStorage().totalSupply;
158131
}
159132

160133
/// @notice Returns the contract URI of the collection (follows OpenSea contract-level metadata standard).
161134
function contractURI() external view virtual returns (string memory) {
162-
return _contractURI;
135+
return _getBaseStoryNFTStorage().contractURI;
163136
}
164137

165138
/// @notice Initializes the StoryNFT with custom data, required to be overridden by the inheriting contracts.
@@ -169,6 +142,13 @@ abstract contract BaseStoryNFT is IStoryNFT, ERC721URIStorage, Ownable, Initiali
169142

170143
/// @notice Returns the base URI of the collection (see {ERC721URIStorage-tokenURI} for how it is used).
171144
function _baseURI() internal view virtual override returns (string memory) {
172-
return _baseURI_;
145+
return _getBaseStoryNFTStorage().baseURI;
146+
}
147+
148+
/// @dev Returns the storage struct of BaseStoryNFT.
149+
function _getBaseStoryNFTStorage() private pure returns (BaseStoryNFTStorage storage $) {
150+
assembly {
151+
$.slot := BaseStoryNFTStorageLocation
152+
}
173153
}
174154
}

contracts/story-nft/StoryBadgeNFT.sol

+15-12
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.26;
33

4-
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
4+
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
55
import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
6-
import { ERC721URIStorage } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
6+
/* solhint-disable-next-line max-line-length */
7+
import { ERC721URIStorageUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
78
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
89
import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
910
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
1011
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
1112

12-
import { BaseStoryNFT } from "./BaseStoryNFT.sol";
13+
import { BaseOrgStoryNFT } from "./BaseOrgStoryNFT.sol";
1314
import { IStoryBadgeNFT } from "../interfaces/story-nft/IStoryBadgeNFT.sol";
1415

1516
/// @title Story Badge NFT
1617
/// @notice A Story Badge is a soulbound NFT that has an unified token URI for all tokens.
17-
contract StoryBadgeNFT is IStoryBadgeNFT, BaseStoryNFT, ERC721Holder {
18+
contract StoryBadgeNFT is IStoryBadgeNFT, BaseOrgStoryNFT, ERC721Holder {
1819
using MessageHashUtils for bytes32;
1920

2021
/// @notice Story Proof-of-Creativity PILicense Template address.
@@ -38,7 +39,7 @@ contract StoryBadgeNFT is IStoryBadgeNFT, BaseStoryNFT, ERC721Holder {
3839
address orgNft,
3940
address pilTemplate,
4041
uint256 defaultLicenseTermsId
41-
) BaseStoryNFT(ipAssetRegistry, licensingModule, orgNft) {
42+
) BaseOrgStoryNFT(ipAssetRegistry, licensingModule, orgNft) {
4243
if (
4344
ipAssetRegistry == address(0) ||
4445
licensingModule == address(0) ||
@@ -82,7 +83,7 @@ contract StoryBadgeNFT is IStoryBadgeNFT, BaseStoryNFT, ERC721Holder {
8283

8384
address[] memory parentIpIds = new address[](1);
8485
uint256[] memory licenseTermsIds = new uint256[](1);
85-
parentIpIds[0] = orgIpId;
86+
parentIpIds[0] = orgIpId();
8687
licenseTermsIds[0] = DEFAULT_LICENSE_TERMS_ID;
8788

8889
// Make the badge a derivative of the organization IP
@@ -111,14 +112,16 @@ contract StoryBadgeNFT is IStoryBadgeNFT, BaseStoryNFT, ERC721Holder {
111112
/// @notice Returns the token URI for the given token ID.
112113
/// @param tokenId The token ID.
113114
/// @return The unified token URI for all badges.
114-
function tokenURI(uint256 tokenId) public view override(ERC721URIStorage, IERC721Metadata) returns (string memory) {
115+
function tokenURI(
116+
uint256 tokenId
117+
) public view override(ERC721URIStorageUpgradeable, IERC721Metadata) returns (string memory) {
115118
return _tokenURI;
116119
}
117120

118121
/// @notice Initializes the StoryBadgeNFT with custom data (see {IStoryBadgeNFT-CustomInitParams}).
119122
/// @dev This function is called by BaseStoryNFT's `initialize` function.
120123
/// @param customInitData The custom data to initialize the StoryBadgeNFT.
121-
function _customize(bytes memory customInitData) internal override {
124+
function _customize(bytes memory customInitData) internal override onlyInitializing {
122125
CustomInitParams memory customParams = abi.decode(customInitData, (CustomInitParams));
123126
if (customParams.signer == address(0)) revert StoryBadgeNFT__ZeroAddressParam();
124127

@@ -136,15 +139,15 @@ contract StoryBadgeNFT is IStoryBadgeNFT, BaseStoryNFT, ERC721Holder {
136139
// Locked Functions //
137140
////////////////////////////////////////////////////////////////////////////
138141

139-
function approve(address to, uint256 tokenId) public pure override(ERC721, IERC721) {
142+
function approve(address to, uint256 tokenId) public pure override(ERC721Upgradeable, IERC721) {
140143
revert StoryBadgeNFT__TransferLocked();
141144
}
142145

143-
function setApprovalForAll(address operator, bool approved) public pure override(ERC721, IERC721) {
146+
function setApprovalForAll(address operator, bool approved) public pure override(ERC721Upgradeable, IERC721) {
144147
revert StoryBadgeNFT__TransferLocked();
145148
}
146149

147-
function transferFrom(address from, address to, uint256 tokenId) public pure override(ERC721, IERC721) {
150+
function transferFrom(address from, address to, uint256 tokenId) public pure override(ERC721Upgradeable, IERC721) {
148151
revert StoryBadgeNFT__TransferLocked();
149152
}
150153

@@ -153,7 +156,7 @@ contract StoryBadgeNFT is IStoryBadgeNFT, BaseStoryNFT, ERC721Holder {
153156
address to,
154157
uint256 tokenId,
155158
bytes memory data
156-
) public pure override(ERC721, IERC721) {
159+
) public pure override(ERC721Upgradeable, IERC721) {
157160
revert StoryBadgeNFT__TransferLocked();
158161
}
159162
}

0 commit comments

Comments
 (0)