Skip to content

Commit

Permalink
Made premint executor upgradeable via a proxy. (#168)
Browse files Browse the repository at this point in the history
Moved some common factory setup logic to some library fixtures
  • Loading branch information
oveddan committed Sep 20, 2023
1 parent baae149 commit 5dee2b5
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 25 deletions.
4 changes: 3 additions & 1 deletion src/interfaces/IContractMetadata.sol
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions src/premint/ZoraCreator1155Attribution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ library ZoraCreator1155Attribution {
}
}

// todo: make it consistent.
library PremintTokenSetup {
uint256 constant PERMISSION_BIT_MINTER = 2 ** 2;

Expand Down
35 changes: 32 additions & 3 deletions src/premint/ZoraCreator1155PremintExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)));
}
}
35 changes: 35 additions & 0 deletions src/proxies/Zora1155PremintExecutorProxy.sol
Original file line number Diff line number Diff line change
@@ -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) {}
}
48 changes: 48 additions & 0 deletions test/fixtures/Zora1155FactoryFixtures.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
40 changes: 40 additions & 0 deletions test/fixtures/Zora1155PremintFixtures.sol
Original file line number Diff line number Diff line change
@@ -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)
});
}
}
111 changes: 111 additions & 0 deletions test/premint/Zora1155PremintExecutorProxy.t.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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) {
Expand Down

0 comments on commit 5dee2b5

Please sign in to comment.