Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creator attribution - added status rpc read methods #157

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 32 additions & 30 deletions package/preminter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { describe, it, beforeEach, expect } from "vitest";
import { parseEther } from "viem";
import {
zoraCreator1155PremintExecutorABI as preminterAbi,
zoraCreator1155PremintExecutorAddress,
zoraCreator1155ImplABI,
zoraCreator1155FactoryImplAddress,
zoraCreator1155FactoryImplConfig,
Expand Down Expand Up @@ -211,15 +212,16 @@ describe("ZoraCreator1155Preminter", () => {

ctx.preminterAddress = preminterAddress;
}, 20 * 1000);

it<TestContext>(
"can sign for another chain",
async ({ preminterAddress: preminterAddress, fixedPriceMinterAddress }) => {
"can sign on the forked premint contract",
async ({ fixedPriceMinterAddress, forkedChainId }) => {
const premintConfig = defaultPremintConfig(fixedPriceMinterAddress);
const contractConfig = defaultContractConfig({
contractAdmin: creatorAccount,
});

const preminterAddress = zoraCreator1155PremintExecutorAddress[forkedChainId as keyof typeof zoraCreator1155PremintExecutorAddress] as Address;

const contractAddress = await publicClient.readContract({
abi: preminterAbi,
address: preminterAddress,
Expand All @@ -239,14 +241,10 @@ describe("ZoraCreator1155Preminter", () => {
console.log({
creatorAccount,
signedMessage,
contractConfig,
premintConfig,
contractAddress: await publicClient.readContract({
abi: preminterAbi,
address: preminterAddress,
functionName: "getContractAddress",
args: [contractConfig],
}),
});
contractAddress
});
},
20 * 1000
);
Expand Down Expand Up @@ -285,20 +283,14 @@ describe("ZoraCreator1155Preminter", () => {
abi: preminterAbi,
address: preminterAddress,
functionName: "recoverSigner",
args: [
premintConfig,
contractAddress,
signedMessage,
BigInt(anvilChainId),
],
args: [premintConfig, contractAddress, signedMessage],
});

expect(recoveredAddress).to.equal(creatorAccount);
},

20 * 1000
);

it<TestContext>(
"can sign and mint multiple tokens",
async ({
Expand Down Expand Up @@ -349,7 +341,16 @@ describe("ZoraCreator1155Preminter", () => {
});

// get the premint status - it should not be minted
let tokenId: bigint;
let [contractCreated, tokenId] = await publicClient.readContract({
abi: preminterAbi,
address: preminterAddress,
functionName: "premintStatus",
args: [contractAddress, premintConfig.uid],
});

expect(contractCreated).toBe(false);
expect(tokenId).toBe(0n);

// now have the collector execute the first signed message;
// it should create the contract, the token,
// and min the quantity to mint tokens to the collector
Expand Down Expand Up @@ -378,21 +379,21 @@ describe("ZoraCreator1155Preminter", () => {
expect(receipt.status).toBe("success");

// fetch the premint token id
let newTokenId = await publicClient.readContract({
abi: zoraCreator1155ImplABI,
address: contractAddress,
functionName: "delegatedTokenId",
args: [premintConfig.uid],
[contractCreated, tokenId] = await publicClient.readContract({
abi: preminterAbi,
address: preminterAddress,
functionName: "premintStatus",
args: [contractAddress, premintConfig.uid],
});

expect(newTokenId).not.toBe(0n);
expect(tokenId).not.toBe(0n);

// now use what was created, to get the balance from the created contract
const tokenBalance = await publicClient.readContract({
abi: zoraCreator1155ImplABI,
address: contractAddress,
functionName: "balanceOf",
args: [collectorAccount, newTokenId],
args: [collectorAccount, tokenId],
});

// get token balance - should be amount that was created
Expand Down Expand Up @@ -447,11 +448,11 @@ describe("ZoraCreator1155Preminter", () => {
).toBe("success");

// now premint status for the second mint, it should be minted
tokenId = await publicClient.readContract({
abi: zoraCreator1155ImplABI,
address: contractAddress,
functionName: "delegatedTokenId",
args: [premintConfig2.uid],
[, tokenId] = await publicClient.readContract({
abi: preminterAbi,
address: preminterAddress,
functionName: "premintStatus",
args: [contractAddress, premintConfig2.uid],
});

expect(tokenId).not.toBe(0n);
Expand All @@ -469,4 +470,5 @@ describe("ZoraCreator1155Preminter", () => {
// 10 second timeout
40 * 1000
);

});
2 changes: 2 additions & 0 deletions src/interfaces/IZoraCreator1155.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ interface IZoraCreator1155 is IZoraCreator1155TypesV1, IVersionedContract, IOwna

function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature) external returns (uint256 newTokenId);

function delegatedTokenId(uint32 uid) external view returns (uint256 tokenId);

function updateTokenURI(uint256 tokenId, string memory _newURI) external;

function updateContractMetadata(string memory _newURI, string memory _newName) external;
Expand Down
82 changes: 51 additions & 31 deletions src/premint/ZoraCreator1155PremintExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ import {ZoraCreatorFixedPriceSaleStrategy} from "../minters/fixed-price/ZoraCrea
import {IMinter1155} from "../interfaces/IMinter1155.sol";
import {PremintConfig, ContractCreationConfig, TokenCreationConfig, ZoraCreator1155Attribution} from "./ZoraCreator1155Attribution.sol";

/// @title Enables a creator to signal intent to create a Zora erc1155 contract or new token on that
/// contract by signing a transaction but not paying gas, and have a third party/collector pay the gas
/// by executing the transaction. Incentivizes the third party to execute the transaction by offering
/// a reward in the form of minted tokens.
/// @title Enables creation of and minting tokens on Zora1155 contracts transactions using eip-712 signatures.
/// 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 {
IZoraCreator1155Factory factory;
Expand All @@ -27,7 +26,6 @@ contract ZoraCreator1155PremintExecutor {
error MintNotYetStarted();
error InvalidSignature();

// todo: make a constructor
constructor(IZoraCreator1155Factory _factory) {
factory = _factory;
}
Expand All @@ -43,37 +41,26 @@ contract ZoraCreator1155PremintExecutor {
uint256 quantityMinted
);

// same signature should work whether or not there is an existing contract
// so it is unaware of order, it just takes the token uri and creates the next token with it
// this could include creating the contract.
/// Creates a new token on the given erc1155 contract on behalf of a creator, and mints x tokens to the executor of this transaction.
/// If the erc1155 contract hasn't been created yet, it will be created with the given config within this same transaction.
/// The creator must sign the intent to create the token, and must have mint new token permission on the erc1155 contract,
/// or match the contract admin on the contract creation config if the contract hasn't been created yet.
/// Contract address of the created contract is deterministically generated from the contract config and this contract's address.
function premint(
ContractCreationConfig calldata contractConfig,
PremintConfig calldata premintConfig,
bytes calldata signature,
uint256 quantityToMint,
string calldata mintComment
) public payable returns (uint256 newTokenId) {
// 1. Validate the signature.
// 2. get or create an erc1155 contract with the same determinsitic address as that from the contract config
// 3. Have the erc1155 contract create a new token. the signer must have permission to do mint new tokens
// (that role enforcement is expected to be in the tokenContract).
// 4. The erc1155 will sedtup the token with the signign address as the creator, and follow the creator rewards standard.
// 5. Mint x tokens, as configured, to the executor of this transaction.
// 6. Future: First minter gets rewards

// get or create the contract with the given params
// contract address is deterministic.
(IZoraCreator1155 tokenContract, bool isNewContract) = _getOrCreateContract(contractConfig);
address contractAddress = address(tokenContract);

// have the address setup the new token. The signer must have permission to do this.
// (that role enforcement is expected to be in the tokenContract).
// the token contract will:

// * setup the token with the signer as the creator, and follow the creator rewards standard.
// * will revert if the token in the contract with the same uid already exists.
// * will make sure creator has admin rights to the token.
// * setup the token with the given token config.
// * return the new token id.
// pass the signature and the premint config to the token contract to create the token.
// The token contract will verify the signature and that the signer has permission to create a new token.
// and then create and setup the token using the given token config.
newTokenId = tokenContract.delegateSetupNewToken(premintConfig, signature);

// mint the initial x tokens for this new token id to the executor.
Expand Down Expand Up @@ -119,16 +106,49 @@ contract ZoraCreator1155PremintExecutor {
tokenContract = IZoraCreator1155(newContractAddresss);
}

/// Gets the deterministic contract address for the given contract creation config.
/// Contract address is generated deterministically from a hash based onthe contract uri, contract name,
/// contract admin, and the msg.sender, which is this contract's address.
function getContractAddress(ContractCreationConfig calldata contractConfig) public view returns (address) {
return factory.deterministicContractAddress(address(this), contractConfig.contractURI, contractConfig.contractName, contractConfig.contractAdmin);
}

function recoverSigner(
/// Recovers the signer of the given premint config created against the specified zora1155 contract address.
function recoverSigner(PremintConfig calldata premintConfig, address zor1155Address, bytes calldata signature) public view returns (address) {
return ZoraCreator1155Attribution.recoverSigner(premintConfig, signature, zor1155Address, block.chainid);
}

/// @notice Utility function to determine if a premint contract has been created for a uid of a premint, and if so,
/// What is the token id that was created for the uid.
function premintStatus(address contractAddress, uint32 uid) public view returns (bool contractCreated, uint256 tokenIdForPremint) {
if (contractAddress.code.length == 0) {
return (false, 0);
}
return (true, IZoraCreator1155(contractAddress).delegatedTokenId(uid));
}

/// @notice Utility function to check if the signature is valid; i.e. the signature can be used to
/// mint a token with the given config. If the contract hasn't been created, then the signer
/// must match the contract admin on the premint config. If it has been created, the signer
/// must have permission to create new tokens on the erc1155 contract.
function isValidSignature(
ContractCreationConfig calldata contractConfig,
PremintConfig calldata premintConfig,
address zor1155Address,
bytes calldata signature,
uint256 chainId
) public pure returns (address) {
return ZoraCreator1155Attribution.recoverSigner(premintConfig, signature, zor1155Address, chainId);
bytes calldata signature
) public view returns (bool isValid, address contractAddress, address recoveredSigner) {
contractAddress = getContractAddress(contractConfig);
recoveredSigner = recoverSigner(premintConfig, contractAddress, signature);

if (recoveredSigner == address(0)) {
return (false, contractAddress, address(0));
}

// if contract hasn't been created, signer must be the contract admin on the config
if (contractAddress.code.length == 0) {
isValid = recoveredSigner == contractConfig.contractAdmin;
} else {
// if contract has been created, signer must have mint new token permission
isValid = IZoraCreator1155(contractAddress).isAdminOrRole(recoveredSigner, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER);
}
}
}
83 changes: 82 additions & 1 deletion test/premint/ZoraCreator1155Preminter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,87 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test {
IZoraCreator1155(contractAddress).mint(fixedPriceMinter, tokenId, quantityToMint, abi.encode(premintExecutor, comment));
}

function test_premintStatus_getsIfContractHasBeenCreatedAndTokenIdForPremint() external {
// build a premint
ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig();
PremintConfig memory premintConfig = makeDefaultPremintConfig();

// get premint status
(bool contractCreated, uint256 tokenId) = preminter.premintStatus(preminter.getContractAddress(contractConfig), premintConfig.uid);
// contract should not be created and token id should be 0
assertEq(contractCreated, false);
assertEq(tokenId, 0);

// sign and execute premint
uint256 newTokenId = _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, block.chainid, vm.addr(701), 1, "hi");

// get status
(contractCreated, tokenId) = preminter.premintStatus(preminter.getContractAddress(contractConfig), premintConfig.uid);
// contract should be created and token id should be same as one that was created
assertEq(contractCreated, true);
assertEq(tokenId, newTokenId);

// get status for another uid
(contractCreated, tokenId) = preminter.premintStatus(preminter.getContractAddress(contractConfig), premintConfig.uid + 1);
// contract should be created and token id should be 0
assertEq(contractCreated, true);
assertEq(tokenId, 0);
}

// todo: pull from elsewhere
uint256 constant CONTRACT_BASE_ID = 0;
uint256 constant PERMISSION_BIT_MINTER = 2 ** 2;

function test_premint_whenContractCreated_premintCanOnlyBeExecutedByPermissionBitMinter() external {
// build a premint
ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig();
PremintConfig memory premintConfig = makeDefaultPremintConfig();

address executor = vm.addr(701);

// sign and execute premint
bytes memory signature = _signPremint(preminter.getContractAddress(contractConfig), premintConfig, creatorPrivateKey, block.chainid);

(bool isValidSignature, address contractAddress, ) = preminter.isValidSignature(contractConfig, premintConfig, signature);

assertTrue(isValidSignature);

_signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, block.chainid, executor, 1, "hi");

// contract has been created

// have another creator sign a premint
uint256 newCreatorPrivateKey = 0xA11CF;
address newCreator = vm.addr(newCreatorPrivateKey);
PremintConfig memory premintConfig2 = premintConfig;
premintConfig2.uid++;

// have new creator sign a premint, isValidSignature should be false, and premint should revert
bytes memory newCreatorSignature = _signPremint(contractAddress, premintConfig2, newCreatorPrivateKey, block.chainid);

// it should not be considered a valid signature
(isValidSignature, , ) = preminter.isValidSignature(contractConfig, premintConfig2, newCreatorSignature);

assertFalse(isValidSignature);

// try to mint, it should revert
vm.expectRevert(abi.encodeWithSelector(IZoraCreator1155.UserMissingRoleForToken.selector, newCreator, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER));
vm.prank(executor);
preminter.premint(contractConfig, premintConfig2, newCreatorSignature, 1, "yo");

// now grant the new creator permission to mint
vm.prank(creator);
IZoraCreator1155(contractAddress).addPermission(CONTRACT_BASE_ID, newCreator, PERMISSION_BIT_MINTER);

// should now be considered a valid signature
(isValidSignature, , ) = preminter.isValidSignature(contractConfig, premintConfig2, newCreatorSignature);
assertTrue(isValidSignature);

// try to mint again, should not revert
vm.prank(executor);
preminter.premint(contractConfig, premintConfig2, newCreatorSignature, 1, "yo");
}

function _signAndExecutePremint(
ContractCreationConfig memory contractConfig,
PremintConfig memory premintConfig,
Expand All @@ -540,7 +621,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test {
PremintConfig memory premintConfig,
uint256 privateKey,
uint256 chainId
) private view returns (bytes memory) {
) private pure returns (bytes memory) {
bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId);

// 3. Sign the digest
Expand Down
Loading