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
  • Loading branch information
oveddan authored May 16, 2024
1 parent d9b8a71 commit 43a394a
Show file tree
Hide file tree
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
Expand Up @@ -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";
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
)
);
}
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 43a394a

Please sign in to comment.