diff --git a/contracts/ERC721Baseline.sol b/contracts/ERC721Baseline.sol index 65d584a..7921d43 100644 --- a/contracts/ERC721Baseline.sol +++ b/contracts/ERC721Baseline.sol @@ -14,7 +14,6 @@ import {Utils} from "./Utils.sol"; * @custom:version v0.1.0-alpha.3 * @notice A baseline ERC721 contract implementation that exposes internal methods to a proxy instance. */ - contract ERC721Baseline is ERC721, IERC2981, IERC721Baseline { /** @@ -39,6 +38,9 @@ contract ERC721Baseline is ERC721, IERC2981, IERC721Baseline { * Supported Interfaces ************************************************/ + /** + * @inheritdoc IERC165 + */ function supportsInterface(bytes4 interfaceId) public view override(IERC165, ERC721) returns (bool) { return ( interfaceId == /* NFT Royalty Standard */ type(IERC2981).interfaceId || @@ -105,8 +107,6 @@ contract ERC721Baseline is ERC721, IERC2981, IERC721Baseline { return _symbol; } - event MetadataUpdate(uint256 tokenId); - /** * Metadata > Token URI */ @@ -199,7 +199,7 @@ contract ERC721Baseline is ERC721, IERC2981, IERC721Baseline { function royaltyInfo( uint256, uint256 - ) external pure returns (address receiver, uint256 royaltyAmount) { + ) external pure returns (address, uint256) { return (address(0), 0); } @@ -488,14 +488,14 @@ contract ERC721Baseline is ERC721, IERC2981, IERC721Baseline { /** * @inheritdoc IERC721Baseline */ - function recover(bytes32 hash, bytes memory signature) external view returns (address result) { + function recover(bytes32 hash, bytes memory signature) external view returns (address) { return Utils.recover(Utils.toEthSignedMessageHash(hash), signature); } /** * @inheritdoc IERC721Baseline */ - function recoverCalldata(bytes32 hash, bytes calldata signature) external view returns (address result) { + function recoverCalldata(bytes32 hash, bytes calldata signature) external view returns (address) { return Utils.recoverCalldata(Utils.toEthSignedMessageHash(hash), signature); } diff --git a/contracts/IERC721Baseline.sol b/contracts/IERC721Baseline.sol index 853dab5..750e1de 100644 --- a/contracts/IERC721Baseline.sol +++ b/contracts/IERC721Baseline.sol @@ -9,7 +9,6 @@ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; * @custom:version v0.1.0-alpha.3 * @notice A baseline ERC721 contract implementation that exposes internal methods to a proxy instance. */ - interface IERC721Baseline is IERC721 { /** @@ -44,6 +43,29 @@ interface IERC721Baseline is IERC721 { * Metadata ************************************************/ + /** + * Metadata > ERC-4906 events + */ + + /** + * @dev This event emits when the metadata of a token is changed. + * So that the third-party platforms such as NFT market could + * timely update the images and related attributes of the NFT. + * + * @param _tokenId the token ID being updated + */ + event MetadataUpdate(uint256 _tokenId); + + /** + * @dev This event emits when the metadata of a range of tokens is changed. + * So that the third-party platforms such as NFT market could + * timely update the images and related attributes of the NFTs. + * + * @param _fromTokenId the starting token ID + * @param _toTokenId the ending token ID + */ + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + /** * @notice The total minted supply. * @dev The supply is decreased when a token is burned. @@ -72,6 +94,8 @@ interface IERC721Baseline is IERC721 { /** * @notice Sets the token URI for a token ID. * @dev Emits EIP-4906's `MetadataUpdate` event with the `tokenId`. + * This method is internal and only the proxy contract can call it. + * * * @param tokenId token ID * @param tokenURI URI pointing to the metadata @@ -80,6 +104,7 @@ interface IERC721Baseline is IERC721 { /** * @notice Returns the shared URI for the tokens. + * @dev This method is internal and only the proxy contract can call it. */ function __sharedURI() external view returns (string memory); @@ -89,6 +114,8 @@ interface IERC721Baseline is IERC721 { * because ERC721Baseline allows to mint any token ID, starting at any index. * The proxy should emit `BatchMetadataUpdate`. * + * This method is internal and only the proxy contract can call it. + * * @param sharedURI shared URI for the tokens */ function __setSharedURI(string calldata sharedURI) external; @@ -105,6 +132,8 @@ interface IERC721Baseline is IERC721 { * because ERC721Baseline allows to mint any token ID, starting at any index. * The proxy should emit `BatchMetadataUpdate`. * + * This method is internal and only the proxy contract can call it. + * * @param baseURI shared base URI for the tokens */ function __setBaseURI(string calldata baseURI) external; @@ -173,15 +202,13 @@ interface IERC721Baseline is IERC721 { /** * @dev See {ERC721-_checkOnERC721Received}. - * This method is internal and only the proxy contract can call it. * - * @dev NOTE that this method accepts an additional first parameter that is the original transaction's `msg.sender`. + * NOTE: this method accepts an additional first parameter that is the original transaction's `msg.sender`. */ function __checkOnERC721Received(address sender, address from, address to, uint256 tokenId, bytes memory data) external; /** * @dev See {ERC721-_isAuthorized}. - * This method is internal and only the proxy contract can call it. */ function __isAuthorized(address owner, address spender, uint256 tokenId) external view returns (bool); @@ -292,6 +319,11 @@ interface IERC721Baseline is IERC721 { * Utils ************************************************/ + /** + * @dev Indicates an invalid signature. + */ + error InvalidSignature(); + /** * @notice Recovers the signer's address from a message digest `hash`, and the `signature`. * diff --git a/contracts/Utils.sol b/contracts/Utils.sol index e8be727..d2ad356 100644 --- a/contracts/Utils.sol +++ b/contracts/Utils.sol @@ -7,14 +7,11 @@ pragma solidity 0.8.21; * @custom:version v0.1.0-alpha.3 * @notice Utilities used in ERC721Baseline. */ - library Utils { /************************************************ * ECDSA Utils ************************************************/ - error InvalidSignature(); - /** * recover * diff --git a/contracts/mocks/ERC721ConstructorAttackerMock.sol b/contracts/mocks/ERC721ConstructorAttackerMock.sol index 8d9a2dc..968af57 100644 --- a/contracts/mocks/ERC721ConstructorAttackerMock.sol +++ b/contracts/mocks/ERC721ConstructorAttackerMock.sol @@ -6,7 +6,6 @@ import {IERC721Baseline} from "../IERC721Baseline.sol"; /// @title {title} /// @author {name} - contract ERC721ConstructorAttackerMock { constructor(address ERC721BaselineImplementation) { IERC721Baseline(ERC721BaselineImplementation).initialize("hack", "HACK"); diff --git a/contracts/mocks/ERC721ProxyMock.sol b/contracts/mocks/ERC721ProxyMock.sol index 8421581..91a3a5a 100644 --- a/contracts/mocks/ERC721ProxyMock.sol +++ b/contracts/mocks/ERC721ProxyMock.sol @@ -9,7 +9,6 @@ import {IERC721Baseline} from "../IERC721Baseline.sol"; /// @title {title} /// @author {name} - contract ERC721ProxyMock is Proxy { IERC721Baseline baseline = IERC721Baseline(address(this)); @@ -78,8 +77,6 @@ contract ERC721ProxyMock is Proxy { require(_beforeTokenTransferHookEnabled, 'not enabled'); - // @todo Try to alter state and make sure it is not reflected in the implementation. - if (sender == to) { revert('Call to self'); } diff --git a/package-lock.json b/package-lock.json index 6d465d8..f031388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0-alpha.3", "license": "AGPL-3.0-or-later", "dependencies": { - "@openzeppelin/contracts": "5.0.0" + "@openzeppelin/contracts": "5.0.1" }, "devDependencies": { "@openzeppelin/test-helpers": "^0.5.16", @@ -1797,9 +1797,9 @@ } }, "node_modules/@openzeppelin/contracts": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.0.tgz", - "integrity": "sha512-bv2sdS6LKqVVMLI5+zqnNrNU/CA+6z6CmwFXm/MzmOPBRSO5reEJN7z0Gbzvs0/bv/MZZXNklubpwy3v2+azsw==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.1.tgz", + "integrity": "sha512-yQJaT5HDp9hYOOp4jTYxMsR02gdFZFXhewX5HW9Jo4fsqSVqqyIO/xTHdWDaKX5a3pv1txmf076Lziz+sO7L1w==" }, "node_modules/@openzeppelin/test-helpers": { "version": "0.5.16", @@ -14725,9 +14725,9 @@ } }, "@openzeppelin/contracts": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.0.tgz", - "integrity": "sha512-bv2sdS6LKqVVMLI5+zqnNrNU/CA+6z6CmwFXm/MzmOPBRSO5reEJN7z0Gbzvs0/bv/MZZXNklubpwy3v2+azsw==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.1.tgz", + "integrity": "sha512-yQJaT5HDp9hYOOp4jTYxMsR02gdFZFXhewX5HW9Jo4fsqSVqqyIO/xTHdWDaKX5a3pv1txmf076Lziz+sO7L1w==" }, "@openzeppelin/test-helpers": { "version": "0.5.16", diff --git a/package.json b/package.json index a84d070..9310caa 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "truffle": "^5.11.2" }, "dependencies": { - "@openzeppelin/contracts": "5.0.0" + "@openzeppelin/contracts": "5.0.1" }, "prettier": { "plugins": [ diff --git a/test/ERC721Baseline.js b/test/ERC721Baseline.js index 4a6439d..fe7e46b 100644 --- a/test/ERC721Baseline.js +++ b/test/ERC721Baseline.js @@ -374,6 +374,90 @@ contract( }); }); + describe("Metadata", () => { + it("sets name and symbols", async () => { + assert.equal("Test", await proxyDelegate.name()); + assert.equal("TEST", await proxyDelegate.symbol()); + }); + + it("updates totalSupply correctly", async () => { + await proxy.onlyProxy_mint(user, 3); + assert.equal(1, await proxyDelegate.totalSupply()); + await proxy.onlyProxy_burn(3, { from: user }); + assert.equal(0, await proxyDelegate.totalSupply()); + }); + + describe("token URI", () => { + const tokenId = 3; + + beforeEach(async () => { + await proxy.onlyProxy_mint(user, tokenId); + }); + + it("throws if the token does not exist", async () => { + await expectRevert( + proxyDelegate.tokenURI(100), + "ERC721NonexistentToken(uint256)", + ); + }); + + it("returns empty string when nothing is set", async () => { + assert.equal("", await proxyDelegate.tokenURI(tokenId)); + }); + + it("returns token-specific URI when set", async () => { + const anotherTokenId = tokenId + 1; + const uri = "ipfs://test"; + + await proxy.onlyProxy_mint(user, anotherTokenId, uri); + + assert.equal(uri, await proxyDelegate.tokenURI(anotherTokenId)); + assert.equal(uri, await proxyDelegate.__tokenURI(anotherTokenId)); + + assert.equal("", await proxyDelegate.tokenURI(tokenId)); + }); + + it("can update token-specific URI and emits MetadataUpdate", async () => { + const uri = "ipfs://updated"; + const receipt = await proxy.onlyProxy_setTokenURI(tokenId, uri); + + await expectEvent.inTransaction( + receipt.tx, + proxyDelegate, + "MetadataUpdate", + { + _tokenId: String(tokenId), + }, + ); + + assert.equal(uri, await proxyDelegate.tokenURI(tokenId)); + assert.equal(uri, await proxyDelegate.__tokenURI(tokenId)); + }); + + it("can define shared URI", async () => { + const uri = "ipfs://shared"; + + const anotherTokenId = tokenId + 1; + await proxy.onlyProxy_mint(user, anotherTokenId); + + await proxy.onlyProxy_setSharedURI(uri); + + assert.equal(uri, await proxyDelegate.tokenURI(anotherTokenId)); + assert.equal( + await proxyDelegate.tokenURI(tokenId), + await proxyDelegate.tokenURI(anotherTokenId), + ); + }); + + it("can set base URI", async () => { + const uri = "ipfs://base/"; + await proxy.onlyProxy_setBaseURI(uri); + + assert.equal(uri + tokenId, await proxyDelegate.tokenURI(tokenId)); + }); + }); + }); + describe("Utils", () => { describe("recover", () => { const signer = web3.eth.accounts.create();