Skip to content

Commit 2b3f5ac

Browse files
committed
feat(SPGNFT): add setTokenURI function for token owners
Adds ability for NFT token owners to update their token URIs with proper access control. Only the owner of a token can update its URI. Includes: - New setTokenURI function in SPGNFT contract - Interface update in ISPGNFT - New SPGNFT__CallerNotOwner error - Test cases for successful updates and unauthorized attempts
1 parent 95be715 commit 2b3f5ac

File tree

4 files changed

+71
-0
lines changed

4 files changed

+71
-0
lines changed

contracts/SPGNFT.sol

+12
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
219219
_getSPGNFTStorage()._baseURI = baseURI;
220220
}
221221

222+
/// @notice Sets the token URI for a specific token.
223+
/// @dev Only callable by the owner of the token. This updates the metadata URI
224+
/// for the specified token and emits a MetadataUpdate event.
225+
/// @param tokenId The ID of the token to update.
226+
/// @param tokenURI_ The new metadata URI to associate with the token.
227+
function setTokenURI(uint256 tokenId, string memory tokenURI_) external {
228+
// revert if caller is not the owner of the `tokenId` token
229+
address owner = ownerOf(tokenId);
230+
if (owner != msg.sender) revert Errors.SPGNFT__CallerNotOwner(tokenId, msg.sender, owner);
231+
_setTokenURI(tokenId, tokenURI_);
232+
}
233+
222234
/// @notice Sets the contract URI for the collection.
223235
/// @dev Only callable by the admin role.
224236
/// @param contractURI The new contract URI for the collection. Follows ERC-7572 standard.

contracts/interfaces/ISPGNFT.sol

+7
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ interface ISPGNFT is IAccessControl, IERC721Metadata, IERC7572 {
109109
/// See https://eips.ethereum.org/EIPS/eip-7572
110110
function setContractURI(string memory contractURI) external;
111111

112+
/// @notice Sets the token URI for a specific token.
113+
/// @dev Only callable by the owner of the token. This updates the metadata URI
114+
/// for the specified token and emits a MetadataUpdate event.
115+
/// @param tokenId The ID of the token to update.
116+
/// @param tokenURI_ The new metadata URI to associate with the token.
117+
function setTokenURI(uint256 tokenId, string memory tokenURI_) external;
118+
112119
/// @notice Mints an NFT from the collection. Only callable by the minter role.
113120
/// @param to The address of the recipient of the minted NFT.
114121
/// @param nftMetadataURI OPTIONAL. The desired metadata for the newly minted NFT.

contracts/lib/Errors.sol

+6
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ library Errors {
117117
/// @param nftMetadataHash The hash of the NFT metadata that caused the duplication error.
118118
error SPGNFT__DuplicatedNFTMetadataHash(address spgNftContract, uint256 tokenId, bytes32 nftMetadataHash);
119119

120+
/// @notice Caller is not the owner of the `tokenId` token.
121+
/// @param tokenId The ID of the token.
122+
/// @param caller The address of the caller.
123+
/// @param owner The owner of the token.
124+
error SPGNFT__CallerNotOwner(uint256 tokenId, address caller, address owner);
125+
120126
////////////////////////////////////////////////////////////////////////////
121127
// OwnableERC20 //
122128
////////////////////////////////////////////////////////////////////////////

test/SPGNFT.t.sol

+46
Original file line numberDiff line numberDiff line change
@@ -399,4 +399,50 @@ contract SPGNFTTest is BaseTest {
399399
nftContract.setMintFeeRecipient(u.bob);
400400
vm.stopPrank();
401401
}
402+
403+
function test_SPGNFT_setTokenURI() public {
404+
// mint a token to alice
405+
vm.startPrank(u.alice);
406+
mockToken.mint(address(u.alice), 1000 * 10 ** mockToken.decimals());
407+
mockToken.approve(address(nftContract), 1000 * 10 ** mockToken.decimals());
408+
409+
uint256 tokenId = nftContract.mint(
410+
address(u.alice),
411+
ipMetadataDefault.nftMetadataURI,
412+
ipMetadataDefault.nftMetadataHash,
413+
false
414+
);
415+
416+
// alice can set the token URI as the owner
417+
string memory newTokenURI = string.concat(testBaseURI, "newTokenURI");
418+
nftContract.setTokenURI(tokenId, "newTokenURI");
419+
420+
// Verify the token URI was updated
421+
assertEq(nftContract.tokenURI(tokenId), newTokenURI);
422+
423+
vm.stopPrank();
424+
}
425+
426+
function test_SPGNFT_setTokenURI_revert_callerNotOwner() public {
427+
// mint a token to alice
428+
vm.startPrank(u.alice);
429+
mockToken.mint(address(u.alice), 1000 * 10 ** mockToken.decimals());
430+
mockToken.approve(address(nftContract), 1000 * 10 ** mockToken.decimals());
431+
432+
uint256 tokenId = nftContract.mint(
433+
address(u.alice),
434+
ipMetadataDefault.nftMetadataURI,
435+
ipMetadataDefault.nftMetadataHash,
436+
false
437+
);
438+
vm.stopPrank();
439+
440+
// bob cannot set the token URI as he's not the owner
441+
vm.startPrank(u.bob);
442+
443+
vm.expectRevert(abi.encodeWithSelector(Errors.SPGNFT__CallerNotOwner.selector, tokenId, u.bob, u.alice));
444+
nftContract.setTokenURI(tokenId, "newTokenURI");
445+
446+
vm.stopPrank();
447+
}
402448
}

0 commit comments

Comments
 (0)