我最近在重新学solidity,巩固一下细节,也写一个“Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
欢迎关注我的推特:@0xAA_Science
WTF技术社群discord,内有加微信群方法:链接
所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): github.com/AmazingAng/WTFSolidity
Loot
是以太坊链上的实验性NFT项目,发行于21年8月,共有8000个,前7777个免费mint
,后233个项目方预留,最高时地板价突破20 ETH
,目前稳定在1 ETH
左右。与充斥市场的图片NFT不同,Loot
是文字类NFT,所有元数据都保存在链上,保证了去中心化,没人能篡改。
它的内容比较简单,就是用文字描述了玩家的一套装备,包括武器、头盔、戒指共8类物品。“金指环”,“双子之剑”,“硬皮手套”,复古的名字让我回忆起小时候玩的《暗黑破坏神》。
Loot
是一个开放的生态,项目方希望有更多团队能加入到Loot
元宇宙的建设中。这一讲,我将介绍Loot
是怎么用智能合约生成的文字,又是怎么把它放上链的。
Loot
代码在etherscan上开源,地址:链接
主合约Loot
从1291行
开始,继承了ERC721Enumerable
,ReentrancyGuard
和Ownable
,这些合约都是oppenzepplin
标准库中的。
contract Loot is ERC721Enumerable, ReentrancyGuard, Ownable {
Loot
在状态变量中定义了11个String数组
用于生成装备描述的基本词组:
weapon
, chestArmor
, headArmor
, waistArmor
, footArmor
, handArmor
, necklaces
, rings
。拿weapon
举例,它的String数组
包含战锤
, 木棒
等内容: string[] private weapons = [
"Warhammer",
"Quarterstaff",
"Maul",
"Mace",
"Club",
"Katana",
"Falchion",
"Scimitar",
"Long Sword",
"Short Sword",
"Ghost Wand",
"Grave Wand",
"Bone Wand",
"Wand",
"Grimoire",
"Chronicle",
"Tome",
"Book"
];
suffixes
, namePrefixes
和nameSuffixes
。前缀后缀可以让装备看起来更牛逼,例如:龙的完美之冠
,鹰吼-华丽的巨人胸甲
。拿装备后缀举例,suffixes
包括: string[] private suffixes = [
"of Power",
"of Giants",
"of Titans",
"of Skill",
"of Perfection",
"of Brilliance",
"of Enlightenment",
"of Protection",
"of Anger",
"of Rage",
"of Fury",
"of Vitriol",
"of the Fox",
"of Detection",
"of Reflection",
"of the Twins"
];
装备列表和修饰词随机结合,就能生成出Loot
的文本NFT。
为了区分装备的稀有度,Loot
利用链上伪随机数生成函数random()
为装备描述文本提供随机性:
function random(string memory input) internal pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(input)));
}
random()
函数参数为input
字符串,它计算input
的哈希,再转换为uint256
,将不同的input
均匀的映射到不同的数字上。之后将得到数字映射到稀有度,就可以了,Loot
定义了pluck()
函数来做这一点。
function pluck(uint256 tokenId, string memory keyPrefix, string[] memory sourceArray) internal view returns (string memory) {
uint256 rand = random(string(abi.encodePacked(keyPrefix, toString(tokenId))));
string memory output = sourceArray[rand % sourceArray.length];
uint256 greatness = rand % 21;
if (greatness > 14) {
output = string(abi.encodePacked(output, " ", suffixes[rand % suffixes.length]));
}
if (greatness >= 19) {
string[2] memory name;
name[0] = namePrefixes[rand % namePrefixes.length];
name[1] = nameSuffixes[rand % nameSuffixes.length];
if (greatness == 19) {
output = string(abi.encodePacked('"', name[0], ' ', name[1], '" ', output));
} else {
output = string(abi.encodePacked('"', name[0], ' ', name[1], '" ', output, " +1"));
}
}
return output;
}
pluck()
函数的作用就是在给定tokenId
, keyPrefix
(装备部位)和sourceArray
(装备列表),来生成特定部位装备的描述。一个装备有33.3%
的概率拥有后缀,其中有9.5%
的概率拥有特殊名称
。因此,Loot
中每件装备有66.7%
为普通,23.8%
为稀有,9.5%
为史诗。
Loot
包含8个get()
函数来获取8个部位的装备,拿getWeapon()
举例,这个函数获得武器描述:它调用了pluck
函数,装备部位为"WEAPON"
,装备列表为状态变量weapons
。
function getWeapon(uint256 tokenId) public view returns (string memory) {
return pluck(tokenId, "WEAPON", weapons);
}
由于keyPrefix
和sourceArray
都是复用的,因此Loot
的装备描述
/稀有度完全由tokenId
决定:给定tokenId
,总会得到同一组装备。因此,Loot
没有"保存"所有装备描述。每次用户查询元数据的时候,合约会生成一份装备描述。这个方法非常创新非常聪明,显著减少了存储占用,让元数据上链成为可能。
我们看一下它的tokenURI()
函数:
function tokenURI(uint256 tokenId) override public view returns (string memory) {
string[17] memory parts;
parts[0] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family: serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="10" y="20" class="base">';
parts[1] = getWeapon(tokenId);
parts[2] = '</text><text x="10" y="40" class="base">';
parts[3] = getChest(tokenId);
parts[4] = '</text><text x="10" y="60" class="base">';
parts[5] = getHead(tokenId);
parts[6] = '</text><text x="10" y="80" class="base">';
parts[7] = getWaist(tokenId);
parts[8] = '</text><text x="10" y="100" class="base">';
parts[9] = getFoot(tokenId);
parts[10] = '</text><text x="10" y="120" class="base">';
parts[11] = getHand(tokenId);
parts[12] = '</text><text x="10" y="140" class="base">';
parts[13] = getNeck(tokenId);
parts[14] = '</text><text x="10" y="160" class="base">';
parts[15] = getRing(tokenId);
parts[16] = '</text></svg>';
string memory output = string(abi.encodePacked(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6], parts[7], parts[8]));
output = string(abi.encodePacked(output, parts[9], parts[10], parts[11], parts[12], parts[13], parts[14], parts[15], parts[16]));
string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "Bag #', toString(tokenId), '", "description": "Loot is randomized adventurer gear generated and stored on chain. Stats, images, and other functionality are intentionally omitted for others to interpret. Feel free to use Loot in any way you want.", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}'))));
output = string(abi.encodePacked('data:application/json;base64,', json));
return output;
}
一般pfp NFT的tokenURI()
函数是直接返回一个带有元数据json
的网址;而Loot
的则是直接返回一个json
。它定义了parts
变量,然后通过8个get()
函数拼接出含有装备描述的svg
文件,作为元数据的image
,方便展示。最后,它把name
,description
和image
一起打包成一个Base64
编码的json
,作为tokenURI()
查询的返回值。
下面是一个tokenURI()
的返回值例子:
data:application/json;base64,eyJuYW1lIjogIkJhZyAjNSIsICJkZXNjcmlwdGlvbiI6ICJMb290IGlzIHJhbmRvbWl6ZWQgYWR2ZW50dXJlciBnZWFyIGdlbmVyYXRlZCBhbmQgc3RvcmVkIG9uIGNoYWluLiBTdGF0cywgaW1hZ2VzLCBhbmQgb3RoZXIgZnVuY3Rpb25hbGl0eSBhcmUgaW50ZW50aW9uYWxseSBvbWl0dGVkIGZvciBvdGhlcnMgdG8gaW50ZXJwcmV0LiBGZWVsIGZyZWUgdG8gdXNlIExvb3QgaW4gYW55IHdheSB5b3Ugd2FudC4iLCAiaW1hZ2UiOiAiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhCeVpYTmxjblpsUVhOd1pXTjBVbUYwYVc4OUluaE5hVzVaVFdsdUlHMWxaWFFpSUhacFpYZENiM2c5SWpBZ01DQXpOVEFnTXpVd0lqNDhjM1I1YkdVK0xtSmhjMlVnZXlCbWFXeHNPaUIzYUdsMFpUc2dabTl1ZEMxbVlXMXBiSGs2SUhObGNtbG1PeUJtYjI1MExYTnBlbVU2SURFMGNIZzdJSDA4TDNOMGVXeGxQanh5WldOMElIZHBaSFJvUFNJeE1EQWxJaUJvWldsbmFIUTlJakV3TUNVaUlHWnBiR3c5SW1Kc1lXTnJJaUF2UGp4MFpYaDBJSGc5SWpFd0lpQjVQU0l5TUNJZ1kyeGhjM005SW1KaGMyVWlQazFoZFd3Z2IyWWdVbVZtYkdWamRHbHZiand2ZEdWNGRENDhkR1Y0ZENCNFBTSXhNQ0lnZVQwaU5EQWlJR05zWVhOelBTSmlZWE5sSWo1UWJHRjBaU0JOWVdsc1BDOTBaWGgwUGp4MFpYaDBJSGc5SWpFd0lpQjVQU0kyTUNJZ1kyeGhjM005SW1KaGMyVWlQa1J5WVdkdmJpZHpJRU55YjNkdUlHOW1JRkJsY21abFkzUnBiMjQ4TDNSbGVIUStQSFJsZUhRZ2VEMGlNVEFpSUhrOUlqZ3dJaUJqYkdGemN6MGlZbUZ6WlNJK1UyRnphRHd2ZEdWNGRENDhkR1Y0ZENCNFBTSXhNQ0lnZVQwaU1UQXdJaUJqYkdGemN6MGlZbUZ6WlNJK1NHOXNlU0JIY21WaGRtVnpQQzkwWlhoMFBqeDBaWGgwSUhnOUlqRXdJaUI1UFNJeE1qQWlJR05zWVhOelBTSmlZWE5sSWo1SVlYSmtJRXhsWVhSb1pYSWdSMnh2ZG1WelBDOTBaWGgwUGp4MFpYaDBJSGc5SWpFd0lpQjVQU0l4TkRBaUlHTnNZWE56UFNKaVlYTmxJajVRWlc1a1lXNTBQQzkwWlhoMFBqeDBaWGgwSUhnOUlqRXdJaUI1UFNJeE5qQWlJR05zWVhOelBTSmlZWE5sSWo1VWFYUmhibWwxYlNCU2FXNW5QQzkwWlhoMFBqd3ZjM1puUGc9PSJ9
把它复制到浏览器打开,可以直接获取Loot
的元数据,挺神奇的:
下面是Loot
生成的文字描述svg
图片的例子
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIG1lZXQiIHZpZXdCb3g9IjAgMCAzNTAgMzUwIj48c3R5bGU+LmJhc2UgeyBmaWxsOiB3aGl0ZTsgZm9udC1mYW1pbHk6IHNlcmlmOyBmb250LXNpemU6IDE0cHg7IH08L3N0eWxlPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9ImJsYWNrIiAvPjx0ZXh0IHg9IjEwIiB5PSIyMCIgY2xhc3M9ImJhc2UiPk1hdWwgb2YgUmVmbGVjdGlvbjwvdGV4dD48dGV4dCB4PSIxMCIgeT0iNDAiIGNsYXNzPSJiYXNlIj5QbGF0ZSBNYWlsPC90ZXh0Pjx0ZXh0IHg9IjEwIiB5PSI2MCIgY2xhc3M9ImJhc2UiPkRyYWdvbidzIENyb3duIG9mIFBlcmZlY3Rpb248L3RleHQ+PHRleHQgeD0iMTAiIHk9IjgwIiBjbGFzcz0iYmFzZSI+U2FzaDwvdGV4dD48dGV4dCB4PSIxMCIgeT0iMTAwIiBjbGFzcz0iYmFzZSI+SG9seSBHcmVhdmVzPC90ZXh0Pjx0ZXh0IHg9IjEwIiB5PSIxMjAiIGNsYXNzPSJiYXNlIj5IYXJkIExlYXRoZXIgR2xvdmVzPC90ZXh0Pjx0ZXh0IHg9IjEwIiB5PSIxNDAiIGNsYXNzPSJiYXNlIj5QZW5kYW50PC90ZXh0Pjx0ZXh0IHg9IjEwIiB5PSIxNjAiIGNsYXNzPSJiYXNlIj5UaXRhbml1bSBSaW5nPC90ZXh0Pjwvc3ZnPg==
把它复制到浏览器打开,得到下面的图片:
由于tokenId
对应的稀有度在mint
前已经决定,黑客可以写一个合约来mint
稀有的NFT。
具体方法:计算每个tokenURI
对应在pluck()
函数中greatness
,当greatness%21 >= 19
,装备必是史诗。优先加高gas
来mint
这类稀有的Loot NFT
。
Loot
是我知道的第一个将元数据全部上链的文字NFT
项目,非常有开创性和可拓展性。它并不是把元数据直接存到链上(太费链上存储空间),而是每次都通过智能合约重新生成一份元数据返回。如果大家想fork
这个项目,需要认真看下random()
, pluck()
和tokenURI()
这三个函数。