使用opensea伪造NFT

https://app.pinata.cloud/pinmanager

使用openzepplin部署NFT合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts@4.5.0/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts@4.5.0/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts@4.5.0/access/Ownable.sol";
import "@openzeppelin/contracts@4.5.0/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts@4.5.0/utils/cryptography/ECDSA.sol";

contract WarriorERC721 is ERC721("Galxe Stargator Warrior NFTs", ""), ERC721Enumerable, Ownable, ReentrancyGuard {

    string private _baseURIextended;
    string private _baseURISuffix = "";
    bool public frozenMetadata = false;
    uint256 public MAX_TOKENS = 10000;
    bool public saleIsActive = false;
    bool public greenListSaleIsActive = false;
    mapping(address => uint256) addressToGreenListNumberMinted;
    mapping(address => uint256) addressToNumberMinted;
    address private bouncer = 0xd47257ee6cD0a80Ed1ba7bDdC722Ef8eE44a8d7d;

    bool private _baseUriIncludeTokenId = true;

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

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

    
    function flipSaleState() public onlyOwner {
            saleIsActive = !saleIsActive;
    }


    
    function flipGreenListSaleState() public onlyOwner {
            greenListSaleIsActive = !greenListSaleIsActive;
    }


    function setBaseURI(string memory baseURI_, bool includeTokenId, string memory suffix) external onlyOwner() {
        require(!frozenMetadata, "The metadata URI is frozen. You can no longer set the baseURI");
        _baseUriIncludeTokenId = includeTokenId;
        _baseURIextended = baseURI_;
        _baseURISuffix = suffix;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return _baseURIextended;
    }

    function freezeMetadata() external onlyOwner {
        frozenMetadata = true;
    }

    function showBaseURI() public view returns (string memory) {
        return _baseURIextended;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI)) : "";
        // if (_baseUriIncludeTokenId) {
        //     return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, Strings.toString(tokenId), _baseURISuffix)) : "";        
        // } else {
        //     return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI)) : "";
        // }
    }

    
    function mintToken(uint numberOfTokens) public payable nonReentrant {
        require(tx.origin == _msgSender(), "This function is only callable from an EOA.");
        require(saleIsActive, "Sale must be active to mint Tokens");
        require(numberOfTokens + addressToNumberMinted[_msgSender()] <= 10000, "Exceeded max token purchase");

        require(totalSupply() + numberOfTokens <= MAX_TOKENS, "Purchase would exceed max supply of tokens");
        require(0 * numberOfTokens <= msg.value, "Ether value sent is not correct");

        for (uint i = 0; i < numberOfTokens; i++) {
            uint mintIndex = totalSupply();
            if (totalSupply() < MAX_TOKENS) {
                addressToNumberMinted[_msgSender()] += 1;
                _safeMint(_msgSender(), mintIndex);
            }
        }
    }


    
    function greenListMintToken(uint numberOfTokens, bytes memory signature) public payable nonReentrant {
        require(tx.origin == _msgSender(), "This function is only callable from an EOA.");
        require(greenListSaleIsActive, "Sale must be active to mint Tokens");
        require(numberOfTokens + addressToGreenListNumberMinted[_msgSender()] <= 10000, "Exceeded max token purchase");
        require(totalSupply() + numberOfTokens <= MAX_TOKENS, "Purchase would exceed max supply of tokens");
        require(0 * numberOfTokens <= msg.value, "Ether value sent is not correct");

        bytes32 hashedMinter = ECDSA.toEthSignedMessageHash(keccak256(abi.encodePacked(_msgSender())));
        address recoveredBouncer = ECDSA.recover(hashedMinter, signature);
        require(recoveredBouncer == bouncer, "The signature for the greenlist is invalid");

        for (uint i = 0; i < numberOfTokens; i++) {
            uint mintIndex = totalSupply();
            if (totalSupply() < MAX_TOKENS) {
                addressToGreenListNumberMinted[_msgSender()] += 1;
                _safeMint(_msgSender(), mintIndex);
            }
        }
    }
    

    
    function reserveTokens(uint quantity, address recipient) public onlyOwner {
        uint supply = totalSupply();
        require(supply + quantity <= MAX_TOKENS, "We have already hit the reserve limit");
        uint i;
        for (i = 0; i < quantity; i++) {
            uint mintIndex = totalSupply();
            _safeMint(recipient, mintIndex);
        }
    }
}

这个应该是quix launchpad的部分代码。

使用pinata生成ipfs,这里注意一下现在穿一个json里面配置好资源信息(重要的就是名字和图片地址了),如果所有图片都一样,那就传一个就可以了。如果每个需要单独配,可以看下合约代码,里面有关于index的部分。

对于所有NFT图片都一样的,可以如合约里直接写死baseURI,并且可以预留一个接口用来修改baseURI。

我开始还有个想法,就是用一个合约通过设置baseURI达到复用效果,但是这样的话需要把所有mint过的id都设置一遍,这个gas太高了所以放弃。

在市场上发布有几个注意点:

1、NFT的名字是唯一的,所以可以尽量写和原系列相似的

2、quix上可以添加表情符号,比如这个符号✅,懂得都懂

3、NFT类别可以多选几个,容易被搜索到

4、非常适合官方盲盒刚mint但是还不能开并且名字图片都一样的(因为id不一样配置文件要多个),基本做到一模一样。

5、成本就是部署合约(上面合约应该可以再简化例如全部写成onlyowner然后判断啥的都去了)和mint。现在polygon最合适的链,成本非常低可以忽略而且有很多热度项目。

6、OS上有很严格的审查程序,我试着改了几个热门项目的名字,然后修改元数据图片,没多久就被封号了。所以机会可能还是在新出的项目(比如刚mint的)这种OS不可能会很快审查。其他平台暂时没发现这种