Skip to content

Commit

Permalink
PremintV3 - erc20 premints with generalized minter (#379)
Browse files Browse the repository at this point in the history
* generealized miner

* deleted premint erc20
oveddan authored May 16, 2024
1 parent d9b8a71 commit 43a394a
Showing 12 changed files with 288 additions and 225 deletions.
106 changes: 106 additions & 0 deletions .changeset/cold-otters-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
"@zoralabs/zora-1155-contracts": minor
---

`ERC20PremintConfig` replaced by a more general purpose `PremintConfigV3`, which instead of having erc20 premint specific properties, as an abi encoded `premintSalesConfig`, that is passed to the function `setPremintSale` on the corresponding minter contract.

The new `TokenCreationConfigV3` looks like:

```solidity
struct TokenCreationConfigV3 {
// Metadata URI for the created token
string tokenURI;
// Max supply of the created token
uint256 maxSupply;
// RoyaltyBPS for created tokens. The royalty amount in basis points for secondary sales.
uint32 royaltyBPS;
// The address that the will receive rewards/funds/royalties.
address payoutRecipient;
// The address that referred the creation of the token.
address createReferral;
// The start time of the mint, 0 for immediate.
uint64 mintStart;
// The address of the minter module.
address minter;
// The abi encoded data to be passed to the minter to setup the sales config for the premint.
bytes premintSalesConfig;
}
```

where the `premintSalesConfig` is an abi encoded struct that is passed to the minter's function `setPremintSale`:

```solidity
ERC20Minter.PremintSalesConfig memory premintSalesConfig = ERC20Minter.PremintSalesConfig({
currency: address(mockErc20),
pricePerToken: 1e18,
maxTokensPerAddress: 5000,
duration: 1000,
payoutRecipient: collector
});
// this would be set as the property `premintSalesConfig` in the `TokenCreationConfigV3`
bytes memory encodedPremintSalesConfig = abi.encode(premintSalesConfig);
```

Correspondingly, new minters must implement the new interface `ISetPremintSale` to be compatible with the new `TokenCreationConfigV3`:

```solidity
interface ISetPremintSale {
function setPremintSale(uint256 tokenId, bytes calldata salesConfig) external;
}
// example implementation:
contract ERC20Minter is ISetPremintSale {
struct PremintSalesConfig {
address currency;
uint256 pricePerToken;
uint64 maxTokensPerAddress;
uint64 duration;
address payoutRecipient;
}
function buildSalesConfigForPremint(
PremintSalesConfig memory config
) public view returns (ERC20Minter.SalesConfig memory) {
uint64 saleStart = uint64(block.timestamp);
uint64 saleEnd = config.duration == 0
? type(uint64).max
: saleStart + config.duration;
return
IERC20Minter.SalesConfig({
saleStart: saleStart,
saleEnd: saleEnd,
maxTokensPerAddress: config.maxTokensPerAddress,
pricePerToken: config.pricePerToken,
fundsRecipient: config.payoutRecipient,
currency: config.currency
});
}
function toSaleConfig(
bytes calldata encodedPremintSalesConfig
) private returns (IERC20Minter.SalesConfig memory) {
PremintSalesConfig memory premintSalesConfig = abi.decode(
encodedPremintSalesConfig,
(PremintSalesConfig)
);
return buildSalesConfigForPremint(premintSalesConfig);
}
mapping(address => mapping(uint256 => IERC20Minter.SalesConfig)) public sale;
function setPremintSale(
uint256 tokenId,
bytes calldata premintSalesConfig
) external override {
IERC20Minter.SalesConfig memory salesConfig = toSaleConfig(
premintSalesConfig
);
sale[msg.sender][tokenId] = salesConfig;
}
}
```
145 changes: 40 additions & 105 deletions packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol
Original file line number Diff line number Diff line change
@@ -9,9 +9,11 @@ import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/con
import {ZoraCreatorFixedPriceSaleStrategy} from "../minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol";
import {PremintEncoding} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol";
import {IERC20Minter, ERC20Minter} from "../minters/erc20/ERC20Minter.sol";
import {IMinterPremintSetup} from "../interfaces/IMinterPremintSetup.sol";
import {IERC1271} from "../interfaces/IERC1271.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

import {PremintConfig, ContractCreationConfig, TokenCreationConfig, PremintConfigV2, TokenCreationConfigV2, Erc20TokenCreationConfigV1, Erc20PremintConfigV1} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {PremintConfig, ContractCreationConfig, TokenCreationConfig, PremintConfigV2, TokenCreationConfigV2, TokenCreationConfigV3, PremintConfigV3} from "@zoralabs/shared-contracts/entities/Premint.sol";

library ZoraCreator1155Attribution {
string internal constant NAME = "Preminter";
@@ -122,7 +124,7 @@ library ZoraCreator1155Attribution {
"CreatorAttribution(TokenCreationConfig tokenConfig,uint32 uid,uint32 version,bool deleted)TokenCreationConfig(string tokenURI,uint256 maxSupply,uint32 royaltyBPS,address payoutRecipient,address createReferral,address erc20Minter,uint64 mintStart,uint64 mintDuration,uint64 maxTokensPerAddress,address currency,uint256 pricePerToken)"
);

function hashPremint(Erc20PremintConfigV1 memory premintConfig) internal pure returns (bytes32) {
function hashPremint(PremintConfigV3 memory premintConfig) internal pure returns (bytes32) {
return
keccak256(
abi.encode(ATTRIBUTION_DOMAIN_ERC20_V1, _hashToken(premintConfig.tokenConfig), premintConfig.uid, premintConfig.version, premintConfig.deleted)
@@ -177,27 +179,24 @@ library ZoraCreator1155Attribution {
);
}

bytes32 constant TOKEN_DOMAIN_ERC20_V1 =
bytes32 constant TOKEN_DOMAIN_V3 =
keccak256(
"TokenCreationConfig(string tokenURI,uint256 maxSupply,uint32 royaltyBPS,address payoutRecipient,address createReferral,address erc20Minter,uint64 mintStart,uint64 mintDuration,uint64 maxTokensPerAddress,address currency,uint256 pricePerToken)"
"TokenCreationConfig(string tokenURI,uint256 maxSupply,uint32 royaltyBPS,address payoutRecipient,address createReferral,uint64 mintStart,address minter,bytes premintSalesConfig)"
);

function _hashToken(Erc20TokenCreationConfigV1 memory tokenConfig) private pure returns (bytes32) {
function _hashToken(TokenCreationConfigV3 memory tokenConfig) private pure returns (bytes32) {
return
keccak256(
abi.encode(
TOKEN_DOMAIN_ERC20_V1,
TOKEN_DOMAIN_V3,
_stringHash(tokenConfig.tokenURI),
tokenConfig.maxSupply,
tokenConfig.royaltyBPS,
tokenConfig.payoutRecipient,
tokenConfig.createReferral,
tokenConfig.erc20Minter,
tokenConfig.mintStart,
tokenConfig.mintDuration,
tokenConfig.maxTokensPerAddress,
tokenConfig.currency,
tokenConfig.pricePerToken
tokenConfig.minter,
keccak256(tokenConfig.premintSalesConfig)
)
);
}
@@ -215,139 +214,75 @@ library PremintTokenSetup {
uint256 constant PERMISSION_BIT_MINTER = 2 ** 2;

/// @notice Build token setup actions for a v3 preminted token
function makeSetupNewTokenCalls(uint256 newTokenId, Erc20TokenCreationConfigV1 memory tokenConfig) internal view returns (bytes[] memory calls) {
function makeSetupNewTokenCalls(uint256 newTokenId, TokenCreationConfigV3 memory tokenConfig) internal view returns (bytes[] memory calls) {
bytes memory setupMinterCall = abi.encodeWithSelector(IMinterPremintSetup.setPremintSale.selector, newTokenId, tokenConfig.premintSalesConfig);

return
_buildCalls({
newTokenId: newTokenId,
erc20MinterAddress: tokenConfig.erc20Minter,
currency: tokenConfig.currency,
pricePerToken: tokenConfig.pricePerToken,
maxTokensPerAddress: tokenConfig.maxTokensPerAddress,
mintDuration: tokenConfig.mintDuration,
minter: tokenConfig.minter,
setupMinterCall: setupMinterCall,
royaltyBPS: tokenConfig.royaltyBPS,
payoutRecipient: tokenConfig.payoutRecipient
});
}

/// @notice Build token setup actions for a v2 preminted token
function makeSetupNewTokenCalls(uint256 newTokenId, TokenCreationConfigV2 memory tokenConfig) internal view returns (bytes[] memory calls) {
bytes memory setupMinterCall = abi.encodeWithSelector(
ZoraCreatorFixedPriceSaleStrategy.setSale.selector,
newTokenId,
_buildNewSalesConfig(tokenConfig.pricePerToken, tokenConfig.maxTokensPerAddress, tokenConfig.mintDuration, tokenConfig.payoutRecipient)
);

return
_buildCalls({
newTokenId: newTokenId,
fixedPriceMinterAddress: tokenConfig.fixedPriceMinter,
pricePerToken: tokenConfig.pricePerToken,
maxTokensPerAddress: tokenConfig.maxTokensPerAddress,
mintDuration: tokenConfig.mintDuration,
minter: tokenConfig.fixedPriceMinter,
setupMinterCall: setupMinterCall,
royaltyBPS: tokenConfig.royaltyBPS,
payoutRecipient: tokenConfig.payoutRecipient
});
}

/// @notice Build token setup actions for a v1 preminted token
function makeSetupNewTokenCalls(uint256 newTokenId, TokenCreationConfig memory tokenConfig) internal view returns (bytes[] memory calls) {
bytes memory setupMinterCall = abi.encodeWithSelector(
ZoraCreatorFixedPriceSaleStrategy.setSale.selector,
newTokenId,
_buildNewSalesConfig(tokenConfig.pricePerToken, tokenConfig.maxTokensPerAddress, tokenConfig.mintDuration, tokenConfig.royaltyRecipient)
);

return
_buildCalls({
newTokenId: newTokenId,
fixedPriceMinterAddress: tokenConfig.fixedPriceMinter,
pricePerToken: tokenConfig.pricePerToken,
maxTokensPerAddress: tokenConfig.maxTokensPerAddress,
mintDuration: tokenConfig.mintDuration,
minter: tokenConfig.fixedPriceMinter,
setupMinterCall: setupMinterCall,
royaltyBPS: tokenConfig.royaltyBPS,
payoutRecipient: tokenConfig.royaltyRecipient
});
}

function _buildCalls(
uint256 newTokenId,
address erc20MinterAddress,
address currency,
uint256 pricePerToken,
uint64 maxTokensPerAddress,
uint64 mintDuration,
uint32 royaltyBPS,
address payoutRecipient
) private view returns (bytes[] memory calls) {
calls = new bytes[](3);

calls[0] = abi.encodeWithSelector(IZoraCreator1155.addPermission.selector, newTokenId, erc20MinterAddress, PERMISSION_BIT_MINTER);

calls[1] = abi.encodeWithSelector(
IZoraCreator1155.callSale.selector,
newTokenId,
IMinter1155(erc20MinterAddress),
abi.encodeWithSelector(
IERC20Minter.setSale.selector,
newTokenId,
_buildNewERC20SalesConfig(currency, pricePerToken, maxTokensPerAddress, mintDuration, payoutRecipient)
)
);

calls[2] = abi.encodeWithSelector(
IZoraCreator1155.updateRoyaltiesForToken.selector,
newTokenId,
ICreatorRoyaltiesControl.RoyaltyConfiguration({royaltyBPS: royaltyBPS, royaltyRecipient: payoutRecipient, royaltyMintSchedule: 0})
);
}

function _buildCalls(
uint256 newTokenId,
address fixedPriceMinterAddress,
uint96 pricePerToken,
uint64 maxTokensPerAddress,
uint64 mintDuration,
address minter,
bytes memory setupMinterCall,
uint32 royaltyBPS,
address payoutRecipient
) private view returns (bytes[] memory calls) {
calls = new bytes[](3);

// build array of the calls to make
// get setup actions and invoke them
// set up the sales strategy
// first, grant the fixed price sale strategy minting capabilities on the token
// tokenContract.addPermission(newTokenId, address(fixedPriceMinter), PERMISSION_BIT_MINTER);
calls[0] = abi.encodeWithSelector(IZoraCreator1155.addPermission.selector, newTokenId, fixedPriceMinterAddress, PERMISSION_BIT_MINTER);
calls[0] = abi.encodeWithSelector(IZoraCreator1155.addPermission.selector, newTokenId, minter, PERMISSION_BIT_MINTER);

// set the sales config on that token
calls[1] = abi.encodeWithSelector(
IZoraCreator1155.callSale.selector,
newTokenId,
IMinter1155(fixedPriceMinterAddress),
abi.encodeWithSelector(
ZoraCreatorFixedPriceSaleStrategy.setSale.selector,
newTokenId,
_buildNewSalesConfig(pricePerToken, maxTokensPerAddress, mintDuration, payoutRecipient)
)
);
calls[1] = abi.encodeWithSelector(IZoraCreator1155.callSale.selector, newTokenId, IMinter1155(minter), setupMinterCall);

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

function _buildNewERC20SalesConfig(
address currency,
uint256 pricePerToken,
uint64 maxTokensPerAddress,
uint64 duration,
address payoutRecipient
) private view returns (ERC20Minter.SalesConfig memory) {
uint64 saleStart = uint64(block.timestamp);
uint64 saleEnd = duration == 0 ? type(uint64).max : saleStart + duration;

return
IERC20Minter.SalesConfig({
saleStart: saleStart,
saleEnd: saleEnd,
maxTokensPerAddress: maxTokensPerAddress,
pricePerToken: pricePerToken,
fundsRecipient: payoutRecipient,
currency: currency
});
}

function _buildNewSalesConfig(
uint96 pricePerToken,
uint64 maxTokensPerAddress,
@@ -431,11 +366,11 @@ library DelegatedTokenCreation {
);

(params, tokenSetupActions) = _recoverDelegatedTokenSetup(premintConfig, newTokenId);
} else if (premintVersion == PremintEncoding.HASHED_ERC20_VERSION_1) {
Erc20PremintConfigV1 memory premintConfig = abi.decode(premintConfigEncoded, (Erc20PremintConfigV1));
} else if (premintVersion == PremintEncoding.HASHED_VERSION_3) {
PremintConfigV3 memory premintConfig = abi.decode(premintConfigEncoded, (PremintConfigV3));

creatorAttribution = recoverCreatorAttribution(
PremintEncoding.ERC20_VERSION_1,
PremintEncoding.VERSION_3,
ZoraCreator1155Attribution.hashPremint(premintConfig),
tokenContract,
signature,
@@ -456,7 +391,7 @@ library DelegatedTokenCreation {
versions = new string[](3);
versions[0] = PremintEncoding.VERSION_1;
versions[1] = PremintEncoding.VERSION_2;
versions[2] = PremintEncoding.ERC20_VERSION_1;
versions[2] = PremintEncoding.VERSION_3;
}

function recoverCreatorAttribution(
@@ -483,7 +418,7 @@ library DelegatedTokenCreation {
}

function _recoverDelegatedTokenSetup(
Erc20PremintConfigV1 memory premintConfig,
PremintConfigV3 memory premintConfig,
uint256 nextTokenId
) private view returns (DelegatedTokenSetup memory params, bytes[] memory tokenSetupActions) {
validatePremint(premintConfig.tokenConfig.mintStart, premintConfig.deleted);
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ import {ERC1155DelegationStorageV1} from "../delegation/ERC1155DelegationStorage
import {ZoraCreator1155PremintExecutorImplLib, GetOrCreateContractResult} from "./ZoraCreator1155PremintExecutorImplLib.sol";
import {ZoraCreator1155Attribution, DelegatedTokenCreation} from "./ZoraCreator1155Attribution.sol";
import {PremintEncoding, EncodedPremintConfig} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol";
import {ContractCreationConfig, ContractWithAdditionalAdminsCreationConfig, PremintConfig, PremintConfigV2, TokenCreationConfig, TokenCreationConfigV2, MintArguments, PremintResult, Erc20PremintConfigV1, Erc20TokenCreationConfigV1} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {ContractCreationConfig, ContractWithAdditionalAdminsCreationConfig, PremintConfig, PremintConfigV2, TokenCreationConfig, TokenCreationConfigV2, MintArguments, PremintResult, PremintConfigV3, TokenCreationConfigV3} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {IZoraCreator1155PremintExecutor} from "../interfaces/IZoraCreator1155PremintExecutor.sol";
import {IZoraCreator1155DelegatedCreationLegacy, IHasSupportedPremintSignatureVersions} from "../interfaces/IZoraCreator1155DelegatedCreation.sol";
import {ZoraCreator1155FactoryImpl} from "../factory/ZoraCreator1155FactoryImpl.sol";
@@ -170,9 +170,9 @@ contract ZoraCreator1155PremintExecutorImpl is
encodedPremintConfig.premintConfigVersion == PremintEncoding.HASHED_VERSION_2
) {
ZoraCreator1155PremintExecutorImplLib.mintWithEth(tokenContract, encodedPremintConfig.minter, tokenId, quantityToMint, mintArguments);
} else if (encodedPremintConfig.premintConfigVersion == PremintEncoding.HASHED_ERC20_VERSION_1) {
} else if (encodedPremintConfig.premintConfigVersion == PremintEncoding.HASHED_VERSION_3) {
ZoraCreator1155PremintExecutorImplLib.performERC20Mint(
encodedPremintConfig.premintConfig,
encodedPremintConfig.minter,
quantityToMint,
address(tokenContract),
tokenId,
@@ -192,28 +192,6 @@ contract ZoraCreator1155PremintExecutorImpl is
});
}

// @custom:deprecated use premintNewContract instead
function premintErc20V1(
ContractCreationConfig calldata contractConfig,
Erc20PremintConfigV1 calldata premintConfig,
bytes calldata signature,
uint256 quantityToMint,
MintArguments calldata mintArguments,
address firstMinter,
address signerContract
) external returns (PremintResult memory result) {
return
_premintNewContract(
_withEmptySetup(contractConfig),
PremintEncoding.encodePremintErc20V1(premintConfig),
signature,
quantityToMint,
mintArguments,
firstMinter,
signerContract
);
}

// @custom:deprecated use premintNewContract instead
function premintV2WithSignerContract(
ContractCreationConfig calldata contractConfig,
@@ -281,7 +259,7 @@ contract ZoraCreator1155PremintExecutorImpl is
revert OnlyForAbiDefinition();
}

function premintERC20V1Definition(Erc20PremintConfigV1 memory) external pure {
function premintV3Definition(PremintConfigV3 memory) external pure {
revert OnlyForAbiDefinition();
}

@@ -348,8 +326,8 @@ contract ZoraCreator1155PremintExecutorImpl is
}

/// @notice Checks if the signer of a premint is authorized to sign a premint for a given contract. If the contract hasn't been created yet,
/// then the signer is authorized if the signer's address matches contractConfig.contractAdmin. Otherwise, the signer must have the PERMISSION_BIT_MINTER
/// role on the contract
/// then the signer is authorized if the signer's address matches contractConfig.contractAdmin. Otherwise, the signer must be
/// in the list of additional admins
/// @param signer The signer of the premint
/// @param premintContractConfigContractAdmin If this contract was created via premint, the original contractConfig.contractAdmin. Otherwise, set to address(0)
/// @param contractAddress The determinstic 1155 contract address the premint is for
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {PremintConfig, ContractCreationConfig, ContractWithAdditionalAdminsCreationConfig, PremintResult, MintArguments, Erc20TokenCreationConfigV1, Erc20PremintConfigV1} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {PremintConfig, ContractCreationConfig, ContractWithAdditionalAdminsCreationConfig, PremintResult, MintArguments, TokenCreationConfigV3, PremintConfigV3} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Minter} from "../interfaces/IERC20Minter.sol";
import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol";
@@ -12,7 +12,6 @@ import {IZoraCreator1155PremintExecutor} from "../interfaces/IZoraCreator1155Pre
import {IZoraCreator1155DelegatedCreation, IZoraCreator1155DelegatedCreationLegacy, ISupportsAABasedDelegatedTokenCreation} from "../interfaces/IZoraCreator1155DelegatedCreation.sol";
import {EncodedPremintConfig} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol";
import {IMintWithRewardsRecipients} from "../interfaces/IMintWithRewardsRecipients.sol";
import {IERC20Minter} from "../interfaces/IERC20Minter.sol";

interface ILegacyZoraCreator1155DelegatedMinter {
function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature, address sender) external returns (uint256 newTokenId);
@@ -151,16 +150,10 @@ library ZoraCreator1155PremintExecutorImplLib {
}
}

function performERC20Mint(
bytes memory encodePremintConfig,
uint256 quantityToMint,
address contractAddress,
uint256 tokenId,
MintArguments memory mintArguments
) internal {
Erc20TokenCreationConfigV1 memory tokenConfig = abi.decode(encodePremintConfig, (Erc20PremintConfigV1)).tokenConfig;
function performERC20Mint(address minter, uint256 quantityToMint, address contractAddress, uint256 tokenId, MintArguments memory mintArguments) internal {
IERC20Minter.SalesConfig memory salesConfig = IERC20Minter(minter).sale(contractAddress, tokenId);

_performERC20Mint(tokenConfig.erc20Minter, tokenConfig.currency, tokenConfig.pricePerToken, quantityToMint, contractAddress, tokenId, mintArguments);
_performERC20Mint(minter, salesConfig.currency, salesConfig.pricePerToken, quantityToMint, contractAddress, tokenId, mintArguments);
}

function _performERC20Mint(
@@ -188,7 +181,7 @@ library ZoraCreator1155PremintExecutorImplLib {

IERC20(currency).approve(erc20Minter, totalValue);

IERC20Minter(erc20Minter).mint(
IERC20Minter(erc20Minter).mint{value: msg.value}(
mintArguments.mintRecipient,
quantityToMint,
contractAddress,
135 changes: 96 additions & 39 deletions packages/1155-contracts/test/premint/PremintERC20.t.sol
Original file line number Diff line number Diff line change
@@ -12,14 +12,18 @@ import {IMinter1155} from "../../src/interfaces/IMinter1155.sol";
import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol";
import {IZoraCreator1155PremintExecutor, ZoraCreator1155PremintExecutorImpl} from "../../src/delegation/ZoraCreator1155PremintExecutorImpl.sol";
import {ZoraCreator1155PremintExecutorImplLib} from "../../src/delegation/ZoraCreator1155PremintExecutorImplLib.sol";
import {ZoraCreator1155Attribution, ContractCreationConfig} from "../../src/delegation/ZoraCreator1155Attribution.sol";
import {Erc20TokenCreationConfigV1, Erc20PremintConfigV1, MintArguments} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {ZoraCreator1155Attribution} from "../../src/delegation/ZoraCreator1155Attribution.sol";
import {TokenCreationConfigV3, PremintConfigV3, MintArguments, ContractWithAdditionalAdminsCreationConfig} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {PremintEncoding} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol";
import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol";
import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol";
import {Zora1155PremintExecutor} from "../../src/proxies/Zora1155PremintExecutor.sol";
import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol";
import {Zora1155} from "../../src/proxies/Zora1155.sol";
import {IERC20Minter} from "../../src/interfaces/IERC20Minter.sol";
import {IMinterPremintSetup} from "../../src/interfaces/IMinterPremintSetup.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {IMinter1155} from "../../src/interfaces/IMinter1155.sol";

contract PremintERC20Test is Test {
uint256 internal creatorPK;
@@ -65,82 +69,135 @@ contract PremintERC20Test is Test {
}

function testPremintERC20() public {
// TODO: fix when we have a way to support payable erc20 mints
vm.skip(true);
ContractCreationConfig memory contractConfig = ContractCreationConfig({contractAdmin: creator, contractName: "test", contractURI: "test.uri"});
ContractWithAdditionalAdminsCreationConfig memory contractConfig = ContractWithAdditionalAdminsCreationConfig({
contractAdmin: creator,
contractName: "test",
contractURI: "test.uri",
additionalAdmins: new address[](0)
});

uint256 zerodTokenId = 0;

Erc20TokenCreationConfigV1 memory tokenConfig = Erc20TokenCreationConfigV1({
IERC20Minter.PremintSalesConfig memory premintSalesConfig = IERC20Minter.PremintSalesConfig({
currency: address(mockErc20),
pricePerToken: 1e18,
maxTokensPerAddress: 5000,
duration: 1000,
fundsRecipient: collector
});

TokenCreationConfigV3 memory tokenConfig = TokenCreationConfigV3({
tokenURI: "test.token.uri",
maxSupply: 1000,
royaltyBPS: 0,
payoutRecipient: collector,
createReferral: address(0),
erc20Minter: address(erc20Minter),
minter: address(erc20Minter),
mintStart: 0,
mintDuration: 0,
maxTokensPerAddress: 0,
currency: address(mockErc20),
pricePerToken: 1e18
premintSalesConfig: abi.encode(premintSalesConfig)
});

Erc20PremintConfigV1 memory premintConfig = Erc20PremintConfigV1({tokenConfig: tokenConfig, uid: 1, version: 3, deleted: false});
PremintConfigV3 memory premintConfig = PremintConfigV3({tokenConfig: tokenConfig, uid: 1, version: 3, deleted: false});

address contractAddress = premint.getContractAddress(contractConfig);
bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig);
bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, PremintEncoding.HASHED_ERC20_VERSION_1, block.chainid);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPK, digest);
bytes memory signature = abi.encodePacked(r, s, v);
address contractAddress = premint.getContractWithAdditionalAdminsAddress(contractConfig);
bytes memory signature = signPremint(premintConfig, contractAddress);

MintArguments memory mintArguments = MintArguments({mintRecipient: collector, mintComment: "test comment", mintRewardsRecipients: new address[](0)});

uint256 quantityToMint = 1;
uint256 totalValue = tokenConfig.pricePerToken * quantityToMint;

vm.deal(collector, ethReward);
uint256 quantityToMint = 3;
uint256 totalValue = premintSalesConfig.pricePerToken * quantityToMint;
mockErc20.mint(collector, totalValue);

vm.prank(collector);
mockErc20.approve(address(premint), totalValue);

uint256 totalEthReward = ethReward * quantityToMint;

vm.deal(collector, totalEthReward);
vm.prank(collector);
premint.premintErc20V1(contractConfig, premintConfig, signature, quantityToMint, mintArguments, collector, address(0));
// validate that the erc20 minter is called with the correct arguments
vm.expectCall(
address(erc20Minter),
totalEthReward,
abi.encodeCall(erc20Minter.mint, (collector, quantityToMint, contractAddress, 1, totalValue, address(mockErc20), address(0), "test comment"))
);
premint.premintNewContract{value: totalEthReward}(
contractConfig,
abi.encode(premintConfig),
PremintEncoding.VERSION_3,
signature,
quantityToMint,
mintArguments,
collector,
address(0)
);

// validate that the erc20 minter has the proper sales config set
IERC20Minter.SalesConfig memory salesConfig = erc20Minter.sale(contractAddress, 1);
assertEq(salesConfig.saleStart, uint64(block.timestamp));
assertEq(salesConfig.saleEnd, uint64(block.timestamp) + premintSalesConfig.duration);
}

function testRevertExecutorMustApproveERC20Transfer() public {
ContractCreationConfig memory contractConfig = ContractCreationConfig({contractAdmin: creator, contractName: "test", contractURI: "test.uri"});
ContractWithAdditionalAdminsCreationConfig memory contractConfig = ContractWithAdditionalAdminsCreationConfig({
contractAdmin: creator,
contractName: "test",
contractURI: "test.uri",
additionalAdmins: new address[](0)
});

IERC20Minter.PremintSalesConfig memory premintSalesConfig = IERC20Minter.PremintSalesConfig({
currency: address(mockErc20),
pricePerToken: 1e18,
maxTokensPerAddress: 0,
duration: 0,
fundsRecipient: collector
});

Erc20TokenCreationConfigV1 memory tokenConfig = Erc20TokenCreationConfigV1({
TokenCreationConfigV3 memory tokenConfig = TokenCreationConfigV3({
tokenURI: "test.token.uri",
maxSupply: 1000,
royaltyBPS: 0,
payoutRecipient: collector,
createReferral: address(0),
erc20Minter: address(erc20Minter),
minter: address(erc20Minter),
mintStart: 0,
mintDuration: 0,
maxTokensPerAddress: 0,
currency: address(mockErc20),
pricePerToken: 1e18
premintSalesConfig: abi.encode(premintSalesConfig)
});

Erc20PremintConfigV1 memory premintConfig = Erc20PremintConfigV1({tokenConfig: tokenConfig, uid: 1, version: 3, deleted: false});
PremintConfigV3 memory premintConfig = PremintConfigV3({tokenConfig: tokenConfig, uid: 1, version: 3, deleted: false});

address contractAddress = premint.getContractAddress(contractConfig);
bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig);
bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, PremintEncoding.HASHED_ERC20_VERSION_1, block.chainid);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPK, digest);
bytes memory signature = abi.encodePacked(r, s, v);
address contractAddress = premint.getContractWithAdditionalAdminsAddress(contractConfig);
bytes memory signature = signPremint(premintConfig, contractAddress);

MintArguments memory mintArguments = MintArguments({mintRecipient: collector, mintComment: "test comment", mintRewardsRecipients: new address[](0)});

uint256 quantityToMint = 1;
uint256 totalValue = tokenConfig.pricePerToken * quantityToMint;
uint256 totalValue = premintSalesConfig.pricePerToken * quantityToMint;
mockErc20.mint(collector, totalValue);

uint256 totalEthReward = ethReward * quantityToMint;

vm.deal(collector, totalEthReward);
vm.prank(collector);
vm.expectRevert("ERC20: insufficient allowance");
premint.premintErc20V1(contractConfig, premintConfig, signature, quantityToMint, mintArguments, collector, address(0));
premint.premintNewContract{value: totalEthReward}(
contractConfig,
abi.encode(premintConfig),
PremintEncoding.VERSION_3,
signature,
quantityToMint,
mintArguments,
collector,
address(0)
);
}

function signPremint(PremintConfigV3 memory premintConfig, address contractAddress) public view returns (bytes memory) {
bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig);
bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, PremintEncoding.HASHED_VERSION_3, block.chainid);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPK, digest);
return abi.encodePacked(r, s, v);
}
}
Original file line number Diff line number Diff line change
@@ -852,7 +852,7 @@ contract ZoraCreator1155PreminterTest is Test {
assertEq(supportedVersions.length, 3);
assertEq(supportedVersions[0], "1");
assertEq(supportedVersions[1], "2");
assertEq(supportedVersions[2], "ERC20_1");
assertEq(supportedVersions[2], "3");
}

function test_premintVersion_whenCreated_returnsAllVersion() external {
@@ -870,7 +870,7 @@ contract ZoraCreator1155PreminterTest is Test {
assertEq(supportedVersions.length, 3);
assertEq(supportedVersions[0], "1");
assertEq(supportedVersions[1], "2");
assertEq(supportedVersions[2], "ERC20_1");
assertEq(supportedVersions[2], "3");
}

function testPremintWithCreateReferral() public {
10 changes: 5 additions & 5 deletions packages/protocol-deployments/src/typedData.ts
Original file line number Diff line number Diff line change
@@ -10,9 +10,9 @@ import {
zoraCreator1155PremintExecutorImplABI,
} from "./generated/wagmi";
import {
Erc20PremintConfigV1,
PremintConfigV1,
PremintConfigV2,
PremintConfigV3,
PremintConfigVersion,
PremintConfigWithVersion,
} from "./types";
@@ -75,10 +75,10 @@ const encodePremintConfigV2 = (config: PremintConfigV2) => {
return encodeAbiParameters(abiItem.inputs, [config]);
};

const encodePremintConfigERC20V1 = (config: Erc20PremintConfigV1) => {
export const encodePremintConfigV3 = (config: PremintConfigV3) => {
const abiItem = getAbiItem({
abi: zoraCreator1155PremintExecutorImplABI,
name: "premintERC20V1Definition",
name: "premintV3Definition",
});

return encodeAbiParameters(abiItem.inputs, [config]);
@@ -94,8 +94,8 @@ export const encodePremintConfig = <T extends PremintConfigVersion>({
if (premintConfigVersion === PremintConfigVersion.V2) {
return encodePremintConfigV2(premintConfig as PremintConfigV2);
}
if (premintConfigVersion === PremintConfigVersion.ERC20V1) {
return encodePremintConfigERC20V1(premintConfig as Erc20PremintConfigV1);
if (premintConfigVersion === PremintConfigVersion.V3) {
return encodePremintConfigV3(premintConfig as PremintConfigV3);
}

throw new Error("Invalid PremintConfigVersion: " + premintConfigVersion);
16 changes: 8 additions & 8 deletions packages/protocol-deployments/src/types.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { zoraCreator1155PremintExecutorImplABI } from "./generated/wagmi";
export enum PremintConfigVersion {
V1 = "1",
V2 = "2",
ERC20V1 = "ERC20_1",
V3 = "3",
}

export type ContractCreationConfig = AbiParametersToPrimitiveTypes<
@@ -27,10 +27,10 @@ export type PremintConfigV2 = AbiParametersToPrimitiveTypes<
"premintV2Definition"
>["inputs"]
>[0];
export type Erc20PremintConfigV1 = AbiParametersToPrimitiveTypes<
export type PremintConfigV3 = AbiParametersToPrimitiveTypes<
ExtractAbiFunction<
typeof zoraCreator1155PremintExecutorImplABI,
"premintERC20V1Definition"
"premintV3Definition"
>["inputs"]
>[0];

@@ -46,7 +46,7 @@ export type PremintConfigForVersion<T extends PremintConfigVersion> =
? PremintConfigV1
: T extends PremintConfigVersion.V2
? PremintConfigV2
: Erc20PremintConfigV1;
: PremintConfigV3;

export type PremintConfigWithVersion<T extends PremintConfigVersion> = {
premintConfig: PremintConfigForVersion<T>;
@@ -55,25 +55,25 @@ export type PremintConfigWithVersion<T extends PremintConfigVersion> = {
export type PremintConfigAndVersion =
| PremintConfigWithVersion<PremintConfigVersion.V1>
| PremintConfigWithVersion<PremintConfigVersion.V2>
| PremintConfigWithVersion<PremintConfigVersion.ERC20V1>;
| PremintConfigWithVersion<PremintConfigVersion.V3>;

export type PremintConfig = PremintConfigV1 | PremintConfigV2;

export type TokenCreationConfigV1 = PremintConfigV1["tokenConfig"];
export type TokenCreationConfigV2 = PremintConfigV2["tokenConfig"];
export type Erc20TokenCreationConfigV1 = Erc20PremintConfigV1["tokenConfig"];
export type TokenCreationConfigV3 = PremintConfigV3["tokenConfig"];

export type TokenCreationConfig =
| TokenCreationConfigV1
| TokenCreationConfigV2
| Erc20TokenCreationConfigV1;
| TokenCreationConfigV3;

export type PremintConfigForTokenCreationConfig<T extends TokenCreationConfig> =
T extends TokenCreationConfigV1
? PremintConfigV1
: T extends TokenCreationConfigV2
? PremintConfigV2
: Erc20PremintConfigV1;
: PremintConfigV3;

export type TokenConfigForVersion<T extends PremintConfigVersion> =
PremintConfigForVersion<T>["tokenConfig"];
2 changes: 1 addition & 1 deletion packages/protocol-sdk/src/premint/contract-types.ts
Original file line number Diff line number Diff line change
@@ -3,5 +3,5 @@ import { PremintConfigVersion as PremintConfigVersionOrig } from "@zoralabs/prot
export enum PremintConfigVersion {
V1 = PremintConfigVersionOrig.V1,
V2 = PremintConfigVersionOrig.V2,
ERC20V1 = PremintConfigVersionOrig.ERC20V1,
V3 = PremintConfigVersionOrig.V3,
}
8 changes: 4 additions & 4 deletions packages/protocol-sdk/src/premint/premint-client.ts
Original file line number Diff line number Diff line change
@@ -97,8 +97,8 @@ const makeTokenConfigWithDefaults = <T extends PremintConfigVersion>({
tokenCreationConfig: Partial<TokenConfigForVersion<T>> & { tokenURI: string };
creatorAccount: Address;
}): TokenConfigForVersion<T> => {
if (premintConfigVersion === PremintConfigVersion.ERC20V1) {
throw new Error("ERC20V1 not supported in SDK");
if (premintConfigVersion === PremintConfigVersion.V3) {
throw new Error("PremintV3 not supported in SDK");
}
const fixedPriceMinter =
(tokenCreationConfig as TokenCreationConfigV1 | TokenCreationConfigV2)
@@ -536,8 +536,8 @@ class PremintClient {

const numberToMint = BigInt(mintArguments?.quantityToMint || 1);

if (premintConfigVersion === PremintConfigVersion.ERC20V1) {
throw new Error("ERC20 premint not supported in premint SDK");
if (premintConfigVersion === PremintConfigVersion.V3) {
throw new Error("PremintV3 not supported in premint SDK");
}

const value = (
20 changes: 7 additions & 13 deletions packages/shared-contracts/src/entities/Premint.sol
Original file line number Diff line number Diff line change
@@ -108,9 +108,9 @@ struct TokenCreationConfigV2 {
address createReferral;
}

struct Erc20PremintConfigV1 {
struct PremintConfigV3 {
// The config for the token to be created
Erc20TokenCreationConfigV1 tokenConfig;
TokenCreationConfigV3 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;
@@ -120,7 +120,7 @@ struct Erc20PremintConfigV1 {
bool deleted;
}

struct Erc20TokenCreationConfigV1 {
struct TokenCreationConfigV3 {
// Metadata URI for the created token
string tokenURI;
// Max supply of the created token
@@ -131,18 +131,12 @@ struct Erc20TokenCreationConfigV1 {
address payoutRecipient;
// The address that referred the creation of the token.
address createReferral;
// The address of the ERC20 minter module.
address erc20Minter;
// The start time of the mint, 0 for immediate.
uint64 mintStart;
// The duration of the mint, starting from the first mint of this token. 0 for infinite
uint64 mintDuration;
// Max tokens that can be minted for an address, 0 if unlimited
uint64 maxTokensPerAddress;
// The ERC20 currency address
address currency;
// Price per token in ERC20 currency
uint256 pricePerToken;
// The address of the minter module.
address minter;
// The abi encoded data to be passed to the minter to setup the sales config for the premint.
bytes premintSalesConfig;
}

struct MintArguments {
16 changes: 8 additions & 8 deletions packages/shared-contracts/src/premint/PremintEncoding.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {PremintConfig, PremintConfigV2, Erc20PremintConfigV1, PremintConfigCommon, TokenCreationConfig, TokenCreationConfigV2, Erc20TokenCreationConfigV1} from "../entities/Premint.sol";
import {PremintConfig, PremintConfigV2, PremintConfigV3, PremintConfigCommon, TokenCreationConfig, TokenCreationConfigV2, TokenCreationConfigV3} from "../entities/Premint.sol";
import {IMinter1155} from "../interfaces/IMinter1155.sol";
import {IZoraCreator1155Errors} from "../interfaces/errors/IZoraCreator1155Errors.sol";

@@ -17,8 +17,8 @@ library PremintEncoding {
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));
string internal constant ERC20_VERSION_1 = "ERC20_1";
bytes32 internal constant HASHED_ERC20_VERSION_1 = keccak256(bytes(ERC20_VERSION_1));
string internal constant VERSION_3 = "3";
bytes32 internal constant HASHED_VERSION_3 = keccak256(bytes(VERSION_3));

function encodePremintV1(PremintConfig memory premintConfig) internal pure returns (EncodedPremintConfig memory) {
return EncodedPremintConfig(abi.encode(premintConfig), HASHED_VERSION_1, premintConfig.uid, premintConfig.tokenConfig.fixedPriceMinter);
@@ -28,8 +28,8 @@ library PremintEncoding {
return EncodedPremintConfig(abi.encode(premintConfig), HASHED_VERSION_2, premintConfig.uid, premintConfig.tokenConfig.fixedPriceMinter);
}

function encodePremintErc20V1(Erc20PremintConfigV1 memory premintConfig) internal pure returns (EncodedPremintConfig memory) {
return EncodedPremintConfig(abi.encode(premintConfig), HASHED_ERC20_VERSION_1, premintConfig.uid, premintConfig.tokenConfig.erc20Minter);
function encodePremintErc20V1(PremintConfigV3 memory premintConfig) internal pure returns (EncodedPremintConfig memory) {
return EncodedPremintConfig(abi.encode(premintConfig), HASHED_VERSION_3, premintConfig.uid, premintConfig.tokenConfig.minter);
}

function encodePremintConfig(bytes memory encodedPremintConfig, string calldata premintConfigVersion) internal pure returns (EncodedPremintConfig memory) {
@@ -43,10 +43,10 @@ library PremintEncoding {
PremintConfigV2 memory config = abi.decode(encodedPremintConfig, (PremintConfigV2));

return EncodedPremintConfig(encodedPremintConfig, hashedVersion, config.uid, config.tokenConfig.fixedPriceMinter);
} else if (hashedVersion == HASHED_ERC20_VERSION_1) {
Erc20PremintConfigV1 memory config = abi.decode(encodedPremintConfig, (Erc20PremintConfigV1));
} else if (hashedVersion == HASHED_VERSION_3) {
PremintConfigV3 memory config = abi.decode(encodedPremintConfig, (PremintConfigV3));

return EncodedPremintConfig(encodedPremintConfig, hashedVersion, config.uid, config.tokenConfig.erc20Minter);
return EncodedPremintConfig(encodedPremintConfig, hashedVersion, config.uid, config.tokenConfig.minter);
} else {
revert IZoraCreator1155Errors.InvalidSignatureVersion();
}

0 comments on commit 43a394a

Please sign in to comment.