深入分析NFT合约源码——以Surge Women为例

github源码地址:https://github.com/youngqqcn/mynft

  • Surge Women合约地址:0x0632aDCab8F12edD3b06F99Dc6078FE1FEDD32B0
  • 智能合约源码:surge.sol
  • tokenId: 1802
  • opensea链接:https://opensea.io/assets/0x0632adcab8f12edd3b06f99dc6078fe1fedd32b0/1802
  • token mint 交易链接:https://etherscan.io/tx/0xbede5e44cc631303a22d066cc269f989469742b5bb6d9a74185e146dab9211e4

问题1:NFT(non-fungible token),即非同质化代币,如何理解“非同质化”?在代码层面如何实现的?

答:fungible中文意思是“可互换的”,可互换的东西是没有特殊性的,如果是独一无二的东西则具有了“不可互换的”属性。例如,1元钱的硬币和1元钱的纸钞则可以互换,虽然在形态上不同,但是在作为货币的属性上本质相同,都是代表1元。

至于如何编码实现,前面说了non-fungible的东西必须具备“独一无二”的属性,在编程领域什么东西独一无二呢?
答案很简单,就是唯一的id,用一个整数即可,在solidity中uint256能够表示的整数完全够用。

问题2:NFT的图片(或者音频、视频等)是怎样和智能中tokenId一一对应起来的?

图片等资源文件放在IPFS,智能合约中只存储每个token对应的IPFS上的URI即可。每个NFT项目有一个目录,目录下可以放很多资源文件,在构造合约的时候将目录在IPFS上的URI设置为baseURI,那么每个token的资源文件在IPFS的URI就确定了。例如,某个NFT项目在IPFS上总目录的URI为ipfs://QmYVsw73haPgm9jK9BopsuKtzuxLANjYn75xeHLpht13D5,tokenId为1802的token在IPFS上的URI则为ipfs://QmYVsw73haPgm9jK9BopsuKtzuxLANjYn75xeHLpht13D5/1802

from web3 import Web3import json# https://etherscan.io/tx/0xbede5e44cc631303a22d066cc269f989469742b5bb6d9a74185e146dab9211e4# https://mainnet.infura.io/v3/8a264f274fd94de48eb290d35db030ab# contract address is 0x0632aDCab8F12edD3b06F99Dc6078FE1FEDD32B0 from web3 import Web3my_provider = Web3.HTTPProvider('https://mainnet.infura.io/v3/8a264f274fd94de48eb290d35db030ab')w3 = Web3(my_provider)def main():contract_address = '0x0632aDCab8F12edD3b06F99Dc6078FE1FEDD32B0'contract_abi = json.load(open('surge.abi', 'r'))# print(contract_abi)mycontract = w3.eth.contract(address=contract_address, abi=contract_abi)name = mycontract.functions.name().call()print(name)symbol = mycontract.functions.symbol().call()print(symbol)tokenURI = mycontract.functions.tokenURI(1802).call()print(tokenURI)passif __name__ == '__main__':main()

运行打印的结果是:

Surge Women PassportSURGEipfs://QmYVsw73haPgm9jK9BopsuKtzuxLANjYn75xeHLpht13D5/1802

但是从ipfs浏览器中的链接却是:ipfs/Qmaseu2BbetLjA6eU7mQ2THEkjdBum5wq1EfuLAY2AoiEA/1802.png

分析tokenURI函数的代码

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {if (!_exists(tokenId)) revert URIQueryForNonexistentToken();// baseURI是目录的URIstring memory baseURI = _baseURI();// 将目录的URI和tokenId拼接在一起就是token的URIreturn bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : '';}

而 _baseURI由 Surge合约重写了父合约的_baseURI函数。Surge合约在构造函数中设置了baseURI,也就是在构造合约时已经设置了baseURI

constructor(string memory _name,string memory _symbol,string memory _baseTokenURI,uint128 _price,address _receiver,uint256 _royalties) payable ERC721A(_name, _symbol) {setBaseURI(_baseTokenURI);setPrice(_price);setRoyalties(_receiver, _royalties);}

https://ipfs.io/ipfs/QmYVsw73haPgm9jK9BopsuKtzuxLANjYn75xeHLpht13D5

https://tth-ipfs.com/ipfs/QmYVsw73haPgm9jK9BopsuKtzuxLANjYn75xeHLpht13D5

presaleMint为什么要用到merkleProof?

项目方做了预售,对所有参加预售的地址构造了一棵merkle tree,并将merkle root填入智能合约,调用presale的地址必须在merkle tree中。

使用merkle tree可以隐藏了具体地址。

/// @notice Presale minting verifies callers address is in Merkle Root/// @param _amountOfTokens Amount of tokens to mint/// @param _merkleProof Hash of the callers address used to verify the location of that address in the Merkle Rootfunction presaleMint(uint256 _amountOfTokens, bytes32[] calldata _merkleProof)externalpayableverifyMaxPerUser(msg.sender, _amountOfTokens)verifyMaxSupply(_amountOfTokens)isEnoughEth(_amountOfTokens){require(status == SaleStatus.Presale, "Presale not active");bytes32 leaf = keccak256(abi.encodePacked(msg.sender));require(MerkleProof.verify(_merkleProof, merkleRoot, leaf), "Not in presale list");_mintedAmount[msg.sender] += _amountOfTokens;_safeMint(msg.sender, _amountOfTokens);}

设置merkle root
https://etherscan.io/tx/0x4d6e0c07516115b8a803f77fe3067d52091c8d888eecb8f60fe897a68501ea27

/// @notice Set Presale Merkle Root/// @param _merkleRoot Merkle Root hashfunction setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {merkleRoot = _merkleRoot;}

presale

https://etherscan.io/tx/0x387dd09362758758b52d56dd2093724039fbd5592b13613cc347a2c1a216b581

同一个地址2次调用presale,那么它提供的merkle proof两次肯定是一样的。
https://etherscan.io/tx/0x5c76c3e78933ccc9f50e3a6f979226c02b9ab96ed320cbd68d4fbf3361c2b366
https://etherscan.io/tx/0xe64591ba680b9fb18f3bac61a20b7343801f03a9905d1f260df4d945089a056e