Skip to content

Commit

Permalink
Add presave functionality to SuperMinterV1_1
Browse files Browse the repository at this point in the history
  • Loading branch information
Vectorized committed Dec 5, 2023
1 parent 9df9a7a commit c7b1931
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 5 deletions.
132 changes: 128 additions & 4 deletions contracts/modules/SuperMinterV1_1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,23 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
")"
);

/**
* @dev For EIP-712 presave signature digest calculation.
*/
bytes32 public constant PRESAVE_TYPEHASH =
// prettier-ignore
keccak256(
"Presave("
"address edition,"
"uint8 tier,"
"uint8 scheduleNum,"
"address[] to,"
"uint32 signedQuantity,"
"uint32 signedClaimTicket,"
"uint32 signedDeadline"
")"
);

/**
* @dev For EIP-712 signature digest calculation.
*/
Expand All @@ -120,6 +137,11 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
*/
uint8 public constant VERIFY_SIGNATURE = 2;

/**
* @dev The Presave mint mode.
*/
uint8 public constant PRESAVE = 3;

/**
* @dev The denominator of all BPS calculations.
*/
Expand Down Expand Up @@ -240,6 +262,11 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
_validateSigner(c.signer);
c.merkleRoot = bytes32(0);
c.maxMintablePerAccount = type(uint32).max;
} else if (mode == PRESAVE) {
_validateSigner(c.signer);
c.merkleRoot = bytes32(0);
c.maxMintablePerAccount = type(uint32).max;
c.price = 0; // Presave mode doesn't have a price.
} else {
revert InvalidMode();
}
Expand Down Expand Up @@ -308,15 +335,13 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {

/* ------------------- CHECKS AND UPDATES ------------------- */

// Check if the mint is open.
if (LibOps.or(block.timestamp < d.startTime, block.timestamp > d.endTime))
revert MintNotOpen(block.timestamp, d.startTime, d.endTime);
if (_isPaused(d)) revert MintPaused(); // Check if the mint is not paused.
_requireMintOpen(d);

// Perform the sub workflows depending on the mint mode.
uint8 mode = d.mode;
if (mode == VERIFY_MERKLE) _verifyMerkle(d, p);
else if (mode == VERIFY_SIGNATURE) _verifyAndClaimSignature(d, p);
else if (mode == PRESAVE) revert InvalidMode();

_incrementMinted(mode, d, p);

Expand Down Expand Up @@ -395,6 +420,35 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
emit Minted(p.edition, p.tier, p.scheduleNum, p.to, l, p.attributionId);
}

/**
* @inheritdoc ISuperMinterV1_1
*/
function presave(Presave calldata p) public {
MintData storage d = _getMintData(LibOps.packId(p.edition, p.tier, p.scheduleNum));

/* ------------------- CHECKS AND UPDATES ------------------- */

_requireMintOpen(d);

if (d.mode != PRESAVE) revert InvalidMode();
_verifyAndClaimPresaveSignature(d, p);

_incrementPresaveMinted(d, p);

/* ------------------------- MINT --------------------------- */

unchecked {
ISoundEditionV2_1 edition = ISoundEditionV2_1(p.edition);

uint256 toLength = p.to.length;
for (uint256 i; i != toLength; ++i) {
edition.mint(p.tier, p.to[i], p.signedQuantity);
}
}

emit Presaved(p.edition, p.tier, p.scheduleNum, p.to, p.signedQuantity);
}

// Per edition mint parameter setters:
// -----------------------------------
// These functions can only be called by the owner or admin of the edition.
Expand All @@ -412,6 +466,8 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
MintData storage d = _getMintData(mintId);
// If the tier is GA and the `mode` is `VERIFY_SIGNATURE`, we'll use `gaPrice[platform]`.
if (tier == GA_TIER && d.mode != VERIFY_SIGNATURE) revert NotConfigurable();
// Presave mints will not have a price.
if (d.mode == PRESAVE) revert NotConfigurable();
d.price = price;
emit PriceSet(edition, tier, scheduleNum, price);
}
Expand Down Expand Up @@ -510,6 +566,8 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
if (tier == GA_TIER) revert NotConfigurable();
// Signature mints will have `type(uint32).max`.
if (d.mode == VERIFY_SIGNATURE) revert NotConfigurable();
// Presave mints will have `type(uint32).max`.
if (d.mode == PRESAVE) revert NotConfigurable();
_validateMaxMintablePerAccount(value);
d.maxMintablePerAccount = value;
emit MaxMintablePerAccountSet(edition, tier, scheduleNum, value);
Expand Down Expand Up @@ -692,6 +750,24 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
)));
}

/**
* @inheritdoc ISuperMinterV1_1
*/
function computePresaveDigest(Presave calldata p) public view returns (bytes32) {
// prettier-ignore
return
_hashTypedData(keccak256(abi.encode(
PRESAVE_TYPEHASH,
p.edition,
p.tier,
p.scheduleNum,
keccak256(abi.encodePacked(p.to)),
p.signedQuantity,
p.signedClaimTicket,
p.signedDeadline
)));
}

/**
* @inheritdoc ISuperMinterV1_1
*/
Expand Down Expand Up @@ -1040,6 +1116,39 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
}
}

/**
* @dev Increments the number minted in the mint and the number minted by the collector.
* @param d The mint data storage pointer.
* @param p The presave parameters.
*/
function _incrementPresaveMinted(MintData storage d, Presave calldata p) internal {
unchecked {
uint256 mintId = LibOps.packId(p.edition, p.tier, p.scheduleNum);
uint256 toLength = p.to.length;

// Increment the number minted in the mint.
uint256 n = uint256(d.minted) + toLength * uint256(p.signedQuantity); // The next `minted`.
if (n > d.maxMintable) revert ExceedsMintSupply();
d.minted = uint32(n);

// Increment the number minted by the collectors.
for (uint256 i; i != toLength; ++i) {
LibMap.Uint32Map storage m = _numberMinted[p.to[i]];
m.set(mintId, uint32(uint256(m.get(mintId)) + uint256(p.signedQuantity)));
}
}
}

/**
* @dev Requires that the mint is open and not paused.
* @param d The mint data storage pointer.
*/
function _requireMintOpen(MintData storage d) internal view {
if (LibOps.or(block.timestamp < d.startTime, block.timestamp > d.endTime))
revert MintNotOpen(block.timestamp, d.startTime, d.endTime);
if (_isPaused(d)) revert MintPaused(); // Check if the mint is not paused.
}

/**
* @dev Verify the signature, and mark the signed claim ticket as claimed.
* @param d The mint data storage pointer.
Expand All @@ -1055,6 +1164,21 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
if (!_claimsBitmaps[mintId].toggle(p.signedClaimTicket)) revert SignatureAlreadyUsed();
}

/**
* @dev Verify the presave signature, and mark the signed claim ticket as claimed.
* @param d The mint data storage pointer.
* @param p The presave parameters.
*/
function _verifyAndClaimPresaveSignature(MintData storage d, Presave calldata p) internal {
// Unlike regular signature mints, presave mints only used `signedQuantity`.
address signer = _effectiveSigner(d);
if (!SignatureCheckerLib.isValidSignatureNowCalldata(signer, computePresaveDigest(p), p.signature))
revert InvalidSignature();
if (block.timestamp > p.signedDeadline) revert SignatureExpired();
uint256 mintId = LibOps.packId(p.edition, p.tier, p.scheduleNum);
if (!_claimsBitmaps[mintId].toggle(p.signedClaimTicket)) revert SignatureAlreadyUsed();
}

/**
* @dev Verify the Merkle proof.
* @param d The mint data storage pointer.
Expand Down
59 changes: 58 additions & 1 deletion contracts/modules/interfaces/ISuperMinterV1_1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,28 @@ interface ISuperMinterV1_1 is IERC165 {
uint256 attributionId;
}

/**
* @dev A struct containing the arguments for presave.
*/
struct Presave {
// The mint ID.
address edition;
// The tier of the mint.
uint8 tier;
// The edition-tier schedule number.
uint8 scheduleNum;
// The addresses to mint to.
address[] to;
// The signed quantity.
uint32 signedQuantity;
// The signed claimed ticket. Used if `mode` is `VERIFY_SIGNATURE`.
uint32 signedClaimTicket;
// The expiry timestamp for the signature. Used if `mode` is `VERIFY_SIGNATURE`.
uint32 signedDeadline;
// The signature by the signer. Used if `mode` is `VERIFY_SIGNATURE`.
bytes signature;
}

/**
* @dev A struct containing the total prices and fees.
*/
Expand Down Expand Up @@ -219,7 +241,7 @@ interface ISuperMinterV1_1 is IERC165 {
bytes32 affiliateMerkleRoot;
// The Merkle root hash, required if `mode` is `VERIFY_MERKLE`.
bytes32 merkleRoot;
// The signer address, required if `mode` is `VERIFY_SIGNATURE`.
// The signer address, required if `mode` is `VERIFY_SIGNATURE` or `PRESAVE`.
// This value will be the platform signer instead if it is configured to be `address(1)`.
address signer;
// Whether the platform signer is being used instead
Expand Down Expand Up @@ -340,6 +362,16 @@ interface ISuperMinterV1_1 is IERC165 {
uint256 indexed attributionId
);

/**
* @dev Emitted when tokens are minted for presave.
* @param edition The address of the Sound Edition.
* @param tier The tier.
* @param scheduleNum The edition-tier schedule number.
* @param to The recipients of the tokens minted.
* @param signedQuantity The amount of tokens per address.
*/
event Presaved(address indexed edition, uint8 tier, uint8 scheduleNum, address[] to, uint32 signedQuantity);

/**
* @dev Emitted when the platform fee configuration for `tier` is updated.
* @param platform The platform address.
Expand Down Expand Up @@ -556,6 +588,12 @@ interface ISuperMinterV1_1 is IERC165 {
*/
function mintTo(MintTo calldata p) external payable;

/**
* @dev Performs a presave mint.
* @param p The presave parameters.
*/
function presave(Presave calldata p) external;

/**
* @dev Sets the price of the mint.
* @param edition The address of the Sound Edition.
Expand Down Expand Up @@ -757,6 +795,12 @@ interface ISuperMinterV1_1 is IERC165 {
*/
function MINT_TO_TYPEHASH() external pure returns (bytes32);

/**
* @dev The EIP-712 typehash for presave mints.
* @return The constant value.
*/
function PRESAVE_TYPEHASH() external pure returns (bytes32);

/**
* @dev The default mint mode.
* @return The constant value.
Expand All @@ -775,6 +819,12 @@ interface ISuperMinterV1_1 is IERC165 {
*/
function VERIFY_SIGNATURE() external pure returns (uint8);

/**
* @dev The mint mode for presave.
* @return The constant value.
*/
function PRESAVE() external pure returns (uint8);

/**
* @dev The denominator used in BPS fee calculations.
* @return The constant value.
Expand Down Expand Up @@ -833,6 +883,13 @@ interface ISuperMinterV1_1 is IERC165 {
*/
function computeMintToDigest(MintTo calldata p) external view returns (bytes32);

/**
* @dev Returns the EIP-712 digest of the mint-to data for presave mints.
* @param p The presave parameters.
* @return The computed value.
*/
function computePresaveDigest(Presave calldata p) external view returns (bytes32);

/**
* @dev Returns the total price and fees for the mint.
* @param edition The address of the Sound Edition.
Expand Down
59 changes: 59 additions & 0 deletions tests/modules/SuperMinterV1_1.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,56 @@ contract SuperMinterV1_1Tests is TestConfigV2_1 {
}
}

function test_presave(uint256) public {
(address signer, uint256 privateKey) = _randomSigner();

ISuperMinterV1_1.MintCreation memory c;
c.maxMintable = uint32(_bound(_random(), 1, 64));
c.platform = address(this);
c.edition = address(edition);
c.startTime = 0;
c.tier = uint8(_random() % 2);
c.endTime = uint32(block.timestamp + 1000);
c.maxMintablePerAccount = uint32(_random()); // Doesn't matter, will be auto set to max.
c.mode = sm.PRESAVE();
c.signer = signer;
assertEq(sm.createEditionMint(c), 0);

unchecked {
ISuperMinterV1_1.Presave memory p;
p.edition = address(edition);
p.tier = c.tier;
p.scheduleNum = 0;
p.to = new address[](_bound(_random(), 0, 8));
p.signedQuantity = uint32(_bound(_random(), 1, 8));
p.signedClaimTicket = uint32(_bound(_random(), 0, type(uint32).max));
p.signedDeadline = type(uint32).max;
for (uint256 i; i < p.to.length; ++i) {
p.to[i] = _randomNonZeroAddress();
}
LibSort.sort(p.to);
LibSort.uniquifySorted(p.to);
p.signature = _generatePresaveSignature(p, privateKey);

uint256 expectedMinted = p.signedQuantity * p.to.length;
if (expectedMinted > c.maxMintable) {
vm.expectRevert(ISuperMinterV1_1.ExceedsMintSupply.selector);
sm.presave(p);
return;
}

sm.presave(p);
assertEq(sm.mintInfo(address(edition), p.tier, p.scheduleNum).minted, expectedMinted);
for (uint256 i; i < p.to.length; ++i) {
assertEq(edition.balanceOf(p.to[i]), p.signedQuantity);
assertEq(sm.numberMinted(address(edition), p.tier, p.scheduleNum, p.to[i]), p.signedQuantity);
}

vm.expectRevert(ISuperMinterV1_1.SignatureAlreadyUsed.selector);
sm.presave(p);
}
}

function test_mintDefaultUpToMaxPerAccount() public {
ISuperMinterV1_1.MintCreation memory c;
c.maxMintable = type(uint32).max;
Expand Down Expand Up @@ -988,6 +1038,15 @@ contract SuperMinterV1_1Tests is TestConfigV2_1 {
signature = abi.encodePacked(r, s, v);
}

function _generatePresaveSignature(ISuperMinterV1_1.Presave memory p, uint256 privateKey)
internal
returns (bytes memory signature)
{
bytes32 digest = sm.computePresaveDigest(p);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
signature = abi.encodePacked(r, s, v);
}

function test_mintGA(uint256) public {
ISuperMinterV1_1.MintCreation memory c;
c.maxMintable = type(uint32).max;
Expand Down

0 comments on commit c7b1931

Please sign in to comment.