Skip to content

Commit 9feb33e

Browse files
committed
feat(story-nft): add cachable functionality to StoryBadgeNFT
1 parent 111f171 commit 9feb33e

File tree

3 files changed

+90
-18
lines changed

3 files changed

+90
-18
lines changed

contracts/story-nft/CachableNFT.sol

+20
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@ abstract contract CachableNFT is OwnableUpgradeable {
2424
bytes32 private constant CacheableNFTStorageLocation =
2525
0xb2c28ba4bb2a3f74a63ac2785b5af0c41313804d8b65acc69c0b2736a57e5f00;
2626

27+
/// @notice Sets the cache mode.
28+
/// @param useCache The new cache mode, true for cache mode, false for passthrough mode.
2729
function setCacheMode(bool useCache) external onlyOwner {
2830
CacheableNFTStorage storage $ = _getCacheableNFTStorage();
2931
$.cacheMode = useCache;
3032
}
3133

34+
/// @notice Mints NFTs to the cache.
35+
/// @param amount The number of NFTs to mint.
3236
function mintToCache(uint256 amount) external onlyOwner {
3337
// mint NFT to cache
3438
for (uint256 i = 0; i < amount; i++) {
@@ -38,21 +42,37 @@ abstract contract CachableNFT is OwnableUpgradeable {
3842
}
3943
}
4044

45+
/// @notice Returns the number of NFTs in the cache.
46+
/// @return The number of NFTs in the cache.
4147
function cacheSize() external view returns (uint256) {
4248
CacheableNFTStorage storage $ = _getCacheableNFTStorage();
4349
return $.cache.length();
4450
}
4551

52+
/// @notice Transfers the first NFT from the cache to the recipient.
53+
/// @param recipient The recipient of the NFT.
54+
/// @return tokenId The token ID of the transferred NFT.
55+
/// @return ipId The IP ID of the transferred NFT.
4656
function _transferFromCache(address recipient) internal returns (uint256 tokenId, address ipId) {
4757
CacheableNFTStorage storage $ = _getCacheableNFTStorage();
4858
if (!$.cacheMode || $.cache.length() == 0) {
4959
return (0, address(0));
5060
}
5161
(tokenId, ipId) = $.cache.at(0);
62+
$.cache.remove(0);
63+
5264
_transferFrom(address(this), recipient, tokenId);
5365
}
5466

67+
/// @notice Mints an NFT to the contract itself.
68+
/// @return tokenId The token ID of the minted NFT.
69+
/// @return ipId The IP ID of the minted NFT.
5570
function _mintToSelf() internal virtual returns (uint256 tokenId, address ipId);
71+
72+
/// @notice Transfers an NFT from one address to another.
73+
/// @param from The address to transfer the NFT from.
74+
/// @param to The address to transfer the NFT to.
75+
/// @param tokenId The token ID of the NFT to transfer.
5676
function _transferFrom(address from, address to, uint256 tokenId) internal virtual;
5777

5878
/// @dev Returns the storage struct of CacheableNFT.

contracts/story-nft/StoryBadgeNFT.sol

+42-18
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/Mes
1111
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
1212

1313
import { BaseOrgStoryNFT } from "./BaseOrgStoryNFT.sol";
14+
import { CachableNFT } from "./CachableNFT.sol";
1415
import { IStoryBadgeNFT } from "../interfaces/story-nft/IStoryBadgeNFT.sol";
1516

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

2122
/// @notice Story Proof-of-Creativity PILicense Template address.
@@ -97,25 +98,16 @@ contract StoryBadgeNFT is IStoryBadgeNFT, BaseOrgStoryNFT, ERC721Holder {
9798
if (!SignatureChecker.isValidSignatureNow($.signer, digest, signature))
9899
revert StoryBadgeNFT__InvalidSignature();
99100

100-
// Mint the badge and register it as an IP
101-
(tokenId, ipId) = _mintAndRegisterIp(
102-
address(this),
103-
$.tokenURI,
104-
$.ipMetadataURI,
105-
$.ipMetadataHash,
106-
$.nftMetadataHash
107-
);
101+
// Try to transfer from cache first
102+
(tokenId, ipId) = _transferFromCache(recipient);
108103

109-
address[] memory parentIpIds = new address[](1);
110-
uint256[] memory licenseTermsIds = new uint256[](1);
111-
parentIpIds[0] = orgIpId();
112-
licenseTermsIds[0] = DEFAULT_LICENSE_TERMS_ID;
113-
114-
// Make the badge a derivative of the organization IP
115-
_makeDerivative(ipId, parentIpIds, PIL_TEMPLATE, licenseTermsIds, "", 0);
104+
if (ipId == address(0)) {
105+
// cache miss or cache mode is disabled, mint directly
106+
(tokenId, ipId) = _mintToSelf();
116107

117-
// Transfer the badge to the recipient
118-
_safeTransfer(address(this), recipient, tokenId);
108+
// Transfer the badge to the recipient
109+
_transfer(address(this), recipient, tokenId);
110+
}
119111

120112
emit StoryBadgeNFTMinted(recipient, tokenId, ipId);
121113
}
@@ -164,6 +156,38 @@ contract StoryBadgeNFT is IStoryBadgeNFT, BaseOrgStoryNFT, ERC721Holder {
164156
return "";
165157
}
166158

159+
/// @notice Mints an NFT to the contract itself.
160+
/// @return tokenId The token ID of the minted NFT.
161+
/// @return ipId The IP ID of the minted NFT.
162+
function _mintToSelf() internal override returns (uint256 tokenId, address ipId) {
163+
StoryBadgeNFTStorage storage $ = _getStoryBadgeNFTStorage();
164+
165+
// Mint the badge and register it as an IP
166+
(tokenId, ipId) = _mintAndRegisterIp(
167+
address(this),
168+
$.tokenURI,
169+
$.ipMetadataURI,
170+
$.ipMetadataHash,
171+
$.nftMetadataHash
172+
);
173+
174+
address[] memory parentIpIds = new address[](1);
175+
uint256[] memory licenseTermsIds = new uint256[](1);
176+
parentIpIds[0] = orgIpId();
177+
licenseTermsIds[0] = DEFAULT_LICENSE_TERMS_ID;
178+
179+
// Make the badge a derivative of the organization IP
180+
_makeDerivative(ipId, parentIpIds, PIL_TEMPLATE, licenseTermsIds, "", 0);
181+
}
182+
183+
/// @notice Transfers an NFT from one address to another.
184+
/// @param from The address to transfer the NFT from.
185+
/// @param to The address to transfer the NFT to.
186+
/// @param tokenId The token ID of the NFT to transfer.
187+
function _transferFrom(address from, address to, uint256 tokenId) internal override {
188+
_transfer(from, to, tokenId);
189+
}
190+
167191
/// @dev Returns the storage struct of StoryBadgeNFT.
168192
function _getStoryBadgeNFTStorage() private pure returns (StoryBadgeNFTStorage storage $) {
169193
assembly {

test/story-nft/StoryBadgeNFT.t.sol

+28
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,34 @@ contract StoryBadgeNFTTest is BaseTest {
212212
assertEq(rootOrgStoryNft.tokenURI(0), "New Token URI");
213213
}
214214

215+
function test_StoryBadgeNFT_cachedMint() public {
216+
vm.startPrank(rootOrgStoryNftOwner);
217+
rootOrgStoryNft.mintToCache(1);
218+
assertEq(rootOrgStoryNft.cacheSize(), 1); // 1 cached
219+
rootOrgStoryNft.mintToCache(100);
220+
assertEq(rootOrgStoryNft.cacheSize(), 101); // 100 cached + 1 minted
221+
rootOrgStoryNft.setCacheMode(true); // enable cache mode
222+
vm.stopPrank();
223+
224+
bytes memory signature = _signAddress(rootOrgStoryNftSignerSk, u.carl);
225+
vm.startPrank(u.carl);
226+
(uint256 tokenId, ) = rootOrgStoryNft.mint(u.carl, signature);
227+
assertEq(rootOrgStoryNft.ownerOf(tokenId), u.carl); // minted from cache
228+
vm.stopPrank();
229+
assertEq(rootOrgStoryNft.cacheSize(), 100); // cache size is reduced by 1
230+
231+
vm.startPrank(rootOrgStoryNftOwner);
232+
rootOrgStoryNft.setCacheMode(false); // disable cache mode
233+
vm.stopPrank();
234+
235+
signature = _signAddress(rootOrgStoryNftSignerSk, u.bob);
236+
vm.startPrank(u.bob);
237+
(tokenId, ) = rootOrgStoryNft.mint(u.bob, signature);
238+
assertEq(rootOrgStoryNft.ownerOf(tokenId), u.bob); // minted directly
239+
vm.stopPrank();
240+
assertEq(rootOrgStoryNft.cacheSize(), 100); // cache size is unchanged
241+
}
242+
215243
function test_StoryBadgeNFT_revert_setSigner_CallerIsNotOwner() public {
216244
vm.startPrank(u.carl);
217245
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, u.carl));

0 commit comments

Comments
 (0)