Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SuperMinterV1_1 et al. #296

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d0e97dc
Prep files
Vectorized Nov 17, 2023
814e6f2
Reinstall multicaller
Vectorized Nov 17, 2023
56ecca9
forge install: multicaller
Vectorized Nov 17, 2023
289f5f5
Add LibMulticaller.senderOrSigner support
Vectorized Nov 17, 2023
767e950
Tidy
Vectorized Nov 17, 2023
51a192b
freeMintIncentive -> cheapMintIncentive
Vectorized Nov 24, 2023
83f02ac
Add comment on the two types of affiliate fees
Vectorized Nov 24, 2023
00273e4
Remove first collector incentives
Vectorized Nov 25, 2023
e993498
Add more comments and use checked math in mintTo
Vectorized Dec 1, 2023
9df9a7a
Add finalAffiliateIncentive to Minted log. Change finalCheapMintFee -…
Vectorized Dec 1, 2023
9d7530b
Add platform airdrop functionality to SuperMinterV1_1 (#298)
Vectorized Dec 15, 2023
ab5d9c3
Create modern-shirts-try.md
vigneshka Dec 15, 2023
9dc3992
pin version for typechain
vigneshka Dec 15, 2023
8e48a58
tweak ci
vigneshka Dec 15, 2023
3e86b2b
more ci tweaks
vigneshka Dec 15, 2023
babe37c
add back foundry
vigneshka Dec 15, 2023
12c912f
Merge branch 'main' of github.com:soundxyz/sound-protocol into vector…
vigneshka Dec 15, 2023
78f8bd9
bump
vigneshka Dec 15, 2023
4abef49
Merge branch 'main' of github.com:soundxyz/sound-protocol into vector…
vigneshka Dec 15, 2023
44373d5
bump typechain
vigneshka Dec 15, 2023
815ce5b
Merge branch 'main' of github.com:soundxyz/sound-protocol into vector…
vigneshka Dec 15, 2023
df00147
Implement fee changes (#299)
Vectorized Dec 18, 2023
30e8987
Update modern-shirts-try.md
vigneshka Dec 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Prep files
  • Loading branch information
Vectorized committed Nov 17, 2023
commit d0e97dc563d1d18cfb18fa21ab0cbea0c2146c85
20 changes: 14 additions & 6 deletions contracts/core/SoundEditionV2_1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,10 @@ contract SoundEditionV2_1 is ISoundEditionV2_1, ERC721AQueryableUpgradeable, ERC
address to,
uint256 quantity
) external payable onlyRolesOrOwner(ADMIN_ROLE | MINTER_ROLE) returns (uint256 fromTokenId) {
fromTokenId = _beforeTieredMint(tier, quantity);
uint32 fromTierTokenIdIndex;
(fromTokenId, fromTierTokenIdIndex) = _beforeTieredMint(tier, quantity);
_batchMint(to, quantity);
emit Minted(tier, to, quantity, fromTokenId);
emit Minted(tier, to, quantity, fromTokenId, fromTierTokenIdIndex);
}

/**
Expand All @@ -232,16 +233,17 @@ contract SoundEditionV2_1 is ISoundEditionV2_1, ERC721AQueryableUpgradeable, ERC
address[] calldata to,
uint256 quantity
) external payable onlyRolesOrOwner(ADMIN_ROLE) returns (uint256 fromTokenId) {
uint32 fromTierTokenIdIndex;
unchecked {
// Multiplication overflow is not possible due to the max block gas limit.
// If `quantity` is too big (e.g. 2**64), the loop in `_batchMint` will run out of gas.
// If `to.length` is too big (e.g. 2**64), the airdrop mint loop will run out of gas.
fromTokenId = _beforeTieredMint(tier, to.length * quantity);
(fromTokenId, fromTierTokenIdIndex) = _beforeTieredMint(tier, to.length * quantity);
for (uint256 i; i != to.length; ++i) {
_batchMint(to[i], quantity);
}
}
emit Airdropped(tier, to, quantity, fromTokenId);
emit Airdropped(tier, to, quantity, fromTokenId, fromTierTokenIdIndex);
}

/**
Expand Down Expand Up @@ -604,7 +606,7 @@ contract SoundEditionV2_1 is ISoundEditionV2_1, ERC721AQueryableUpgradeable, ERC
unchecked {
uint256 l = stop - start;
uint256 n = tierMinted(tier);
if (LibOps.or(start >= stop, stop > n)) revert InvalidQueryRange();
if (LibOps.or(start > stop, stop > n)) revert InvalidQueryRange();
tokenIds = new uint256[](l);
LibMap.Uint32Map storage m = _tierTokenIds[tier];
for (uint256 i; i != l; ++i) {
Expand Down Expand Up @@ -910,8 +912,13 @@ contract SoundEditionV2_1 is ISoundEditionV2_1, ERC721AQueryableUpgradeable, ERC
* Reverts if there is insufficient supply.
* @param tier The tier.
* @param quantity The total number of tokens to mint.
* @return fromTokenId The first token ID minted.
* @return fromTierTokenIdIndex The first token index in the tier.
*/
function _beforeTieredMint(uint8 tier, uint256 quantity) internal returns (uint256 fromTokenId) {
function _beforeTieredMint(uint8 tier, uint256 quantity)
internal
returns (uint256 fromTokenId, uint32 fromTierTokenIdIndex)
{
unchecked {
if (quantity == 0) revert MintZeroQuantity();
fromTokenId = _nextTokenId();
Expand Down Expand Up @@ -941,6 +948,7 @@ contract SoundEditionV2_1 is ISoundEditionV2_1, ERC721AQueryableUpgradeable, ERC
m.set(minted + i, uint32(fromTokenId + i)); // Set the token IDs for the tier.
if (tier != 0) _tokenTiers.set(fromTokenId + i, tier); // Set the tier for the token ID.
}
fromTierTokenIdIndex = uint32(minted);
}
}

Expand Down
22 changes: 12 additions & 10 deletions contracts/core/interfaces/ISoundEditionV2_1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -230,21 +230,23 @@ interface ISoundEditionV2_1 is IERC721AUpgradeable, IERC2981Upgradeable {

/**
* @dev Emitted upon a mint.
* @param tier The tier.
* @param to The address to mint to.
* @param quantity The number of minted.
* @param fromTokenId The first token ID minted.
* @param tier The tier.
* @param to The address to mint to.
* @param quantity The number of minted.
* @param fromTokenId The first token ID minted.
* @param fromTierTokenIdIndex The first token index in the tier.
*/
event Minted(uint8 tier, address to, uint256 quantity, uint256 fromTokenId);
event Minted(uint8 tier, address to, uint256 quantity, uint256 fromTokenId, uint32 fromTierTokenIdIndex);

/**
* @dev Emitted upon an airdrop.
* @param tier The tier.
* @param to The recipients of the airdrop.
* @param quantity The number of tokens airdropped to each address in `to`.
* @param fromTokenId The first token ID minted to the first address in `to`.
* @param tier The tier.
* @param to The recipients of the airdrop.
* @param quantity The number of tokens airdropped to each address in `to`.
* @param fromTokenId The first token ID minted to the first address in `to`.
* @param fromTierTokenIdIndex The first token index in the tier.
*/
event Airdropped(uint8 tier, address[] to, uint256 quantity, uint256 fromTokenId);
event Airdropped(uint8 tier, address[] to, uint256 quantity, uint256 fromTokenId, uint32 fromTierTokenIdIndex);

/**
* @dev EIP-4906 event to signal marketplaces to refresh the metadata.
Expand Down
111 changes: 82 additions & 29 deletions contracts/modules/SuperMinterV1_1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.16;

import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol";
import { ISoundEditionV2_1 } from "@core/interfaces/ISoundEditionV2_1.sol";
import { ISoundEditionV2 } from "@core/interfaces/ISoundEditionV2.sol";
import { ISuperMinterV1_1 } from "@modules/interfaces/ISuperMinterV1_1.sol";
import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol";
import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
Expand Down Expand Up @@ -137,6 +137,7 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {

/**
* @dev The maximum platform per-mint flat fee.
* Also applies to the maximum per-mint flat fee.
*/
uint96 public constant MAX_PLATFORM_PER_MINT_FLAT_FEE = 0.1 ether;

Expand Down Expand Up @@ -185,6 +186,16 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
*/
mapping(address => uint256) public affiliateFeesAccrued;

/**
* @dev A mapping of `firstCollector` => `feesAccrued`.
*/
mapping(address => uint256) public firstCollectorFeesAccrued;

/**
* @dev The first collector for the edition.
*/
mapping(address => address) public firstCollector;

/**
* @dev A mapping of `platform` => `price`.
*/
Expand Down Expand Up @@ -319,45 +330,60 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {

_incrementMinted(mode, d, p);

/* ----------- AFFILIATE AND PLATFORM FEES LOGIC ------------ */
/* ----------------- COMPUTE AND ACCRUE FEES ---------------- */

TotalPriceAndFees memory f = _totalPriceAndFees(p.tier, d, p.quantity, p.signedPrice);

uint256 remaining; // The fee sent to the Sound Edition (i.e. artist fee).
bool affiliated;
MintedLogData memory l;

unchecked {
if (msg.value != f.total) revert WrongPayment(msg.value, f.total); // Require exact payment.

remaining = f.total - f.platformFee; // `platformFee <= total`;
platformFeesAccrued[d.platform] += f.platformFee; // Accrue the platform fee.
l.finalArtistFee = f.total - f.platformFee; // `platformFee <= total`;
l.finalPlatformFee = f.platformFee; // Initialize to the platform fee.
l.affiliate = p.to == p.affiliate ? address(0) : p.affiliate; // Yeah, we know it's left curved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol


if (affiliated = _isAffiliatedWithProof(d, p.affiliate, p.affiliateProof)) {
remaining -= f.affiliateFee; // `affiliateFee <= remaining`.
affiliateFeesAccrued[p.affiliate] += f.affiliateFee; // Accrue the affiliate fee.
/* --------------------- AFFILIATE FEES --------------------- */

if (l.affiliated = _isAffiliatedWithProof(d, l.affiliate, p.affiliateProof)) {
vigneshka marked this conversation as resolved.
Show resolved Hide resolved
l.finalArtistFee -= f.affiliateFee;
l.finalPlatformFee -= f.affiliateIncentive;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking through all the calculations and configuration checks to try to resolve if this is underflowable gets too complex to be realiably accurate. Can we remove unchecked or do explicit checks for values that are especially prone to error (values that can't be easily commented on why it isn't underflow/overflowable).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is also worth considering that any underflow condition breaks balance tracking and will inevitably cause issues.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left it as checked math.

I think a few hundred gas more is worth the better assurance.

Most of our mints are on L2 anyways.

l.finalAffiliateFee = f.affiliateFee + f.affiliateIncentive;
affiliateFeesAccrued[p.affiliate] += l.finalAffiliateFee;
} else {
// Proof may be invalid, revert to prevent unintended skipping of affiliate fee.
if (p.affiliate != address(0)) revert InvalidAffiliate();
f.affiliateFee = 0; // Set the affiliate fee to zero if not affiliated.
}

/* --------------------- FREE MINT FEES --------------------- */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be changed to artist price incentives

Comment:

The platform can elect to split some of it's fees with the artist as an incentive for low mint prices.
The finalPlatformFee is reduced and the finalArtistFee is increased by this incentive amount.


if (f.freeMintIncentive != 0 && f.unitPrice == 0) {
l.finalPlatformFee -= f.freeMintIncentive;
l.finalFreeMintFee = f.freeMintIncentive;
l.finalArtistFee += l.finalFreeMintFee;
}

/* ------------------ FIRST COLLECTOR FEES ------------------ */

if (firstCollector[p.edition] == address(0)) firstCollector[p.edition] = p.to;
Copy link
Contributor

@vigneshka vigneshka Nov 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this as most valuable for rewarding the first collector (in deployAndMint) bringing the contract on chain since they pay extra fees.

If the first minter isn't bringing the contract on chain and not paying any extra fees for that, it's a random reward for being first on superminter and a target for frontrunning. Lmk if you have other thoughts here

if (f.firstCollectorIncentive != 0) {
l.finalPlatformFee -= f.firstCollectorIncentive;
l.finalFirstCollectorFee = f.firstCollectorIncentive;
firstCollectorFeesAccrued[firstCollector[p.edition]] += l.finalFirstCollectorFee;
}

platformFeesAccrued[d.platform] += l.finalPlatformFee; // Accrue the platform fee.
}

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

ISoundEditionV2_1 edition = ISoundEditionV2_1(p.edition);
MintedLogData memory l;
ISoundEditionV2 edition = ISoundEditionV2(p.edition);
l.quantity = p.quantity;
l.fromTokenId = edition.mint{ value: remaining }(p.tier, p.to, p.quantity);
l.fromTokenId = edition.mint{ value: l.finalArtistFee }(p.tier, p.to, p.quantity);
l.allowlisted = p.allowlisted;
l.allowlistedQuantity = p.allowlistedQuantity;
l.signedClaimTicket = p.signedClaimTicket;
l.affiliate = p.affiliate;
l.affiliated = affiliated;
l.requiredEtherValue = f.total;
l.unitPrice = f.unitPrice;
l.platformFee = f.platformFee;
l.platformFlatFee = f.platformFlatFee;
l.affiliateFee = f.affiliateFee;

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

/**
* @inheritdoc ISuperMinterV1_1
*/
function withdrawForFirstCollector(address collector) public {
uint256 accrued = firstCollectorFeesAccrued[collector];
if (accrued != 0) {
firstCollectorFeesAccrued[collector] = 0;
SafeTransferLib.forceSafeTransferETH(collector, accrued);
emit FirstCollectorFeesWithdrawn(collector, accrued);
}
}

/**
* @inheritdoc ISuperMinterV1_1
*/
Expand Down Expand Up @@ -840,7 +878,11 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
* @inheritdoc IERC165
*/
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return LibOps.or(interfaceId == type(ISuperMinterV1_1).interfaceId, interfaceId == this.supportsInterface.selector);
return
LibOps.or(
interfaceId == type(ISuperMinterV1_1).interfaceId,
interfaceId == this.supportsInterface.selector
);
}

// =============================================================
Expand Down Expand Up @@ -923,13 +965,20 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
* @param c The platform fee configuration.
*/
function _validatePlatformFeeConfig(PlatformFeeConfig memory c) internal pure {
if (
LibOps.or(
c.perTxFlat > MAX_PLATFORM_PER_TX_FLAT_FEE,
c.perMintFlat > MAX_PLATFORM_PER_MINT_FLAT_FEE,
c.perMintBPS > MAX_PLATFORM_PER_MINT_FEE_BPS
)
) revert InvalidPlatformFeeConfig();
unchecked {
uint256 incentiveSum;
incentiveSum += uint256(c.affiliateIncentive);
incentiveSum += uint256(c.freeMintIncentive);
incentiveSum += uint256(c.firstCollectorIncentive);
if (
LibOps.or(
c.perTxFlat > MAX_PLATFORM_PER_TX_FLAT_FEE,
c.perMintFlat > MAX_PLATFORM_PER_MINT_FLAT_FEE,
c.perMintBPS > MAX_PLATFORM_PER_MINT_FEE_BPS,
incentiveSum > c.perMintFlat
)
) revert InvalidPlatformFeeConfig();
}
}

/**
Expand All @@ -955,8 +1004,8 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
override
returns (string memory name_, string memory version_)
{
name_ = "SuperMinterV1_1";
version_ = "1";
name_ = "SuperMinter";
version_ = "1_1";
}

// Minting:
Expand Down Expand Up @@ -1088,6 +1137,10 @@ contract SuperMinterV1_1 is ISuperMinterV1_1, EIP712 {
// Affiliate fee is to be deducted from the sub total.
// Will be conditionally set to zero during mint if not affiliated.
f.affiliateFee = LibOps.rawMulDiv(f.subTotal, d.affiliateFeeBPS, BPS_DENOMINATOR);
// Calculate the incentives. These may be redirected away from the `platformFee`.
f.affiliateIncentive = c.affiliateIncentive * uint256(quantity);
f.freeMintIncentive = c.freeMintIncentive * uint256(quantity);
f.firstCollectorIncentive = c.firstCollectorIncentive * uint256(quantity);
// The total is the final value which the minter has to pay. It includes all fees.
f.total = f.subTotal + f.platformFlatFee;
}
Expand Down
51 changes: 44 additions & 7 deletions contracts/modules/interfaces/ISuperMinterV1_1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,18 @@ interface ISuperMinterV1_1 is IERC165 {
// The platform per-transaction flat fees.
uint256 platformTxFlatFee;
// The total platform per-token flat fees.
// This includes all incentives.
uint256 platformMintFlatFee;
// The total platform per-token BPS fees.
uint256 platformMintBPSFee;
// The total affiliate fees.
// The affiliate fees (before any incentives).
uint256 affiliateFee;
// The incentive for the affiliate.
uint256 affiliateIncentive;
// The incentive for free mints, to be given to the artist.
uint256 freeMintIncentive;
// The incentive for the first collector.
uint256 firstCollectorIncentive;
}

/**
Expand All @@ -137,21 +144,38 @@ interface ISuperMinterV1_1 is IERC165 {
uint256 requiredEtherValue;
// The price per token.
uint256 unitPrice;
// The total platform fees.
uint256 platformFee;
// The total platform flat fees.
uint256 platformFlatFee;
// The total affiliate fees.
uint256 affiliateFee;
// The final artist fee.
uint256 finalArtistFee;
// The final platform fee.
uint256 finalPlatformFee;
// The total affiliate fee.
uint256 finalAffiliateFee;
// The final free mint fee.
uint256 finalFreeMintFee;
// The final first collector fee.
uint256 finalFirstCollectorFee;
}

/**
* @dev A struct to hold the fee configuration for a platform and a tier.
*/
struct PlatformFeeConfig {
// The amount of platform per-mint flat fee
// to give to the affiliate, if provided.
uint96 affiliateIncentive;
// The amount of platform per-mint flat fee
// to give to the artist, if the mint is free.
uint96 freeMintIncentive;
// The amount of platform per-mint flat fee
// to give to the first collector.
uint96 firstCollectorIncentive;
// The per-transaction flat fee.
uint96 perTxFlat;
// The per-token flat fee.
// This fee includes:
// - `affiliateIncentive`.
// - `freeMintIncentive`.
// - `firstCollectorIncentive`.
uint96 perMintFlat;
// The per-token fee BPS.
uint16 perMintBPS;
Expand Down Expand Up @@ -341,6 +365,13 @@ interface ISuperMinterV1_1 is IERC165 {
*/
event AffiliateFeesWithdrawn(address indexed affiliate, uint256 accrued);

/**
* @dev Emitted with first collector fees are withdrawn.
* @param collector The first collector.
* @param accrued The amount of Ether accrued and withdrawn.
*/
event FirstCollectorFeesWithdrawn(address indexed collector, uint256 accrued);

/**
* @dev Emitted when platform fees are withdrawn.
* @param platform The platform address.
Expand Down Expand Up @@ -683,6 +714,12 @@ interface ISuperMinterV1_1 is IERC165 {
*/
function withdrawForAffiliate(address affiliate) external;

/**
* @dev Withdraws all accrued fees of the first collector, to the first collector.
* @param collector The first collector.
*/
function withdrawForFirstCollector(address collector) external;

/**
* @dev Withdraws all accrued fees of the platform, to the their fee address.
* @param platform The platform address.
Expand Down
2 changes: 1 addition & 1 deletion tests/TestConfigV2_1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "./TestPlus.sol";
import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol";
import { ISoundEditionV2_1, SoundEditionV2_1 } from "@core/SoundEditionV2_1.sol";

contract TestConfigV2 is TestPlus {
contract TestConfigV2_1 is TestPlus {
uint256 internal _salt;

SoundCreatorV1 soundCreator;
Expand Down
Loading
Loading