diff --git a/.gitmodules b/.gitmodules index 690924b..9296efd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 932fddf..01ef448 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 932fddf69a699a9a80fd2396fd1a2ab91cdda123 +Subproject commit 01ef448981be9d20ca85f2faf6ebdf591ce409f3 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..fbdb824 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit fbdb824a735891908d5588b28e0da5852d7ed7ba diff --git a/remappings.txt b/remappings.txt index f810763..7cd78ba 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,5 +1,5 @@ -@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ ds-test/=lib/forge-std/lib/ds-test/src/ erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ forge-std/=lib/forge-std/src/ -openzeppelin-contracts/=lib/openzeppelin-contracts/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ \ No newline at end of file diff --git a/script/Deploy001_NftReward.s.sol b/script/Deploy001_NftReward.s.sol index 7d61a89..b35906b 100644 --- a/script/Deploy001_NftReward.s.sol +++ b/script/Deploy001_NftReward.s.sol @@ -3,11 +3,16 @@ pragma solidity ^0.8.13; import {Script} from "forge-std/Script.sol"; import {NftReward} from "../src/NftReward.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract Deploy001_NftReward is Script { - NftReward nftReward; + function run() public returns(address) { + // deploy NftReward contract + address proxy = deployNftReward(); + return proxy; + } - function run() public { + function deployNftReward() public returns(address) { // read env variables address minterAddress = vm.envAddress("MINTER_ADDRESS"); string memory nftTokenName = vm.envString("NFT_TOKEN_NAME"); @@ -19,15 +24,16 @@ contract Deploy001_NftReward is Script { // start sending owner transactions vm.startBroadcast(ownerPrivateKey); - // deploy NftReward - nftReward = new NftReward{salt: salt}( - nftTokenName, // token name - nftTokenSymbol, // token symbol - ownerAddress, // owner address - minterAddress // minter address (who signs off-chain mint requests) + // deploy NftReward contract + NftReward nftReward = new NftReward{salt: salt}(); + ERC1967Proxy proxy = new ERC1967Proxy(address(nftReward), + abi.encodeWithSignature("initialize(string,string,address,address)", + nftTokenName, nftTokenSymbol, ownerAddress, minterAddress) ); // stop sending owner transactions vm.stopBroadcast(); + + return address(proxy); } } diff --git a/src/NftReward.sol b/src/NftReward.sol index ec7a8f7..c4c54cf 100644 --- a/src/NftReward.sol +++ b/src/NftReward.sol @@ -1,18 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; /** * @title NFT reward contract * @notice Allows NFT minter to sign off-chain mint requests for target users * who can later claim NFTs by minter's signature */ -contract NftReward is ERC721, Ownable, Pausable, EIP712 { +contract NftReward is Initializable, ERC721Upgradeable, OwnableUpgradeable, PausableUpgradeable, EIP712Upgradeable, UUPSUpgradeable { /// @notice Base URI used for all of the tokens string public baseUri; @@ -49,22 +50,35 @@ contract NftReward is ERC721, Ownable, Pausable, EIP712 { } /** - * @notice Contract constructor + * @notice _disableInitializers in the constructor, + * this prevents initialization of the implementation contract itself, + * as extra protection to prevent an attacker from initializing it. + */ + constructor() { + _disableInitializers(); + } + + /** + * @notice Contract initializer (replaces constructor) * @param _tokenName NFT token name * @param _tokenSymbol NFT token symbol * @param _initialOwner Initial owner name * @param _minter Minter address */ - constructor ( + function initialize( string memory _tokenName, string memory _tokenSymbol, address _initialOwner, address _minter ) - ERC721(_tokenName, _tokenSymbol) - Ownable(_initialOwner) - EIP712("NftReward-Domain", "1") + public + initializer { + __ERC721_init(_tokenName, _tokenSymbol); + __Ownable_init(_initialOwner); + __EIP712_init("NftReward-Domain", "1"); + __UUPSUpgradeable_init(); + __Pausable_init(); minter = _minter; } @@ -193,6 +207,13 @@ contract NftReward is ERC721, Ownable, Pausable, EIP712 { _unpause(); } + /** + * @notice Upgrades contract to new implementation + * @param newImplementation New implementation address + */ + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + //==================== // Internal methods //==================== diff --git a/test/NftReward.t.sol b/test/NftReward.t.sol index 190d5fa..a3a798b 100644 --- a/test/NftReward.t.sol +++ b/test/NftReward.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {NftReward} from "../src/NftReward.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract NftRewardTest is Test { NftReward nftReward; @@ -14,14 +15,23 @@ contract NftRewardTest is Test { address user2 = address(3); address minter = vm.addr(minterPrivateKey); + bytes public data; + function setUp() public { vm.prank(owner); - nftReward = new NftReward( - "NFT reward", // token name - "RWD", // token symbol - owner, // initial owner - minter // minter (off-chain signer) + + data = abi.encodeWithSignature( + "initialize(string,string,address,address)", + "NFT reward", + "RWD", + owner, + minter ); + + nftReward = new NftReward(); + ERC1967Proxy proxy = new ERC1967Proxy(address(nftReward), data); + + nftReward = NftReward(address(proxy)); } //================== @@ -47,7 +57,7 @@ contract NftRewardTest is Test { // get mint request digest which should be signed bytes32 digest = nftReward.getMintRequestDigest(mintRequest); - assertEq(digest, 0x2c680706f2350ed5622f229af6736cd20626f7b9b4569b2fd5abb7e086886dc3); + assertEq(digest, 0xf85127466b4ea4a97ec0c1b445b54622c8c3760b720bf9136b491d50f122b043); } function testGetTokenDataKeys_ReturnAllTokenDataKeys() public {