N合约分析
什么是LOOT?
在官网,只用了两行简短的英语对其进行介绍:
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.
Loot是随机生成的冒险者装备,并存储在区块链上。统计数字、图像和其他功能被有意省略,供他人解释。
请自由地以任何方式使用Loot。
通俗一点,Loot 是一种黑色背景,只包含文本的链上 NFT,任何人都可以参与铸造,将会随机获得一组奇幻冒险家装备,当然是以文本的形式,这些装备具有随机分布的稀缺特征。Loot是一个几乎空白的画布,却赋有巨大的吸引力来让人共同创作、建设和传播。
基于此,本文想对一个最简单的loot合约代码以及每个loot发行出来的价值进行分析,为学习loot提供参考。
代码地址:https://github.com/WeLightProject/tai-shang-nft-contracts/blob/feat/basic_n/N.sol
该loot合约发行的NFT是包含0-14的8行数字。下面来分析一下主要的函数功能:
1.random
传进一个string类型的参数,然后对其a bi编码,在对其keccak256哈希运算,最后转成int256返回。
function random(string memory input) internal pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(input)));
}
2.pluck
可以看到,获取每行的数字内容,关键是调用pluck函数,需要给他传进三个参数,tokenId, keyPrefix,sourceArray,一二两个参数可以来构造生成随机,这样可以确保每个NFT的8行数字里面没有相同的数字,同时保证生成的8888个NFT的唯一性,sourceArray则是为数字提供数据集,后面会对从数据集里面选出的数字output,下一步进行if匹配条件再进行相应的加工,例如+1,+2等,最后返回出去就是每行得到的最终数字
function getFirst(uint256 tokenId) public view returns (uint256) {
return pluck(tokenId, "FIRST", units);
}
function getSecond(uint256 tokenId) public view returns (uint256) {
return pluck(tokenId, "SECOND", units);
}
.......
function pluck(
uint256 tokenId,
string memory keyPrefix,
uint8[] memory sourceArray
) internal view returns (uint256) {
//传进tokenId和例如"FIRST"这样的字符,然后返回一个随机数
uint256 rand = random(string(abi.encodePacked(keyPrefix, toString(tokenId))));
//对rand % sourceArray.length取余,获取sourceArray里面的一个值
uint256 output = sourceArray[rand % sourceArray.length];
//对随机数进行取余
uint256 luck = rand % 21;
if (luck > 14) {
//output+1或output+2
output += suffixes[rand % suffixes.length];
}
if (luck >= 19) {
if (luck == 19) {
//(output*1或output*0)再+1或+2
output = (output * multipliers[rand % multipliers.length]) + suffixes[rand % suffixes.length];
} else {
//output*1或output*0
output = (output * multipliers[rand % multipliers.length]);
}
}
return output;
}
3.tokenURI
此函数的作用是返回一个将8个数字以一种黑底白字的svg格式的图片,主要是通过拼接字符串的方式
function tokenURI(uint256 tokenId) public view override returns (string memory) {
string[17] memory parts;
//svg图片格式的前缀
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">';
//数字1
parts[1] = toString(getFirst(tokenId));
parts[2] = '</text><text x="10" y="40" class="base">';
//数字2
parts[3] = toString(getSecond(tokenId));
parts[4] = '</text><text x="10" y="60" class="base">';
//数字3
parts[5] = toString(getThird(tokenId));
parts[6] = '</text><text x="10" y="80" class="base">';
//数字4
parts[7] = toString(getFourth(tokenId));
parts[8] = '</text><text x="10" y="100" class="base">';
//数字5
parts[9] = toString(getFifth(tokenId));
parts[10] = '</text><text x="10" y="120" class="base">';
//数字6
parts[11] = toString(getSixth(tokenId));
parts[12] = '</text><text x="10" y="140" class="base">';
//数字7
parts[13] = toString(getSeventh(tokenId));
parts[14] = '</text><text x="10" y="160" class="base">';
//数字8
parts[15] = toString(getEight(tokenId));
parts[16] = "</text></svg>";
//接下来就是对上面的各部分进行拼接,9个一组
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]
)
);
//拼接完毕,使用Base64编码库函数进行整体编码,方便传输
string memory json = Base64.encode(
bytes(
string(
abi.encodePacked(
'{"name": "N #',
toString(tokenId),
'", "description": "N is just numbers.", "image": "data:image/svg+xml;base64,',
//这里对于图片的内容单独进行了一次Base64编码
Base64.encode(bytes(output)),
'"}'
)
)
)
);
output = string(abi.encodePacked("data:application/json;base64,", json));
return output;
}
这里我们可以使用remix传入参数1调用一下看一下具体返回格式究竟是什么样子:
data:application/json;base64,eyJuYW1lIjogIk4gIzEiLCAiZGVzY3JpcHRpb24iOiAiTiBpcyBqdXN0IG51bWJlcnMuIiwgImltYWdlIjogImRhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEhOMlp5QjRiV3h1Y3owaWFIUjBjRG92TDNkM2R5NTNNeTV2Y21jdk1qQXdNQzl6ZG1jaUlIQnlaWE5sY25abFFYTndaV04wVW1GMGFXODlJbmhOYVc1WlRXbHVJRzFsWlhRaUlIWnBaWGRDYjNnOUlqQWdNQ0F6TlRBZ016VXdJajQ4YzNSNWJHVStMbUpoYzJVZ2V5Qm1hV3hzT2lCM2FHbDBaVHNnWm05dWRDMW1ZVzFwYkhrNklITmxjbWxtT3lCbWIyNTBMWE5wZW1VNklERTBjSGc3SUgwOEwzTjBlV3hsUGp4eVpXTjBJSGRwWkhSb1BTSXhNREFsSWlCb1pXbG5hSFE5SWpFd01DVWlJR1pwYkd3OUltSnNZV05ySWlBdlBqeDBaWGgwSUhnOUlqRXdJaUI1UFNJeU1DSWdZMnhoYzNNOUltSmhjMlVpUGpVOEwzUmxlSFErUEhSbGVIUWdlRDBpTVRBaUlIazlJalF3SWlCamJHRnpjejBpWW1GelpTSStORHd2ZEdWNGRENDhkR1Y0ZENCNFBTSXhNQ0lnZVQwaU5qQWlJR05zWVhOelBTSmlZWE5sSWo0M1BDOTBaWGgwUGp4MFpYaDBJSGc5SWpFd0lpQjVQU0k0TUNJZ1kyeGhjM005SW1KaGMyVWlQak04TDNSbGVIUStQSFJsZUhRZ2VEMGlNVEFpSUhrOUlqRXdNQ0lnWTJ4aGMzTTlJbUpoYzJVaVBqazhMM1JsZUhRK1BIUmxlSFFnZUQwaU1UQWlJSGs5SWpFeU1DSWdZMnhoYzNNOUltSmhjMlVpUGpnOEwzUmxlSFErUEhSbGVIUWdlRDBpTVRBaUlIazlJakUwTUNJZ1kyeGhjM005SW1KaGMyVWlQalk4TDNSbGVIUStQSFJsZUhRZ2VEMGlNVEFpSUhrOUlqRTJNQ0lnWTJ4aGMzTTlJbUpoYzJVaVBqTThMM1JsZUhRK1BDOXpkbWMrIn0=
我们通过在线网站进行解码:https://tool.ip138.com/base64
将data:application/json;base64,之后的内容复制进去,得到一次解码后的内容:
{"name": "N #1", "description": "N is just numbers.", "image": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIG1lZXQiIHZpZXdCb3g9IjAgMCAzNTAgMzUwIj48c3R5bGU+LmJhc2UgeyBmaWxsOiB3aGl0ZTsgZm9udC1mYW1pbHk6IHNlcmlmOyBmb250LXNpemU6IDE0cHg7IH08L3N0eWxlPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9ImJsYWNrIiAvPjx0ZXh0IHg9IjEwIiB5PSIyMCIgY2xhc3M9ImJhc2UiPjU8L3RleHQ+PHRleHQgeD0iMTAiIHk9IjQwIiBjbGFzcz0iYmFzZSI+NDwvdGV4dD48dGV4dCB4PSIxMCIgeT0iNjAiIGNsYXNzPSJiYXNlIj43PC90ZXh0Pjx0ZXh0IHg9IjEwIiB5PSI4MCIgY2xhc3M9ImJhc2UiPjM8L3RleHQ+PHRleHQgeD0iMTAiIHk9IjEwMCIgY2xhc3M9ImJhc2UiPjk8L3RleHQ+PHRleHQgeD0iMTAiIHk9IjEyMCIgY2xhc3M9ImJhc2UiPjg8L3RleHQ+PHRleHQgeD0iMTAiIHk9IjE0MCIgY2xhc3M9ImJhc2UiPjY8L3RleHQ+PHRleHQgeD0iMTAiIHk9IjE2MCIgY2xhc3M9ImJhc2UiPjM8L3RleHQ+PC9zdmc+"}
此时我们就可以看到真的内容,还有base64格式的图片,让我接着再一次解析data:image/svg+xml;base64,之后的内容:
<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">5</text><text x="10" y="40" class="base">4</text><text x="10" y="60" class="base">7</text><text x="10" y="80" class="base">3</text><text x="10" y="100" class="base">9</text><text x="10" y="120" class="base">8</text><text x="10" y="140" class="base">6</text><text x="10" y="160" class="base">3</text></svg>
最终我们得到了,最后s v g图片的代码,展示图如下:
4.claim
发行一个NFT,总量不超过8889个
function claim(uint256 tokenId) public nonReentrant {
require(tokenId > 0 && tokenId < 8889, "Token ID invalid");
_safeMint(_msgSender(), tokenId);
}
至于toString,还有Base64这里就不过多介绍了。
分析完合约,我们来对此合约生成的每个NFT进行分析
分析库代码地址:https://github.com/Anish-Agnihotri/dhof-loot
通过上面这个工具我们可以算出每个NFT的稀有度分数以及他的排名,然后通过一个脚本将json内容写进c s v文件中,如下部分截图:
所以说NFT的内容是根据其token ID确定的——这意味着在最初的NFT发行之前,只要通过阅读智能合约,任何人都可以轻而易举地提前计算出每个NFT稀有度以及排名。由于 claim() 函数将代币 ID 作为一个参数,所以很容易从收藏品中挑选出最稀有的物品,并赶在其他人之前立即将其铸造完成。因为信息的不对称,对于每个玩家来说是极为不公平的,也与loot项目的初衷背道而驰,希望未来可以解决这个问题,让每个NFT的珍稀度变得真正随机。
总结:Loot是充满想象力的,它像一个给了你画笔的画布,赋有巨大的吸引力来让人共同创作、建设和传播,但是由于每个NFT稀缺性的有规可循使得信息不对称,很容易破坏NFT的公平竞争环境。