diff --git a/src/interfaces/IContractMetadata.sol b/src/interfaces/IContractMetadata.sol index 10dccd9dd..faddf69ae 100644 --- a/src/interfaces/IContractMetadata.sol +++ b/src/interfaces/IContractMetadata.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; -interface IContractMetadata { +interface IHasContractName { /// @notice Contract name returns the pretty contract name function contractName() external returns (string memory); +} +interface IContractMetadata is IHasContractName { /// @notice Contract URI returns the uri for more information about the given contract function contractURI() external returns (string memory); } diff --git a/src/premint/ZoraCreator1155Attribution.sol b/src/premint/ZoraCreator1155Attribution.sol index cc3d4bcf7..4c7f5cc2e 100644 --- a/src/premint/ZoraCreator1155Attribution.sol +++ b/src/premint/ZoraCreator1155Attribution.sol @@ -176,6 +176,7 @@ library ZoraCreator1155Attribution { } } +// todo: make it consistent. library PremintTokenSetup { uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; diff --git a/src/premint/ZoraCreator1155PremintExecutor.sol b/src/premint/ZoraCreator1155PremintExecutor.sol index c310eae51..cff98d7d2 100644 --- a/src/premint/ZoraCreator1155PremintExecutor.sol +++ b/src/premint/ZoraCreator1155PremintExecutor.sol @@ -2,8 +2,9 @@ pragma solidity 0.8.17; import {ICreatorRoyaltiesControl} from "../interfaces/ICreatorRoyaltiesControl.sol"; -import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; +import {UUPSUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {Ownable2StepUpgradeable} from "../utils/ownable/Ownable2StepUpgradeable.sol"; +import {IHasContractName} from "../interfaces/IContractMetadata.sol"; import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol"; import {IZoraCreator1155Factory} from "../interfaces/IZoraCreator1155Factory.sol"; import {SharedBaseConstants} from "../shared/SharedBaseConstants.sol"; @@ -15,7 +16,7 @@ import {PremintConfig, ContractCreationConfig, TokenCreationConfig, ZoraCreator1 /// Signature must provided by the contract creator, or an account that's permitted to create new tokens on the contract. /// Mints the first x tokens to the executor of the transaction. /// @author @oveddan -contract ZoraCreator1155PremintExecutor { +contract ZoraCreator1155PremintExecutor is Ownable2StepUpgradeable, UUPSUpgradeable, IHasContractName { IZoraCreator1155Factory public immutable zora1155Factory; /// @notice copied from SharedBaseConstants @@ -30,6 +31,11 @@ contract ZoraCreator1155PremintExecutor { zora1155Factory = _factory; } + function initialize(address _initialOwner) public initializer { + __Ownable_init(_initialOwner); + __UUPSUpgradeable_init(); + } + event Preminted( address indexed contractAddress, uint256 indexed tokenId, @@ -162,4 +168,27 @@ contract ZoraCreator1155PremintExecutor { isValid = IZoraCreator1155(contractAddress).isAdminOrRole(recoveredSigner, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER); } } + + // upgrade related functionality + + /// @notice The name of the contract for upgrade purposes + function contractName() external pure returns (string memory) { + return "ZORA 1155 Premint Executor"; + } + + // upgrade functionality + error UpgradeToMismatchedContractName(string expected, string actual); + + /// @notice Ensures the caller is authorized to upgrade the contract + /// @dev This function is called in `upgradeTo` & `upgradeToAndCall` + /// @param _newImpl The new implementation address + function _authorizeUpgrade(address _newImpl) internal override onlyOwner { + if (!_equals(IHasContractName(_newImpl).contractName(), this.contractName())) { + revert UpgradeToMismatchedContractName(this.contractName(), IHasContractName(_newImpl).contractName()); + } + } + + function _equals(string memory a, string memory b) internal pure returns (bool) { + return (keccak256(bytes(a)) == keccak256(bytes(b))); + } } diff --git a/src/proxies/Zora1155PremintExecutorProxy.sol b/src/proxies/Zora1155PremintExecutorProxy.sol new file mode 100644 index 000000000..01bf0c0bc --- /dev/null +++ b/src/proxies/Zora1155PremintExecutorProxy.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {Enjoy} from "_imagine/mint/Enjoy.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +/* + + + ░░░░░░░░░░░░░░ + ░░▒▒░░░░░░░░░░░░░░░░░░░░ + ░░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░ + ░░▒▒▒▒░░░░░░░░░░░░░░ ░░░░░░░░ + ░▓▓▒▒▒▒░░░░░░░░░░░░ ░░░░░░░ + ░▓▓▓▒▒▒▒░░░░░░░░░░░░ ░░░░░░░░ + ░▓▓▓▒▒▒▒░░░░░░░░░░░░░░ ░░░░░░░░░░ + ░▓▓▓▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░ + ░▓▓▓▓▓▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░ + ░▓▓▓▓▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░ + ░░▓▓▓▓▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ + ░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒░░░░░░░░░▒▒▒▒▒░░ + ░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░ + ░░▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░ + + OURS TRULY, + + + */ + +/// Imagine. Mint. Enjoy. +/// @notice Imagine. Mint. Enjoy. +/// @author @oveddan +contract Zora1155PremintExecutorProxy is Enjoy, ERC1967Proxy { + constructor(address _logic, bytes memory _data) ERC1967Proxy(_logic, _data) {} +} diff --git a/test/fixtures/Zora1155FactoryFixtures.sol b/test/fixtures/Zora1155FactoryFixtures.sol new file mode 100644 index 000000000..2b4c2b7ac --- /dev/null +++ b/test/fixtures/Zora1155FactoryFixtures.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; +import {ZoraCreatorFixedPriceSaleStrategy} from "../../src/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; +import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; +import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; +import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; +import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol"; +import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.sol"; +import {ProxyShim} from "../../src/utils/ProxyShim.sol"; + +library Zora1155FactoryFixtures { + function setupZora1155Impl(uint256 mintFeeAmount, address zora, Zora1155Factory factoryProxy) internal returns (ZoraCreator1155Impl) { + ProtocolRewards rewards = new ProtocolRewards(); + return new ZoraCreator1155Impl(mintFeeAmount, zora, address(factoryProxy), address(rewards)); + } + + function upgradeFactoryProxyToUse1155( + Zora1155Factory factoryProxy, + IZoraCreator1155 zoraCreator1155Impl, + IMinter1155 fixedPriceMinter, + address admin + ) internal returns (ZoraCreator1155FactoryImpl factoryImpl) { + factoryImpl = new ZoraCreator1155FactoryImpl(zoraCreator1155Impl, IMinter1155(address(1)), fixedPriceMinter, IMinter1155(address(3))); + + ZoraCreator1155FactoryImpl factoryAtProxy = ZoraCreator1155FactoryImpl(address(factoryProxy)); + + factoryAtProxy.upgradeTo(address(factoryImpl)); + factoryAtProxy.initialize(admin); + } + + function setupFactoryProxy(address deployer) internal returns (Zora1155Factory factoryProxy) { + address factoryShimAddress = address(new ProxyShim(deployer)); + factoryProxy = new Zora1155Factory(factoryShimAddress, ""); + } + + function setup1155AndFactoryProxy( + uint256 mintFeeAmount, + address zora, + address deployer + ) internal returns (ZoraCreator1155Impl zoraCreator1155Impl, IMinter1155 fixedPriceMinter, Zora1155Factory factoryProxy) { + factoryProxy = setupFactoryProxy(deployer); + fixedPriceMinter = new ZoraCreatorFixedPriceSaleStrategy(); + zoraCreator1155Impl = setupZora1155Impl(mintFeeAmount, zora, factoryProxy); + upgradeFactoryProxyToUse1155(factoryProxy, zoraCreator1155Impl, fixedPriceMinter, deployer); + } +} diff --git a/test/fixtures/Zora1155PremintFixtures.sol b/test/fixtures/Zora1155PremintFixtures.sol new file mode 100644 index 000000000..c1d4c0bc9 --- /dev/null +++ b/test/fixtures/Zora1155PremintFixtures.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; +import {ZoraCreatorFixedPriceSaleStrategy} from "../../src/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; +import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; +import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; +import {ICreatorRoyaltiesControl} from "../../src/interfaces/ICreatorRoyaltiesControl.sol"; +import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; +import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol"; +import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.sol"; +import {ProxyShim} from "../../src/utils/ProxyShim.sol"; +import {ContractCreationConfig, TokenCreationConfig, PremintConfig} from "../../src/premint/ZoraCreator1155Attribution.sol"; + +library Zora1155PremintFixtures { + function makeDefaultContractCreationConfig(address contractAdmin) internal pure returns (ContractCreationConfig memory) { + return ContractCreationConfig({contractAdmin: contractAdmin, contractName: "blah", contractURI: "blah.contract"}); + } + + function defaultRoyaltyConfig(address royaltyRecipient) internal pure returns (ICreatorRoyaltiesControl.RoyaltyConfiguration memory) { + return ICreatorRoyaltiesControl.RoyaltyConfiguration({royaltyBPS: 10, royaltyRecipient: royaltyRecipient, royaltyMintSchedule: 100}); + } + + function makeDefaultTokenCreationConfig(IMinter1155 fixedPriceMinter, address royaltyRecipient) internal pure returns (TokenCreationConfig memory) { + ICreatorRoyaltiesControl.RoyaltyConfiguration memory royaltyConfig = defaultRoyaltyConfig(royaltyRecipient); + return + TokenCreationConfig({ + tokenURI: "blah.token", + maxSupply: 10, + maxTokensPerAddress: 5, + pricePerToken: 0, + mintStart: 0, + mintDuration: 0, + royaltyMintSchedule: royaltyConfig.royaltyMintSchedule, + royaltyBPS: royaltyConfig.royaltyBPS, + royaltyRecipient: royaltyConfig.royaltyRecipient, + fixedPriceMinter: address(fixedPriceMinter) + }); + } +} diff --git a/test/premint/Zora1155PremintExecutorProxy.t.sol b/test/premint/Zora1155PremintExecutorProxy.t.sol new file mode 100644 index 000000000..7873e48aa --- /dev/null +++ b/test/premint/Zora1155PremintExecutorProxy.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; +import {Zora1155FactoryFixtures} from "../fixtures/Zora1155FactoryFixtures.sol"; +import {Zora1155PremintFixtures} from "../fixtures/Zora1155PremintFixtures.sol"; +import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol"; +import {Zora1155PremintExecutorProxy} from "../../src/proxies/Zora1155PremintExecutorProxy.sol"; +import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; +import {ZoraCreator1155PremintExecutor} from "../../src/premint/ZoraCreator1155PremintExecutor.sol"; +import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; +import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; +import {ProxyShim} from "../../src/utils/ProxyShim.sol"; +import {ZoraCreator1155Attribution, ContractCreationConfig, TokenCreationConfig, PremintConfig} from "../../src/premint/ZoraCreator1155Attribution.sol"; +import {IOwnable2StepUpgradeable} from "../../src/utils/ownable/IOwnable2StepUpgradeable.sol"; +import {IHasContractName} from "../../src/interfaces/IContractMetadata.sol"; + +contract Zora1155PremintExecutorProxyTest is Test, IHasContractName { + address internal owner; + uint256 internal creatorPrivateKey; + address internal creator; + address internal collector; + address internal zora; + Zora1155Factory internal factoryProxy; + ZoraCreator1155FactoryImpl factoryAtProxy; + uint256 internal mintFeeAmount = 0.000777 ether; + ZoraCreator1155PremintExecutor preminterAtProxy; + + function setUp() external { + zora = makeAddr("zora"); + owner = makeAddr("owner"); + collector = makeAddr("collector"); + (creator, creatorPrivateKey) = makeAddrAndKey("creator"); + + vm.startPrank(zora); + (, , factoryProxy) = Zora1155FactoryFixtures.setup1155AndFactoryProxy(mintFeeAmount, zora, zora); + factoryAtProxy = ZoraCreator1155FactoryImpl(address(factoryProxy)); + vm.stopPrank(); + + // create preminter implementation + ZoraCreator1155PremintExecutor preminterImplementation = new ZoraCreator1155PremintExecutor(ZoraCreator1155FactoryImpl(address(factoryProxy))); + + // build the proxy + Zora1155PremintExecutorProxy proxy = new Zora1155PremintExecutorProxy(address(preminterImplementation), ""); + + // access the executor implementation via the proxy, and initialize the admin + preminterAtProxy = ZoraCreator1155PremintExecutor(address(proxy)); + preminterAtProxy.initialize(owner); + } + + function test_canInvokeImplementationMethods() external { + // create premint config + IMinter1155 fixedPriceMinter = ZoraCreator1155FactoryImpl(address(factoryProxy)).fixedPriceMinter(); + + PremintConfig memory premintConfig = PremintConfig({ + tokenConfig: Zora1155PremintFixtures.makeDefaultTokenCreationConfig(fixedPriceMinter, creator), + uid: 100, + version: 0, + deleted: false + }); + + // now interface with proxy preminter - sign and execute the premint + ContractCreationConfig memory contractConfig = Zora1155PremintFixtures.makeDefaultContractCreationConfig(creator); + address deterministicAddress = preminterAtProxy.getContractAddress(contractConfig); + + // sign the premint + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, deterministicAddress, block.chainid); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPrivateKey, digest); + + uint256 quantityToMint = 1; + + bytes memory signature = abi.encodePacked(r, s, v); + + // execute the premint + vm.deal(collector, mintFeeAmount); + vm.prank(collector); + uint256 tokenId = preminterAtProxy.premint{value: mintFeeAmount}(contractConfig, premintConfig, signature, quantityToMint, ""); + + assertEq(ZoraCreator1155Impl(deterministicAddress).balanceOf(collector, tokenId), 1); + } + + function test_onlyOwnerCanUpgrade() external { + // try to upgrade as non-owner + ZoraCreator1155PremintExecutor newImplementation = new ZoraCreator1155PremintExecutor(factoryAtProxy); + + vm.expectRevert(IOwnable2StepUpgradeable.ONLY_OWNER.selector); + vm.prank(creator); + preminterAtProxy.upgradeTo(address(newImplementation)); + } + + /// giving this a contract name so that it can be used to fail upgrading preminter contract + function contractName() public pure returns (string memory) { + return "Test Contract"; + } + + function test_canOnlyBeUpgradedToContractWithSameName() external { + // upgrade to bad contract with has wrong name (this contract has mismatched name) + vm.expectRevert( + abi.encodeWithSelector(ZoraCreator1155PremintExecutor.UpgradeToMismatchedContractName.selector, preminterAtProxy.contractName(), contractName()) + ); + vm.prank(owner); + preminterAtProxy.upgradeTo(address(this)); + + // upgrade to good contract which has correct name - it shouldn't revert + ZoraCreator1155PremintExecutor newImplementation = new ZoraCreator1155PremintExecutor(ZoraCreator1155FactoryImpl(address(factoryProxy))); + + vm.prank(owner); + preminterAtProxy.upgradeTo(address(newImplementation)); + } +} diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155PremintExecutor.t.sol similarity index 95% rename from test/premint/ZoraCreator1155Preminter.t.sol rename to test/premint/ZoraCreator1155PremintExecutor.t.sol index 9f4a4b60e..1e3b22515 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155PremintExecutor.t.sol @@ -2,22 +2,18 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; +import {Zora1155FactoryFixtures} from "../fixtures/Zora1155FactoryFixtures.sol"; import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.sol"; -import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.sol"; import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; import {Zora1155} from "../../src/proxies/Zora1155.sol"; import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; -import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; import {ICreatorRoyaltiesControl} from "../../src/interfaces/ICreatorRoyaltiesControl.sol"; -import {IZoraCreator1155Factory} from "../../src/interfaces/IZoraCreator1155Factory.sol"; -import {ILimitedMintPerAddress} from "../../src/interfaces/ILimitedMintPerAddress.sol"; import {ZoraCreatorFixedPriceSaleStrategy} from "../../src/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol"; import {ZoraCreator1155PremintExecutor} from "../../src/premint/ZoraCreator1155PremintExecutor.sol"; -import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; import {ZoraCreator1155Attribution, ContractCreationConfig, TokenCreationConfig, PremintConfig} from "../../src/premint/ZoraCreator1155Attribution.sol"; import {ForkDeploymentConfig} from "../../src/deployment/DeploymentConfig.sol"; import {ProxyShim} from "../../src/utils/ProxyShim.sol"; @@ -27,11 +23,11 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { uint256 internal constant PERMISSION_BIT_MINTER = 2 ** 2; ZoraCreator1155PremintExecutor internal preminter; - ZoraCreator1155FactoryImpl internal factoryImpl; - ZoraCreator1155FactoryImpl internal factory; + Zora1155Factory factoryProxy; + ZoraCreator1155FactoryImpl factoryImpl; ICreatorRoyaltiesControl.RoyaltyConfiguration internal defaultRoyaltyConfig; - uint256 internal mintFeeAmount; + uint256 internal mintFeeAmount = 0.000777 ether; // setup contract config uint256 internal creatorPrivateKey; @@ -52,27 +48,18 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { ); function setUp() external { - mintFeeAmount = 0.000777 ether; - (creator, creatorPrivateKey) = makeAddrAndKey("creator"); zora = makeAddr("zora"); premintExecutor = makeAddr("premintExecutor"); collector = makeAddr("collector"); - address factoryShimAddress = address(new ProxyShim(zora)); - Zora1155Factory factoryProxy = new Zora1155Factory(factoryShimAddress, ""); - ProtocolRewards rewards = new ProtocolRewards(); - ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(mintFeeAmount, zora, address(factoryProxy), address(rewards)); - ZoraCreatorFixedPriceSaleStrategy fixedPriceMinter = new ZoraCreatorFixedPriceSaleStrategy(); - factoryImpl = new ZoraCreator1155FactoryImpl(zoraCreator1155Impl, IMinter1155(address(1)), fixedPriceMinter, IMinter1155(address(3))); - factory = ZoraCreator1155FactoryImpl(address(factoryProxy)); - vm.startPrank(zora); - factory.upgradeTo(address(factoryImpl)); - factory.initialize(zora); + (, , factoryProxy) = Zora1155FactoryFixtures.setup1155AndFactoryProxy(mintFeeAmount, zora, zora); vm.stopPrank(); - preminter = new ZoraCreator1155PremintExecutor(factory); + factoryImpl = ZoraCreator1155FactoryImpl(address(factoryProxy)); + + preminter = new ZoraCreator1155PremintExecutor(factoryImpl); } function makeDefaultContractCreationConfig() internal view returns (ContractCreationConfig memory) {