|
| 1 | +# WTF Solidity极简入门: ERC721专题:1. ERC721相关库 |
| 2 | + |
| 3 | +我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。 |
| 4 | + |
| 5 | +推特:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) |
| 6 | + |
| 7 | +社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) |
| 8 | + |
| 9 | +所有代码和教程开源在 github: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) |
| 10 | + |
| 11 | +在进阶内容之前,我决定做一个`ERC721`的专题,把之前的内容综合运用,帮助大家更好的复习基础知识,并且更深刻的理解`ERC721`合约。希望在学习完这个专题之后,每个人都能发行自己的`NFT` |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## ERC721合约概览 |
| 16 | + |
| 17 | +`ERC721`主合约一共引用了7个合约: |
| 18 | + |
| 19 | +```Solidity |
| 20 | +import "./Address.sol"; |
| 21 | +import "./Context.sol"; |
| 22 | +import "./Strings.sol"; |
| 23 | +import "./IERC721.sol"; |
| 24 | +import "./IERC721Receiver.sol"; |
| 25 | +import "./IERC721Metadata.sol"; |
| 26 | +import "./ERC165.sol"; |
| 27 | +``` |
| 28 | + |
| 29 | +他们分别是: |
| 30 | + |
| 31 | +* 3个库合约:`Address.sol`, `Context.sol`和 `Strings.sol` |
| 32 | +* 3个接口合约:`IERC721.sol`, `IERC721Receiver.sol`, `IERC721Metadata.sol` |
| 33 | +* 1个`EIP165`合约:`ERC165.sol` |
| 34 | +所以在讲`ERC721`的主合约之前,我们会花两讲在引用的库合约和接口合约上。 |
| 35 | + |
| 36 | +## ERC721相关库 |
| 37 | + |
| 38 | +### Address库 |
| 39 | + |
| 40 | +`Address`库是`Address`变量相关函数的合集,包括判断某地址是否为合约,更安全的function call。`ERC721`用到其中的`isContract()`: |
| 41 | + |
| 42 | +```Solidity |
| 43 | +function isContract(address account) internal view returns (bool) { |
| 44 | + return account.code.length > 0; |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +这个函数利用了非合约地址`account.code`的长度为0的特性,从而区分某个地址是否为合约地址。 |
| 49 | + |
| 50 | +ERC721主合约在`_checkOnERC721Received()`函数中调用了`isContract()`。 |
| 51 | + |
| 52 | +```Solidity |
| 53 | +function _checkOnERC721Received( |
| 54 | + address from, |
| 55 | + address to, |
| 56 | + uint256 tokenId, |
| 57 | + bytes memory _data |
| 58 | +) private returns (bool) { |
| 59 | + if (to.isContract()) { |
| 60 | + try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) { |
| 61 | + return retval == IERC721Receiver.onERC721Received.selector; |
| 62 | + } catch (bytes memory reason) { |
| 63 | + if (reason.length == 0) { |
| 64 | + revert("ERC721: transfer to non ERC721Receiver implementer"); |
| 65 | + } else { |
| 66 | + assembly { |
| 67 | + revert(add(32, reason), mload(reason)) |
| 68 | + } |
| 69 | + } |
| 70 | + } |
| 71 | + } else { |
| 72 | + return true; |
| 73 | + } |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +该函数的目的是在接收`ERC721`代币的时候判断该地址是否是合约地址;如果是合约地址,则继续检查是否实现了`IERC721Receiver`接口(`ERC721`的接收接口),防止有人误把代币转到了黑洞。 |
| 78 | + |
| 79 | +### Context库 |
| 80 | + |
| 81 | +`Context`库非常简单,封装了两个Solidity的`global`变量:`msg.sender`和`msg.data` |
| 82 | + |
| 83 | +```Solidity |
| 84 | +abstract contract Context { |
| 85 | + function _msgSender() internal view virtual returns (address) { |
| 86 | + return msg.sender; |
| 87 | + } |
| 88 | +
|
| 89 | + function _msgData() internal view virtual returns (bytes calldata) { |
| 90 | + return msg.data; |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +这两个函数只是单纯的返回`msg.sender`和`msg.data`。所以`Context`库就是为了用函数把`msg.sender`和`msg.data`关键词包装起来,应对Solidity未来某次升级换掉关键字的情况,没其他作用。 |
| 96 | + |
| 97 | +### Strings库 |
| 98 | + |
| 99 | +`Strings`库包含两个库函数:`toString()`和`toHexString()`。`toString()`把`uint256`直接转换成`string`,比如777变为”777”;而`toHexString()`把`uint256`先转换为`16进制`,再转换为`string`,比如170变为”0xaa”。`ERC721`调用了`toString()`函数: |
| 100 | + |
| 101 | +```Solidity |
| 102 | +function toString(uint256 value) internal pure returns (string memory) { |
| 103 | + if (value == 0) { |
| 104 | + return "0"; |
| 105 | + } |
| 106 | + uint256 temp = value; |
| 107 | + uint256 digits; |
| 108 | + while (temp != 0) { |
| 109 | + digits++; |
| 110 | + temp /= 10; |
| 111 | + } |
| 112 | + bytes memory buffer = new bytes(digits); |
| 113 | + while (value != 0) { |
| 114 | + digits -= 1; |
| 115 | + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); |
| 116 | + value /= 10; |
| 117 | + } |
| 118 | + return string(buffer); |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +这个函数先确定了传入的`uint256`参数是几位数,并存在digits变量中。然后用循环把每一位数字的`ASCII码`转换成`bytes1`,存在`buffer`中,最后把`buffer`转换成`string`返回。 |
| 123 | + |
| 124 | +`ERC721`主合约在`tokenURI()`函数中调用了`toString()`: |
| 125 | + |
| 126 | +```Solidity |
| 127 | +function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { |
| 128 | + require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); |
| 129 | +
|
| 130 | + string memory baseURI = _baseURI(); |
| 131 | + return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +这个函数把`baseURI`和指定的`tokenId`拼接到一起,返回`ERC721 metadata`的网址,你花几十个ETH买的的jpeg就是存在这个网址上的。 |
| 136 | + |
| 137 | +## 总结 |
| 138 | + |
| 139 | +这一讲是`ERC721`专题的第一讲,我们概览了`ERC721`的合约,并介绍了`ERC721`主合约调用的3个库合约`Address`,`Context`和`String`。 |
0 commit comments