Skip to content

Commit

Permalink
wip on v1/v2 of create referral fix
Browse files Browse the repository at this point in the history
  • Loading branch information
oveddan committed Oct 17, 2023
1 parent 5ca081e commit 6f3877a
Show file tree
Hide file tree
Showing 12 changed files with 498 additions and 262 deletions.
22 changes: 22 additions & 0 deletions packages/1155-contracts/src/delegation/EIP712.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.sol";

library EIP712 {
bytes32 internal constant TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

/**
* @dev Returns the domain separator for the specified chain.
*/
function _domainSeparatorV4(uint256 chainId, address verifyingContract, bytes32 hashedName, bytes32 hashedVersion) private pure returns (bytes32) {
return _buildDomainSeparator(hashedName, hashedVersion, verifyingContract, chainId);
}

function _buildDomainSeparator(bytes32 nameHash, bytes32 versionHash, address verifyingContract, uint256 chainId) private pure returns (bytes32) {
return keccak256(abi.encode(TYPE_HASH, nameHash, versionHash, chainId, verifyingContract));
}

function hashTypedDataV4(bytes32 structHash, bytes32 hashedName, bytes32 hashedVersion, address verifyingContract, uint256 chainId) internal pure returns (bytes32) {
return ECDSAUpgradeable.toTypedDataHash(_domainSeparatorV4(chainId, verifyingContract, hashedName, hashedVersion), structHash);
}
}
237 changes: 152 additions & 85 deletions packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol";
import {ICreatorRoyaltiesControl} from "../interfaces/ICreatorRoyaltiesControl.sol";
import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.sol";
import {ZoraCreatorFixedPriceSaleStrategy} from "../minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol";
import {EIP712} from './EIP712.sol';

struct ContractCreationConfig {
// Creator/admin of the created contract. Must match the account that signed the message
Expand All @@ -16,6 +17,18 @@ struct ContractCreationConfig {
string contractName;
}

struct PremintConfig {
// The config for the token to be created
TokenCreationConfig tokenConfig;
// Unique id of the token, used to ensure that multiple signatures can't be used to create the same intended token.
// only one signature per token id, scoped to the contract hash can be executed.
uint32 uid;
// Version of this premint, scoped to the uid and contract. Not used for logic in the contract, but used externally to track the newest version
uint32 version;
// If executing this signature results in preventing any signature with this uid from being minted.
bool deleted;
}

struct TokenCreationConfig {
// Metadata URI for the created token
string tokenURI;
Expand All @@ -37,13 +50,12 @@ struct TokenCreationConfig {
address royaltyRecipient;
// Fixed price minter address
address fixedPriceMinter;
// Create referral
address createReferral;
}

struct PremintConfig {

struct PremintConfigV2 {
// The config for the token to be created
TokenCreationConfig tokenConfig;
TokenCreationConfigV2 tokenConfig;
// Unique id of the token, used to ensure that multiple signatures can't be used to create the same intended token.
// only one signature per token id, scoped to the contract hash can be executed.
uint32 uid;
Expand All @@ -53,94 +65,64 @@ struct PremintConfig {
bool deleted;
}

/// @title Library for 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. Functions are exposed as external to allow contracts to import this lib and not increase their
/// size.
/// @author @oveddan
struct TokenCreationConfigV2 {
// Metadata URI for the created token
string tokenURI;
// Max supply of the created token
uint256 maxSupply;
// Max tokens that can be minted for an address, 0 if unlimited
uint64 maxTokensPerAddress;
// Price per token in eth wei. 0 for a free mint.
uint96 pricePerToken;
// The start time of the mint, 0 for immediate. Prevents signatures from being used until the start time.
uint64 mintStart;
// The duration of the mint, starting from the first mint of this token. 0 for infinite
uint64 mintDuration;
// Fixed price minter address
address fixedPriceMinter;
// create referral
address createReferral;
}

library ZoraCreator1155Attribution {
/* start eip712 functionality */
string internal constant NAME = "Preminter";
string internal constant VERSION = "1";
bytes32 internal constant HASHED_NAME = keccak256(bytes(NAME));
bytes32 internal constant HASHED_VERSION = keccak256(bytes(VERSION));
bytes32 internal constant TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
string internal constant VERSION_1 = "1";
bytes32 internal constant HASHED_VERSION_1 = keccak256(bytes(VERSION_1));
string internal constant VERSION_2 = "2";
bytes32 internal constant HASHED_VERSION_2 = keccak256(bytes(VERSION_2));

/**
* @dev Returns the domain separator for the specified chain.
*/
function _domainSeparatorV4(uint256 chainId, address verifyingContract) internal pure returns (bytes32) {
return _buildDomainSeparator(HASHED_NAME, HASHED_VERSION, verifyingContract, chainId);
}

function _buildDomainSeparator(bytes32 nameHash, bytes32 versionHash, address verifyingContract, uint256 chainId) private pure returns (bytes32) {
return keccak256(abi.encode(TYPE_HASH, nameHash, versionHash, chainId, verifyingContract));
}

function _hashTypedDataV4(bytes32 structHash, address verifyingContract, uint256 chainId) private pure returns (bytes32) {
return ECDSAUpgradeable.toTypedDataHash(_domainSeparatorV4(chainId, verifyingContract), structHash);
}

/* end eip712 functionality */

function recoverSigner(
PremintConfig calldata premintConfig,
bytes calldata signature,
address erc1155Contract,
uint256 chainId
) internal pure returns (address signatory) {
// first validate the signature - the creator must match the signer of the message
return recoverSignerHashed(hashPremint(premintConfig), signature, erc1155Contract, chainId);
}

function recoverSignerHashed(
bytes32 hashedPremintConfig,
bytes calldata signature,
address erc1155Contract,
uint256 chainId
) public pure returns (address signatory) {
// first validate the signature - the creator must match the signer of the message
bytes32 digest = _hashTypedDataV4(
hashedPremintConfig,
// here we pass the current contract and chain id, ensuring that the message
// only works for the current chain and contract id
erc1155Contract,
chainId
bytes32 constant ATTRIBUTION_DOMAIN_V1 =
keccak256(
"CreatorAttribution(TokenCreationConfig tokenConfig,uint32 uid,uint32 version,bool deleted)TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyMintSchedule,uint32 royaltyBPS,address royaltyRecipient,address fixedPriceMinter)"
);

signatory = ECDSAUpgradeable.recover(digest, signature);
}

/// Gets hash data to sign for a premint. Allows specifying a different chain id and contract address so that the signature
/// can be verified on a different chain.
/// @param erc1155Contract Contract address that signature is to be verified against
/// @param chainId Chain id that signature is to be verified on
function premintHashedTypeDataV4(PremintConfig calldata premintConfig, address erc1155Contract, uint256 chainId) external pure returns (bytes32) {
// build the struct hash to be signed
// here we pass the chain id, allowing the message to be signed for another chain
return _hashTypedDataV4(hashPremint(premintConfig), erc1155Contract, chainId);
}

bytes32 constant ATTRIBUTION_DOMAIN =
bytes32 constant ATTRIBUTION_DOMAIN_V2 =
keccak256(
"CreatorAttribution(TokenCreationConfig tokenConfig,uint32 uid,uint32 version,bool deleted)TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyMintSchedule,uint32 royaltyBPS,address royaltyRecipient,address fixedPriceMinter)"
"CreatorAttribution(TokenCreationConfig tokenConfig,uint32 uid,uint32 version,bool deleted)TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,address fixedPriceMinter,address createReferral)"
);

function hashPremint(PremintConfig calldata premintConfig) public pure returns (bytes32) {
function hashPremintV1(PremintConfig calldata premintConfig) public pure returns (bytes32) {
return
keccak256(abi.encode(ATTRIBUTION_DOMAIN, _hashToken(premintConfig.tokenConfig), premintConfig.uid, premintConfig.version, premintConfig.deleted));
keccak256(abi.encode(ATTRIBUTION_DOMAIN_V1, _hashTokenV1(premintConfig.tokenConfig), premintConfig.uid, premintConfig.version, premintConfig.deleted));
}

bytes32 constant TOKEN_DOMAIN =
function hashPremint(PremintConfigV2 calldata premintConfig) public pure returns (bytes32) {
return
keccak256(abi.encode(ATTRIBUTION_DOMAIN_V2, _hashToken(premintConfig.tokenConfig), premintConfig.uid, premintConfig.version, premintConfig.deleted));
}

bytes32 constant TOKEN_DOMAIN_V1 =
keccak256(
"TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyMintSchedule,uint32 royaltyBPS,address royaltyRecipient,address fixedPriceMinter)"
);

function _hashToken(TokenCreationConfig calldata tokenConfig) private pure returns (bytes32) {
function _hashTokenV1(TokenCreationConfig calldata tokenConfig) private pure returns (bytes32) {
return
keccak256(
abi.encode(
TOKEN_DOMAIN,
TOKEN_DOMAIN_V1,
_stringHash(tokenConfig.tokenURI),
tokenConfig.maxSupply,
tokenConfig.maxTokensPerAddress,
Expand All @@ -155,9 +137,105 @@ library ZoraCreator1155Attribution {
);
}

bytes32 constant TOKEN_DOMAIN_V2 =
keccak256(
"TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,address fixedPriceMinter,address createReferral)"
);

function _hashToken(TokenCreationConfigV2 calldata tokenConfig) private pure returns (bytes32) {
return
keccak256(
abi.encode(
TOKEN_DOMAIN_V1,
_stringHash(tokenConfig.tokenURI),
tokenConfig.maxSupply,
tokenConfig.maxTokensPerAddress,
tokenConfig.pricePerToken,
tokenConfig.mintStart,
tokenConfig.mintDuration,
tokenConfig.fixedPriceMinter,
tokenConfig.createReferral
)
);
}

bytes32 internal constant TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

function _buildDomainSeparator(bytes32 nameHash, bytes32 versionHash, address verifyingContract, uint256 chainId) private pure returns (bytes32) {
return keccak256(abi.encode(TYPE_HASH, nameHash, versionHash, chainId, verifyingContract));
}

/**
* @dev Returns the domain separator for the specified chain.
*/
function _domainSeparatorV4(uint256 chainId, address verifyingContract, bytes32 hashedName, bytes32 hashedVersion) private pure returns (bytes32) {
return _buildDomainSeparator(hashedName, hashedVersion, verifyingContract, chainId);
}

function hashTypedDataV4(bytes32 structHash, bytes32 hashedName, bytes32 hashedVersion, address verifyingContract, uint256 chainId) internal pure returns (bytes32) {
return ECDSAUpgradeable.toTypedDataHash(_domainSeparatorV4(chainId, verifyingContract, hashedName, hashedVersion), structHash);
}

/// Gets hash data to sign for a premint. Allows specifying a different chain id and contract address so that the signature
/// can be verified on a different chain.
/// @param erc1155Contract Contract address that signature is to be verified against
/// @param chainId Chain id that signature is to be verified on
function premintHashedTypeDataV4(bytes32 structHash, address erc1155Contract, bytes32 signatureVersion, uint256 chainId) public pure returns (bytes32) {
// build the struct hash to be signed
// here we pass the chain id, allowing the message to be signed for another chain
return hashTypedDataV4(structHash, HASHED_NAME, signatureVersion, erc1155Contract, chainId);
}

function recoverSignerHashed(
bytes32 hashedPremintConfig,
bytes calldata signature,
address erc1155Contract,
bytes32 signatureVersion,
uint256 chainId
) public pure returns (address signatory) {
// first validate the signature - the creator must match the signer of the message
bytes32 digest = premintHashedTypeDataV4(
hashedPremintConfig,
// here we pass the current contract and chain id, ensuring that the message
// only works for the current chain and contract id
erc1155Contract,
signatureVersion,
chainId
);

(signatory,) = ECDSAUpgradeable.tryRecover(digest, signature);
}

function _stringHash(string calldata value) private pure returns (bytes32) {
return keccak256(bytes(value));
}

/// @notice copied from SharedBaseConstants
uint256 constant CONTRACT_BASE_ID = 0;
/// @dev copied from ZoraCreator1155Impl
uint256 constant PERMISSION_BIT_MINTER = 2 ** 2;

function isValidSignature(
address originalPremintCreator,
address contractAddress,
bytes32 structHash,
bytes32 hashedVersion,
bytes calldata signature
) internal view returns (bool isValid, address recoveredSigner) {
recoveredSigner = recoverSignerHashed(structHash, signature, contractAddress, hashedVersion, block.chainid);

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

// if contract hasn't been created, signer must be the contract admin on the config
if (contractAddress.code.length == 0) {
isValid = recoveredSigner == originalPremintCreator;
} else {
// if contract has been created, signer must have mint new token permission
isValid = IZoraCreator1155(contractAddress).isAdminOrRole(recoveredSigner, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER);
}
}
}

/// @notice Utility library to setup tokens created via premint. Functions exposed as external to not increase contract size in calling contract.
Expand All @@ -168,9 +246,9 @@ library PremintTokenSetup {
function makeSetupNewTokenCalls(
uint256 newTokenId,
address contractAdmin,
TokenCreationConfig calldata tokenConfig
TokenCreationConfigV2 calldata tokenConfig
) external view returns (bytes[] memory calls) {
calls = new bytes[](3);
calls = new bytes[](2);

address fixedPriceMinterAddress = tokenConfig.fixedPriceMinter;
// build array of the calls to make
Expand All @@ -191,17 +269,6 @@ library PremintTokenSetup {
_buildNewSalesConfig(contractAdmin, tokenConfig.pricePerToken, tokenConfig.maxTokensPerAddress, tokenConfig.mintDuration)
)
);

// set the royalty config on that token:
calls[2] = abi.encodeWithSelector(
IZoraCreator1155.updateRoyaltiesForToken.selector,
newTokenId,
ICreatorRoyaltiesControl.RoyaltyConfiguration({
royaltyBPS: tokenConfig.royaltyBPS,
royaltyRecipient: tokenConfig.royaltyRecipient,
royaltyMintSchedule: tokenConfig.royaltyMintSchedule
})
);
}

function _buildNewSalesConfig(
Expand Down
Loading

0 comments on commit 6f3877a

Please sign in to comment.