Skip to content

Commit

Permalink
feat: add mint fee (#102)
Browse files Browse the repository at this point in the history
* Add mint fee

* lint

* Update

* Add more tests

* fix

* fix
  • Loading branch information
channing-magiceden authored May 7, 2024
1 parent 7dd31f6 commit fc0d3e2
Show file tree
Hide file tree
Showing 9 changed files with 484 additions and 66 deletions.
55 changes: 41 additions & 14 deletions contracts/ERC721CM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
// Address of ERC-20 token used to pay for minting. If 0 address, use native currency.
address private _mintCurrency;

// Total mint fee
uint256 private _totalMintFee;

address public constant MINT_FEE_RECEIVER =
0x0B98151bEdeE73f9Ba5F2C7b72dEa02D38Ce49Fc;

constructor(
string memory collectionName,
string memory collectionSymbol,
Expand Down Expand Up @@ -160,7 +166,7 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
function setStages(MintStageInfo[] calldata newStages) external onlyOwner {
delete _mintStages;

for (uint256 i = 0; i < newStages.length;) {
for (uint256 i = 0; i < newStages.length; ) {
if (i >= 1) {
if (
newStages[i].startTimeUnixSeconds <
Expand All @@ -177,6 +183,7 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
_mintStages.push(
MintStageInfo({
price: newStages[i].price,
mintFee: newStages[i].mintFee,
walletLimit: newStages[i].walletLimit,
merkleRoot: newStages[i].merkleRoot,
maxStageSupply: newStages[i].maxStageSupply,
Expand All @@ -187,14 +194,17 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
emit UpdateStage(
i,
newStages[i].price,
newStages[i].mintFee,
newStages[i].walletLimit,
newStages[i].merkleRoot,
newStages[i].maxStageSupply,
newStages[i].startTimeUnixSeconds,
newStages[i].endTimeUnixSeconds
);

unchecked { ++i; }
unchecked {
++i;
}
}
}

Expand Down Expand Up @@ -376,8 +386,10 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
stage = _mintStages[activeStage];

// Check value if minting with ETH
if (_mintCurrency == address(0) && msg.value < stage.price * qty)
revert NotEnoughValue();
if (
_mintCurrency == address(0) &&
msg.value < (stage.price + stage.mintFee) * qty
) revert NotEnoughValue();

// Check stage supply if applicable
if (stage.maxStageSupply > 0) {
Expand Down Expand Up @@ -409,7 +421,10 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
) revert InvalidProof();

// Verify merkle proof mint limit
if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) {
if (
limit > 0 &&
_stageMintedCountsPerWallet[activeStage][to] + qty > limit
) {
revert WalletStageLimitExceeded();
}
}
Expand All @@ -418,10 +433,12 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
IERC20(_mintCurrency).safeTransferFrom(
msg.sender,
address(this),
stage.price * qty
(stage.price + stage.mintFee) * qty
);
}

_totalMintFee += stage.mintFee * qty;

_stageMintedCountsPerWallet[activeStage][to] += qty;
_stageMintedCounts[activeStage] += qty;
_safeMint(to, qty);
Expand All @@ -444,20 +461,28 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
* @dev Withdraws funds by owner.
*/
function withdraw() external onlyOwner {
uint256 value = address(this).balance;
(bool success, ) = msg.sender.call{value: value}("");
(bool success, ) = MINT_FEE_RECEIVER.call{value: _totalMintFee}("");
if (!success) revert TransferFailed();

uint256 remainingValue = address(this).balance;
(success, ) = msg.sender.call{value: remainingValue}("");
if (!success) revert WithdrawFailed();
emit Withdraw(value);

emit Withdraw(_totalMintFee + remainingValue);
}

/**
* @dev Withdraws ERC-20 funds by owner.
*/
function withdrawERC20() external onlyOwner {
if (_mintCurrency == address(0)) revert WrongMintCurrency();
uint256 value = IERC20(_mintCurrency).balanceOf(address(this));
IERC20(_mintCurrency).safeTransfer(msg.sender, value);
emit WithdrawERC20(_mintCurrency, value);

IERC20(_mintCurrency).safeTransfer(MINT_FEE_RECEIVER, _totalMintFee);

uint256 remaining = IERC20(_mintCurrency).balanceOf(address(this));
IERC20(_mintCurrency).safeTransfer(msg.sender, remaining);

emit WithdrawERC20(_mintCurrency, _totalMintFee + remaining);
}

/**
Expand Down Expand Up @@ -557,14 +582,16 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard {
function getActiveStageFromTimestamp(
uint64 timestamp
) public view returns (uint256) {
for (uint256 i = 0; i < _mintStages.length;) {
for (uint256 i = 0; i < _mintStages.length; ) {
if (
timestamp >= _mintStages[i].startTimeUnixSeconds &&
timestamp < _mintStages[i].endTimeUnixSeconds
) {
return i;
}
unchecked { ++i; }
unchecked {
++i;
}
}
revert InvalidStage();
}
Expand Down
49 changes: 35 additions & 14 deletions contracts/ERC721M.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
// Address of ERC-20 token used to pay for minting. If 0 address, use native currency.
address private _mintCurrency;

// Total mint fee
uint256 private _totalMintFee;

address public constant MINT_FEE_RECEIVER =
0x0B98151bEdeE73f9Ba5F2C7b72dEa02D38Ce49Fc;

constructor(
string memory collectionName,
string memory collectionSymbol,
Expand Down Expand Up @@ -157,10 +163,7 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
* ]
*/
function setStages(MintStageInfo[] calldata newStages) external onlyOwner {
uint256 originalSize = _mintStages.length;
for (uint256 i = 0; i < originalSize; i++) {
_mintStages.pop();
}
delete _mintStages;

for (uint256 i = 0; i < newStages.length; i++) {
if (i >= 1) {
Expand All @@ -179,6 +182,7 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
_mintStages.push(
MintStageInfo({
price: newStages[i].price,
mintFee: newStages[i].mintFee,
walletLimit: newStages[i].walletLimit,
merkleRoot: newStages[i].merkleRoot,
maxStageSupply: newStages[i].maxStageSupply,
Expand All @@ -189,6 +193,7 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
emit UpdateStage(
i,
newStages[i].price,
newStages[i].mintFee,
newStages[i].walletLimit,
newStages[i].merkleRoot,
newStages[i].maxStageSupply,
Expand Down Expand Up @@ -376,8 +381,10 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
stage = _mintStages[activeStage];

// Check value if minting with ETH
if (_mintCurrency == address(0) && msg.value < stage.price * qty)
revert NotEnoughValue();
if (
_mintCurrency == address(0) &&
msg.value < (stage.price + stage.mintFee) * qty
) revert NotEnoughValue();

// Check stage supply if applicable
if (stage.maxStageSupply > 0) {
Expand Down Expand Up @@ -409,19 +416,25 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
) revert InvalidProof();

// Verify merkle proof mint limit
if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) {
if (
limit > 0 &&
_stageMintedCountsPerWallet[activeStage][to] + qty > limit
) {
revert WalletStageLimitExceeded();
}
}

if (_mintCurrency != address(0)) {
// ERC20 mint payment
IERC20(_mintCurrency).safeTransferFrom(
msg.sender,
address(this),
stage.price * qty
(stage.price + stage.mintFee) * qty
);
}

_totalMintFee += stage.mintFee * qty;

_stageMintedCountsPerWallet[activeStage][to] += qty;
_stageMintedCounts[activeStage] += qty;
_safeMint(to, qty);
Expand All @@ -444,20 +457,28 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard {
* @dev Withdraws funds by owner.
*/
function withdraw() external onlyOwner {
uint256 value = address(this).balance;
(bool success, ) = msg.sender.call{value: value}("");
(bool success, ) = MINT_FEE_RECEIVER.call{value: _totalMintFee}("");
if (!success) revert TransferFailed();

uint256 remainingValue = address(this).balance;
(success, ) = msg.sender.call{value: remainingValue}("");
if (!success) revert WithdrawFailed();
emit Withdraw(value);

emit Withdraw(_totalMintFee + remainingValue);
}

/**
* @dev Withdraws ERC-20 funds by owner.
*/
function withdrawERC20() external onlyOwner {
if (_mintCurrency == address(0)) revert WrongMintCurrency();
uint256 value = IERC20(_mintCurrency).balanceOf(address(this));
IERC20(_mintCurrency).safeTransfer(msg.sender, value);
emit WithdrawERC20(_mintCurrency, value);

IERC20(_mintCurrency).safeTransfer(MINT_FEE_RECEIVER, _totalMintFee);

uint256 remaining = IERC20(_mintCurrency).balanceOf(address(this));
IERC20(_mintCurrency).safeTransfer(msg.sender, remaining);

emit WithdrawERC20(_mintCurrency, _totalMintFee + remaining);
}

/**
Expand Down
28 changes: 24 additions & 4 deletions contracts/IERC721M.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface IERC721M is IERC721AQueryable {
error Mintable();
error StageSupplyExceeded();
error TimestampExpired();
error TransferFailed();
error WalletGlobalLimitExceeded();
error WalletStageLimitExceeded();
error WithdrawFailed();
Expand All @@ -29,6 +30,7 @@ interface IERC721M is IERC721AQueryable {

struct MintStageInfo {
uint80 price;
uint80 mintFee;
uint32 walletLimit; // 0 for unlimited
bytes32 merkleRoot; // 0x0 for no presale enforced
uint24 maxStageSupply; // 0 for unlimited
Expand All @@ -39,6 +41,7 @@ interface IERC721M is IERC721AQueryable {
event UpdateStage(
uint256 stage,
uint80 price,
uint80 mintFee,
uint32 walletLimit,
bytes32 merkleRoot,
uint24 maxStageSupply,
Expand Down Expand Up @@ -69,10 +72,27 @@ interface IERC721M is IERC721AQueryable {
function getStageInfo(
uint256 index
) external view returns (MintStageInfo memory, uint32, uint256);

function mint(uint32 qty, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature) external payable;

function mintWithLimit(uint32 qty, uint32 limit, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature) external payable;
function mint(
uint32 qty,
bytes32[] calldata proof,
uint64 timestamp,
bytes calldata signature
) external payable;

function crossmint(uint32 qty, address to, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature) external payable;
function mintWithLimit(
uint32 qty,
uint32 limit,
bytes32[] calldata proof,
uint64 timestamp,
bytes calldata signature
) external payable;

function crossmint(
uint32 qty,
address to,
bytes32[] calldata proof,
uint64 timestamp,
bytes calldata signature
) external payable;
}
1 change: 0 additions & 1 deletion contracts/auctions/IBucketAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ interface IBucketAuction {
error NotClaimable();
error PriceHasBeenSet();
error PriceNotSet();
error TransferFailed();
error UserAlreadyClaimed();

struct User {
Expand Down
2 changes: 2 additions & 0 deletions scripts/setStages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ISetStagesParams {

interface StageConfig {
price: string;
mintFee?: string;
startDate: number;
endDate: number;
walletLimit?: number;
Expand Down Expand Up @@ -92,6 +93,7 @@ export const setStages = async (

const stages = stagesConfig.map((s, i) => ({
price: ethers.utils.parseEther(s.price),
mintFee: s.mintFee ? ethers.utils.parseEther(s.mintFee) : 0,
maxStageSupply: s.maxSupply ?? 0,
walletLimit: s.walletLimit ?? 0,
merkleRoot: merkleRoots[i],
Expand Down
2 changes: 2 additions & 0 deletions test/BucketAuction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe('BucketAuction', function () {
await ownerConn.setStages([
{
price: ethers.utils.parseEther('0.1'),
mintFee: 0,
walletLimit: 0,
merkleRoot: ethers.utils.hexZeroPad('0x0', 32),
maxStageSupply: 100,
Expand All @@ -46,6 +47,7 @@ describe('BucketAuction', function () {
},
{
price: ethers.utils.parseEther('0.2'),
mintFee: 0,
walletLimit: 0,
merkleRoot: ethers.utils.hexZeroPad('0x0', 32),
maxStageSupply: 100,
Expand Down
Loading

0 comments on commit fc0d3e2

Please sign in to comment.