Skip to content

Commit

Permalink
simple classic NFT launchpad
Browse files Browse the repository at this point in the history
  • Loading branch information
tempe-techie committed Mar 22, 2024
1 parent 826dc8c commit a4a125f
Show file tree
Hide file tree
Showing 3 changed files with 739 additions and 0 deletions.
196 changes: 196 additions & 0 deletions contracts/launchpad/erc721/IggyLaunchpad721Classic.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.17;

import { OwnableWithManagers } from "../../access/OwnableWithManagers.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./Nft721Classic.sol";

interface INftDirectory {
function addNftAddressToAllWithUniqueId(address _nftAddress, string calldata uniqueId_) external;
function isUniqueIdAvailable(string calldata _uniqueId) external view returns(bool);
}

interface INftMetadata {
function addImageToCollection(address nftAddress_, string memory imageUrl_) external;
function setCollectionPreview(address nftAddress_, string memory collectionPreview_) external;
function setDescription(address nftAddress_, string memory description_) external;
function setName(address nftAddress_, string memory name_) external;
}

interface IStatsContract {
function addWeiSpent(address user_, uint256 weiSpent_) external;
function addWriterByWriter(address writer_) external;
}

/**
@title Factory contract for launching new ERC721 collections with classic pricing (no bonding curve)
@author Tempe Techie
*/
contract IggyLaunchpad721Classic is OwnableWithManagers, ReentrancyGuard {
address public metadataAddress;
address public mintingFeeReceiver; // the address that receives the ETH paid for launching a new NFT contract & minting fees from NFT contracts
address public nftDirectoryAddress;
address public statsAddress; // usually the stats middleware address

bool public paused = false; // pause launching collections through the factory contract

uint256 public maxNftNameLength = 32;
uint256 public mintingFeePercentage; // in wei
uint256 public price; // price for creating new NFT contract
uint256 public referralFeePercentage = 10**17; // in wei (1 ether is 100%, default is 10**17 = 10%)

// EVENTS
event CollectionLaunch(address indexed contractOwner_, address indexed msgSender_, string name_, string uniqueId_, address indexed nftContract_);

// CONSTRUCTOR
constructor(
address _metadataAddress,
address _mintingFeeReceiver,
address _nftDirectoryAddress,
address _statsAddress,
uint256 _mintingFeePercentage,
uint256 _price
) {
require(_mintingFeePercentage < (5 * 1e16), "IggyLaunchpad721Classic: fee must be lower than 5%");

metadataAddress = _metadataAddress;
mintingFeeReceiver = _mintingFeeReceiver;
nftDirectoryAddress = _nftDirectoryAddress;
statsAddress = _statsAddress;
mintingFeePercentage = _mintingFeePercentage;
price = _price;
}

// WRITE

/// @notice Launch new ERC721 collection with classic pricing
function launch(
address contractOwner_,
address referrer_,
string memory mdDescription_,
string memory mdImage_,
string memory mdName_,
string memory name_,
string memory symbol_,
string calldata uniqueId_, // to easily find the NFT contract address
uint256 maxSupply_, // max NFT supply
uint256 mintPrice_ // NFT minting price
) external payable nonReentrant {
require(!paused, "Launching new collections is paused");
require(msg.value >= price, "Not enough ETH sent to cover price");

require(INftDirectory(nftDirectoryAddress).isUniqueIdAvailable(uniqueId_), "Unique ID is not available");
require(bytes(name_).length <= maxNftNameLength, "Name must be 32 characters or less");

uint256 paid = msg.value;

// send referral fee
if (referrer_ != address(0) && referralFeePercentage > 0) {
uint256 referralFee = msg.value * referralFeePercentage / 1 ether;
(bool sentRef, ) = referrer_.call{value: referralFee}("");

if (statsAddress != address(0)) {
IStatsContract(statsAddress).addWeiSpent(referrer_, referralFee);
}

paid -= referralFee;
}

(bool sent, ) = mintingFeeReceiver.call{value: address(this).balance}("");
require(sent, "Failed to send launch payment to the payment receiver");

// create new NFT contract
bytes32 saltedHash = keccak256(abi.encodePacked(msg.sender, block.timestamp, uniqueId_));
Nft721Classic nftContract = new Nft721Classic{salt: saltedHash}(
address(this),
metadataAddress,
mintingFeeReceiver,
name_,
symbol_,
maxSupply_,
mintPrice_
);

// update nftAddressById mapping and allNftContracts array
INftDirectory(nftDirectoryAddress).addNftAddressToAllWithUniqueId(address(nftContract), uniqueId_);

// update metadata contract
_updateMetadata(address(nftContract), mdDescription_, mdImage_, mdName_);

// update stats
_updateOwnerAndStats(contractOwner_, nftContract, uniqueId_, paid);
}

// INTERNAL
function _updateMetadata(address nftAddress_, string memory mdDescription_, string memory mdImage_, string memory mdName_) internal {
INftMetadata(metadataAddress).addImageToCollection(nftAddress_, mdImage_);
INftMetadata(metadataAddress).setCollectionPreview(nftAddress_, mdImage_);
INftMetadata(metadataAddress).setDescription(nftAddress_, mdDescription_);
INftMetadata(metadataAddress).setName(nftAddress_, mdName_);
}

function _updateOwnerAndStats(address contractOwner_, Nft721Classic nftContract_, string calldata uniqueId_, uint256 weiSpent_) internal {
nftContract_.transferOwnership(contractOwner_);

if (statsAddress != address(0)) {
IStatsContract(statsAddress).addWeiSpent(msg.sender, weiSpent_);
IStatsContract(statsAddress).addWriterByWriter(address(nftContract_));
}

emit CollectionLaunch(contractOwner_, msg.sender, nftContract_.name(), uniqueId_, address(nftContract_));
}

// OWNER

/// @notice Recover ETH sent to this contract
function recoverEth() external onlyManagerOrOwner {
(bool sent, ) = owner().call{value: address(this).balance}("");
require(sent, "Failed to send ETH to TLD owner");
}

/// @notice Set max NFT name length
function setMaxNftNameLength(uint256 _maxNftNameLength) external onlyManagerOrOwner {
maxNftNameLength = _maxNftNameLength;
}

/// @notice Set metadata contract address
function setMetadataAddress(address _metadataAddress) external onlyManagerOrOwner {
metadataAddress = _metadataAddress;
}

/// @notice Set royalty fee receiver
function setMintingFeeReceiver(address _mintingFeeReceiver) external onlyManagerOrOwner {
mintingFeeReceiver = _mintingFeeReceiver;
}

/// @notice Set royalty fee percentage in wei
function setMintingFeePercentage(uint256 _mintingFeePercentage) external onlyManagerOrOwner {
require(_mintingFeePercentage < (5 * 1e16), "IggyLaunchpad721Classic: fee must be lower than 5%");
mintingFeePercentage = _mintingFeePercentage;
}

/// @notice Set NFT directory contract address
function setNftDirectoryAddress(address _nftDirectoryAddress) external onlyManagerOrOwner {
nftDirectoryAddress = _nftDirectoryAddress;
}

/// @notice Set price for creating new NFT contract
function setPrice(uint256 _price) external onlyManagerOrOwner {
price = _price;
}

/// @notice Set referral fee percentage in wei (1 ether is 100%)
function setReferralFeePercentage(uint256 _referralFeePercentage) external onlyManagerOrOwner {
referralFeePercentage = _referralFeePercentage;
}

/// @notice Set stats contract address
function setStatsAddress(address _statsAddress) external onlyManagerOrOwner {
statsAddress = _statsAddress;
}

/// @notice Toggle pausing launching new collections
function togglePaused() external onlyManagerOrOwner {
paused = !paused;
}
}
130 changes: 130 additions & 0 deletions contracts/launchpad/erc721/Nft721Classic.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.17;

import { ERC721, ERC721Enumerable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import { OwnableWithManagers } from "../../access/OwnableWithManagers.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol";

interface IFactory {
function mintingFeePercentage() external view returns (uint256);
function owner() external view returns (address);
function statsAddress() external view returns (address);
}

interface INftMd {
function getCollectionPreviewImage(address nftAddress_) external view returns (string memory);
function getMetadata(address nftAddress_, uint256 tokenId_) external view returns (string memory);
}

interface IStats {
function addWeiSpent(address user_, uint256 weiSpent_) external;
}

/**
@title ERC-721 contract with classic pricing
@author Tempe Techie
*/
contract Nft721Classic is ERC721, ERC721Enumerable, OwnableWithManagers, ReentrancyGuard {
address public factoryAddress;
address public metadataAddress;
address public mintingFeeReceiver;

string public constant pricingType = "classic";

uint256 public counter = 1; // counter for the tokenId
uint256 public immutable createdAt; // NFT creation time
uint256 public immutable maxSupply; // max supply of the NFT collection
uint256 public mintPrice; // price for minting an NFT

// CONSTRUCTOR
constructor(
address factoryAddress_,
address metadataAddress_,
address mintingFeeReceiver_,
string memory name_,
string memory symbol_,
uint256 maxSupply_,
uint256 mintPrice_
) ERC721(name_, symbol_) {
factoryAddress = factoryAddress_;
metadataAddress = metadataAddress_;
mintingFeeReceiver = mintingFeeReceiver_;

maxSupply = maxSupply_;
mintPrice = mintPrice_;
createdAt = block.timestamp;
}

// READ PUBLIC

/// @notice Get collection preview image
function collectionPreview() public view returns (string memory) {
return INftMd(metadataAddress).getCollectionPreviewImage(address(this));
}

/// @dev This function is here because it also exists in the bonding NFT contract (for compatibility)
function getMintPrice() public view returns (uint256) {
return mintPrice;
}

function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
return super.supportsInterface(interfaceId);
}

function tokenURI(uint256 tokenId) public view override returns (string memory) {
_requireMinted(tokenId);

return INftMd(metadataAddress).getMetadata(address(this), tokenId);
}

// READ PRIVATE & INTERNAL

function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize) internal override(ERC721, ERC721Enumerable) {
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}

// WRITE

function mint(address to) external payable nonReentrant returns (uint256) {
require(counter <= maxSupply, "Nft721Classic: Max supply reached");
require(msg.value >= mintPrice, "Nft721Classic: Not enough ETH sent to cover price");

uint256 protocolFee = msg.value * IFactory(factoryAddress).mintingFeePercentage() / 1 ether;
uint256 ownerFee = msg.value - protocolFee;

// add addWeiSpent call to the stats contract
address statsAddress = IFactory(factoryAddress).statsAddress();
if (statsAddress != address(0)) {
IStats(statsAddress).addWeiSpent(msg.sender, protocolFee);
}

// send fees
(bool successOwner, ) = owner().call{value: ownerFee}("");

if (!successOwner) {
protocolFee += ownerFee;
}

(bool successMfReceiver, ) = mintingFeeReceiver.call{value: protocolFee}("");
require(successMfReceiver, "Nft721Classic: Failed to send protocol fee");

_mint(to, counter);
++counter;

return counter - 1; // return the tokenId of the minted NFT for the to address
}

// OWNER

// set metadata address
function setMetadataAddress(address metadataAddress_) external onlyManagerOrOwner {
metadataAddress = metadataAddress_;
}

// MINT FEE RECEIVER

function setMintingFeeReceiver(address mintingFeeReceiver_) external {
require(msg.sender == IFactory(factoryAddress).owner(), "Nft721Classic: Only factory owner can set a new mintingFeeReceiver");
mintingFeeReceiver = mintingFeeReceiver_;
}
}
Loading

0 comments on commit a4a125f

Please sign in to comment.