Skip to content

Commit

Permalink
ERC20 Minter (ourzora#235)
Browse files Browse the repository at this point in the history
* feat: erc20 minter

* feat: Premint with ERC-20

* refactor: revert with custom error

* chore: run lint

* refactor: revert invalid premint version

* refactor: emit firstMinter in PremintedV2 event

* refactor: remove previous local variables

* chore: add referral fetch tests

* chore: update header url

* chore: remove .vscode

* refactor: update erc20 slippage error name

* docs: update requestMint natspec

* refactor: rename address minter in EncodedPremintConfig

* refactor: update premint with erc20 slippage error

* chore: run lint

* feat: modifiable reward recipient address & support for deterministic deploy

* refactor: group constants

* docs: update storage vars natspec

* fix: update IERC20Minter natspec for events and errors

* fix: change erc-20 to erc20

* feat: rename zoraRewardAddress to zoraRewardRecipientAddress

* docs: update natspec

* chore: add ERC20 minter

* chore: add ERC20 minter deterministic config

* Update Subgraph With Deployed ERC20 Minter Addresses (ourzora#278)

* feat: add erc20Minter deployed addresses to configs

* fix: remove abi changes

* feat: add changeset

---------

Co-authored-by: Rohan Kulkarni <[email protected]>
  • Loading branch information
IsabellaSmallcombe and kulkarohan authored Mar 26, 2024
1 parent 079a596 commit 13a4785
Show file tree
Hide file tree
Showing 41 changed files with 2,272 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/clean-eagles-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zoralabs/zora-1155-contracts": patch
---

Adds first minter reward to ERC20 Minter
5 changes: 5 additions & 0 deletions .changeset/flat-carpets-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nft-creator-subgraph": minor
---

Adds ERC20Minter contract and events to the subgraph
5 changes: 5 additions & 0 deletions .changeset/neat-clocks-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zoralabs/zora-1155-contracts": minor
---

Adds ERC20 Minter contract which enables NFTs to be purchased with ERC20 tokens
5 changes: 5 additions & 0 deletions .changeset/yellow-ducks-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zoralabs/nft-creator-subgraph": minor
---

Adds ERC20Minter deployment addresses to the subgraph configs
3 changes: 0 additions & 3 deletions .vscode/settings.json

This file was deleted.

144 changes: 140 additions & 4 deletions packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ pragma solidity 0.8.17;

import {IMinter1155} from "../interfaces/IMinter1155.sol";
import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol";
import {IZoraCreator1155Errors} from "../interfaces/IZoraCreator1155Errors.sol";
import {IZoraCreator1155Errors} from "@zoralabs/shared-contracts/interfaces/errors/IZoraCreator1155Errors.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 {PremintEncoding} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol";
import {IERC20Minter, ERC20Minter} from "../minters/erc20/ERC20Minter.sol";
import {IERC1271} from "../interfaces/IERC1271.sol";

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

library ZoraCreator1155Attribution {
string internal constant NAME = "Preminter";
Expand Down Expand Up @@ -116,6 +117,18 @@ library ZoraCreator1155Attribution {
);
}

bytes32 constant ATTRIBUTION_DOMAIN_ERC20_V1 =
keccak256(
"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) {
return
keccak256(
abi.encode(ATTRIBUTION_DOMAIN_ERC20_V1, _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)"
Expand Down Expand Up @@ -164,6 +177,31 @@ library ZoraCreator1155Attribution {
);
}

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

function _hashToken(Erc20TokenCreationConfigV1 memory tokenConfig) private pure returns (bytes32) {
return
keccak256(
abi.encode(
TOKEN_DOMAIN_ERC20_V1,
_stringHash(tokenConfig.tokenURI),
tokenConfig.maxSupply,
tokenConfig.royaltyBPS,
tokenConfig.payoutRecipient,
tokenConfig.createReferral,
tokenConfig.erc20Minter,
tokenConfig.mintStart,
tokenConfig.mintDuration,
tokenConfig.maxTokensPerAddress,
tokenConfig.currency,
tokenConfig.pricePerToken
)
);
}

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

function _stringHash(string memory value) private pure returns (bytes32) {
Expand Down Expand Up @@ -195,6 +233,21 @@ library ZoraCreator1155Attribution {
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) {
return
_buildCalls({
newTokenId: newTokenId,
erc20MinterAddress: tokenConfig.erc20Minter,
currency: tokenConfig.currency,
pricePerToken: tokenConfig.pricePerToken,
maxTokensPerAddress: tokenConfig.maxTokensPerAddress,
mintDuration: tokenConfig.mintDuration,
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) {
return
Expand Down Expand Up @@ -223,6 +276,38 @@ library PremintTokenSetup {
});
}

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,
Expand Down Expand Up @@ -261,6 +346,27 @@ library PremintTokenSetup {
);
}

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,
Expand Down Expand Up @@ -332,7 +438,7 @@ library DelegatedTokenCreation {
);

(params, tokenSetupActions) = _recoverDelegatedTokenSetup(premintConfig, newTokenId);
} else {
} else if (premintVersion == PremintEncoding.HASHED_VERSION_2) {
PremintConfigV2 memory premintConfig = abi.decode(premintConfigEncoded, (PremintConfigV2));

creatorAttribution = recoverCreatorAttribution(
Expand All @@ -344,6 +450,20 @@ library DelegatedTokenCreation {
);

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

creatorAttribution = recoverCreatorAttribution(
PremintEncoding.ERC20_VERSION_1,
ZoraCreator1155Attribution.hashPremint(premintConfig),
tokenContract,
signature,
premintSignerContract
);

(params, tokenSetupActions) = _recoverDelegatedTokenSetup(premintConfig, newTokenId);
} else {
revert IZoraCreator1155Errors.InvalidPremintVersion();
}
}

Expand All @@ -352,9 +472,10 @@ library DelegatedTokenCreation {
}

function _supportedPremintSignatureVersions() internal pure returns (string[] memory versions) {
versions = new string[](2);
versions = new string[](3);
versions[0] = PremintEncoding.VERSION_1;
versions[1] = PremintEncoding.VERSION_2;
versions[2] = PremintEncoding.ERC20_VERSION_1;
}

function recoverCreatorAttribution(
Expand All @@ -380,6 +501,21 @@ library DelegatedTokenCreation {
attribution.domainName = ZoraCreator1155Attribution.NAME;
}

function _recoverDelegatedTokenSetup(
Erc20PremintConfigV1 memory premintConfig,
uint256 nextTokenId
) private view returns (DelegatedTokenSetup memory params, bytes[] memory tokenSetupActions) {
validatePremint(premintConfig.tokenConfig.mintStart, premintConfig.deleted);

params.uid = premintConfig.uid;

tokenSetupActions = PremintTokenSetup.makeSetupNewTokenCalls({newTokenId: nextTokenId, tokenConfig: premintConfig.tokenConfig});

params.tokenURI = premintConfig.tokenConfig.tokenURI;
params.maxSupply = premintConfig.tokenConfig.maxSupply;
params.createReferral = premintConfig.tokenConfig.createReferral;
}

function _recoverDelegatedTokenSetup(
PremintConfigV2 memory premintConfig,
uint256 nextTokenId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {ERC1155DelegationStorageV1} from "../delegation/ERC1155DelegationStorage
import {ZoraCreator1155PremintExecutorImplLib} from "./ZoraCreator1155PremintExecutorImplLib.sol";
import {ZoraCreator1155Attribution, DelegatedTokenCreation} from "./ZoraCreator1155Attribution.sol";
import {PremintEncoding, EncodedPremintConfig} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol";
import {ContractCreationConfig, PremintConfig, PremintConfigV2, TokenCreationConfig, TokenCreationConfigV2, MintArguments, PremintResult} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {ContractCreationConfig, PremintConfig, PremintConfigV2, TokenCreationConfig, TokenCreationConfigV2, MintArguments, PremintResult, Erc20PremintConfigV1, Erc20TokenCreationConfigV1} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {ZoraCreator1155Attribution, DelegatedTokenCreation} from "./ZoraCreator1155Attribution.sol";
import {IZoraCreator1155PremintExecutor} from "../interfaces/IZoraCreator1155PremintExecutor.sol";
import {IZoraCreator1155DelegatedCreationLegacy, IHasSupportedPremintSignatureVersions} from "../interfaces/IZoraCreator1155DelegatedCreation.sol";
import {ZoraCreator1155FactoryImpl} from "../factory/ZoraCreator1155FactoryImpl.sol";
Expand Down Expand Up @@ -47,6 +48,56 @@ contract ZoraCreator1155PremintExecutorImpl is
__UUPSUpgradeable_init();
}

/// @notice Executes the creation of an 1155 contract, token, and/or ERC20 sale signed by a creator, and mints the first tokens to the executor of this transaction.
/// To mint the first token(s) of an ERC20 sale, the executor must approve this contract the quantity * price of the mint.
/// @dev For use with v3 of premint config, PremintConfig3, which supports ERC20 mints.
/// @param contractConfig Parameters for creating a new contract, if one doesn't exist yet. Used to resolve the deterministic contract address.
/// @param premintConfig Parameters for creating the token, and minting the initial x tokens to the executor.
/// @param signature Signature of the creator of the token, which must match the signer of the premint config, or have permission to create new tokens on the erc1155 contract if it's already been created
/// @param quantityToMint How many tokens to mint to the mintRecipient
/// @param mintArguments mint arguments specifying the token mint recipient, mint comment, and mint referral
/// @param signerContract If a smart wallet was used to create the premint, the address of that smart wallet. Otherwise, set to address(0)
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) {
result = ZoraCreator1155PremintExecutorImplLib.getOrCreateContractAndToken(
zora1155Factory,
contractConfig,
PremintEncoding.encodePremintErc20V1(premintConfig),
signature,
firstMinter,
signerContract
);

if (quantityToMint > 0) {
ZoraCreator1155PremintExecutorImplLib.performERC20Mint(
premintConfig.tokenConfig.erc20Minter,
premintConfig.tokenConfig.currency,
premintConfig.tokenConfig.pricePerToken,
quantityToMint,
result,
mintArguments
);
}

{
emit PremintedV2({
contractAddress: result.contractAddress,
tokenId: result.tokenId,
createdNewContract: result.createdNewContract,
uid: premintConfig.uid,
minter: firstMinter,
quantityMinted: quantityToMint
});
}
}

/// @notice Creates a new token on the given erc1155 contract on behalf of a creator, and mints x tokens to the executor of this transaction.
/// For use for EIP-1271 based signatures, where there is a signer contract.
/// If the erc1155 contract hasn't been created yet, it will be created with the given config within this same transaction.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {ContractCreationConfig, PremintConfig} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {ContractCreationConfig, PremintConfig, PremintResult, MintArguments} 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";
import {IZoraCreator1155Factory} from "../interfaces/IZoraCreator1155Factory.sol";
import {ICreatorRoyaltiesControl} from "../interfaces/ICreatorRoyaltiesControl.sol";
Expand All @@ -10,7 +12,7 @@ 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 {MintArguments, PremintResult} from "@zoralabs/shared-contracts/entities/Premint.sol";
import {IERC20Minter} from "../interfaces/IERC20Minter.sol";

interface ILegacyZoraCreator1155DelegatedMinter {
function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature, address sender) external returns (uint256 newTokenId);
Expand Down Expand Up @@ -124,6 +126,42 @@ library ZoraCreator1155PremintExecutorImplLib {
}
}

function performERC20Mint(
address erc20Minter,
address currency,
uint256 pricePerToken,
uint256 quantityToMint,
PremintResult memory premintResult,
MintArguments memory mintArguments
) internal {
if (quantityToMint != 0) {
address mintReferral = mintArguments.mintRewardsRecipients.length > 0 ? mintArguments.mintRewardsRecipients[0] : address(0);

uint256 totalValue = pricePerToken * quantityToMint;

uint256 beforeBalance = IERC20(currency).balanceOf(address(this));
IERC20(currency).transferFrom(msg.sender, address(this), totalValue);
uint256 afterBalance = IERC20(currency).balanceOf(address(this));

if ((beforeBalance + totalValue) != afterBalance) {
revert IERC20Minter.ERC20TransferSlippage();
}

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

IERC20Minter(erc20Minter).mint(
mintArguments.mintRecipient,
quantityToMint,
premintResult.contractAddress,
premintResult.tokenId,
totalValue,
currency,
mintReferral,
mintArguments.mintComment
);
}
}

function mintWithEth(
IZoraCreator1155 tokenContract,
address fixedPriceMinter,
Expand Down
Loading

0 comments on commit 13a4785

Please sign in to comment.