TRC-721 合约示例
包含元数据扩展和枚举扩展的 TRC-721 标准完整参考实现。
前置阅读
本页包含一个单文件完整 TRC-721 参考实现。使用时,在最终合约中自定义 Token 名称和符号,然后部署即可。
合约按层次组织——工具在顶部,TRC-721 核心居中,最终可部署合约在底部。
文件内容说明
| 层次 | 合约与库 | 用途 |
|---|---|---|
| 工具库 | SafeMath、Address、Counters、Roles、Context | 安全算术、地址类型辅助函数、计数器跟踪以及访问角色管理。 |
| 访问控制 | MinterRole | 将铸造权限限制为授权地址白名单。 |
| 接口 | ITRC165、ITRC721、ITRC721Metadata、ITRC721Enumerable、ITRC721Receiver | 与 TRC-721 协议匹配的抽象接口。 |
| 核心 | TRC165、TRC721 | 必须实现的 TRC-721 和 TRC-165 实现。 |
| 扩展 | TRC721Metadata、TRC721Enumerable | 可选的元数据和枚举扩展。 |
| 可铸造复合合约 | TRC721Mintable、TRC721MetadataMintable | 将核心与 MinterRole 组合以启用铸造功能。 |
| 可部署合约 | TRC721Token | 最终合约——继承完整的合约栈;在其构造函数中修改名称和符号。 |
如何定制
- 在文件底部的构造函数(
TRC721Token)中修改 Token 名称和符号。 - 决定系列是否需要枚举扩展。如不需要,仅继承
TRC721MetadataMintable并去掉TRC721Enumerable,可节省部署和运行时能量。 - 如果需要让非铸造角色地址也能铸造(例如白名单铸造),可通过
MinterRole覆盖_isMinter行为;不要完全删除访问控制,否则任何人都能铸造。 - 部署时使用 Solidity 编译器 0.5.10(合约固定使用
pragma solidity 0.5.10)。
生产环境准备本文件是用于学习的参考实现。在向主网部署包含真实资产的合约之前,请对合约进行审计、使用 linter 检查,并先在 Shasta 测试网部署测试。参见安全最佳实践。
完整参考实现
完整合约如下。为便于复制,特意保持单文件形式。
pragma solidity 0.5.10;
/**
* @dev 对 Solidity 算术运算的封装,增加了溢出检查。
*
* Solidity 中的算术运算在溢出时会发生回绕。这很容易导致 bug,
* 因为程序员通常假设溢出会报错,这是高级编程语言的标准行为。
* `SafeMath` 通过在运算溢出时 revert 交易来恢复这种直觉。
*
* 建议始终使用此库,以消除一整类 bug。
*/
library SafeMath {
/**
* @dev 返回两个无符号整数之和,溢出时 revert。
*
* Solidity `+` 运算符的对应函数。
*
* 要求:
* - 加法不得溢出。
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
/**
* @dev 返回两个无符号整数之差,结果为负时 revert(溢出)。
*
* Solidity `-` 运算符的对应函数。
*
* 要求:
* - 减法不得溢出。
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return sub(a, b, "SafeMath: subtraction overflow");
}
/**
* @dev 返回两个无符号整数之差,溢出时带自定义错误信息 revert(结果为负)。
*
* Solidity `-` 运算符的对应函数。
*
* 要求:
* - 减法不得溢出。
*
* _从 v2.4.0 起可用。_
*/
function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
require(b <= a, errorMessage);
uint256 c = a - b;
return c;
}
/**
* @dev 返回两个无符号整数之积,溢出时 revert。
*
* Solidity `*` 运算符的对应函数。
*
* 要求:
* - 乘法不得溢出。
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas 优化:比要求 'a' 不为零更便宜,但若 'b' 也为零则效果消失。
// 参见:https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
/**
* @dev 返回两个无符号整数的整数除法结果,除以零时 revert。结果向零取整。
*
* Solidity `/` 运算符的对应函数。注意:该函数使用 `revert` opcode
*(保留剩余 gas),而 Solidity 使用无效 opcode(消耗所有剩余 gas)。
*
* 要求:
* - 除数不得为零。
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
return div(a, b, "SafeMath: division by zero");
}
/**
* @dev 返回两个无符号整数的整数除法结果,除以零时带自定义错误信息 revert。结果向零取整。
*
* Solidity `/` 运算符的对应函数。注意:该函数使用 `revert` opcode
*(保留剩余 gas),而 Solidity 使用无效 opcode(消耗所有剩余 gas)。
*
* 要求:
* - 除数不得为零。
*
* _从 v2.4.0 起可用。_
*/
function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
// Solidity 仅在除以 0 时自动断言
require(b > 0, errorMessage);
uint256 c = a / b;
// assert(a == b * c + a % b); // 不存在不成立的情况
return c;
}
/**
* @dev 返回两个无符号整数相除的余数(无符号整数取模),
* 除以零时 revert。
*
* Solidity `%` 运算符的对应函数。该函数使用 `revert` opcode
*(保留剩余 gas),而 Solidity 使用无效 opcode(消耗所有剩余 gas)。
*
* 要求:
* - 除数不得为零。
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
return mod(a, b, "SafeMath: modulo by zero");
}
/**
* @dev 返回两个无符号整数相除的余数(无符号整数取模),
* 除以零时带自定义错误信息 revert。
*
* Solidity `%` 运算符的对应函数。该函数使用 `revert` opcode
*(保留剩余 gas),而 Solidity 使用无效 opcode(消耗所有剩余 gas)。
*
* 要求:
* - 除数不得为零。
*
* _从 v2.4.0 起可用。_
*/
function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
require(b != 0, errorMessage);
return a % b;
}
}
/*
* @dev 提供关于当前执行上下文的信息,包括交易的发送方及其数据。
* 虽然这些信息通常可通过 msg.sender 和 msg.data 获取,
* 但不应以如此直接的方式访问,因为在处理 GSN 元交易时,
* 发送和支付执行的账户可能不是实际发送方(从应用程序角度来看)。
*
* 该合约仅供中间层、类库合约使用。
*/
contract Context {
// 空的内部构造函数,防止人们误将该合约直接部署为实例,
// 该合约应通过继承使用。
constructor () internal { }
// solhint-disable-previous-line no-empty-blocks
function _msgSender() internal view returns (address payable) {
return msg.sender;
}
function _msgData() internal view returns (bytes memory) {
this; // 消除状态可变性警告而不生成字节码——见 https://github.com/ethereum/solidity/issues/2691
return msg.data;
}
}
/**
* @title Roles
* @dev 用于管理分配给角色的地址的库。
*/
library Roles {
struct Role {
mapping (address => bool) bearer;
}
/**
* @dev 授予账户对该角色的访问权限。
*/
function add(Role storage role, address account) internal {
require(!has(role, account), "Roles: account already has role");
role.bearer[account] = true;
}
/**
* @dev 移除账户对该角色的访问权限。
*/
function remove(Role storage role, address account) internal {
require(has(role, account), "Roles: account does not have role");
role.bearer[account] = false;
}
/**
* @dev 检查账户是否具有该角色。
* @return bool
*/
function has(Role storage role, address account) internal view returns (bool) {
require(account != address(0), "Roles: account is the zero address");
return role.bearer[account];
}
}
/**
* @dev 与地址类型相关的函数集合
*/
library Address {
/**
* @dev 如果 `account` 是合约,则返回 true。
*
* 需要注意的是,合约在构造函数执行期间会返回 false,
* 因为合约代码只会在构造函数结束后才存储到链上。
*
* _从 v2.4.0 起可用。_
*/
function isContract(address account) internal view returns (bool) {
uint256 size;
// solhint-disable-next-line no-inline-assembly
assembly { size := extcodesize(account) }
return size > 0;
}
/**
* @dev 将 `address` 转换为 `address payable`。注意这只是类型转换:
* 底层实际值不会改变。
*
* _从 v2.4.0 起可用。_
*/
function toPayable(address account) internal pure returns (address payable) {
return address(uint160(account));
}
/**
* @dev Solidity `transfer` 的替代品:向 `recipient` 发送 `amount` wei,
* 转发所有可用 gas 并在出错时 revert。
*
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[了解更多]。
*
* 重要提示:由于控制权转移给 `recipient`,必须注意避免产生重入漏洞。
* 考虑使用 {ReentrancyGuard} 或
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[检查-效果-交互模式]。
*
* _从 v2.4.0 起可用。_
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
// solhint-disable-next-line avoid-call-value
(bool success, ) = recipient.call.value(amount)("");
require(success, "Address: unable to send value, recipient may have reverted");
}
}
/**
* @title Counters
* @author Matt Condon (@shrugs)
* @dev 提供只能以 1 为步长递增或递减的计数器。
* 可用于跟踪 mapping 中的元素数量、发行 TRC721 ID 或统计请求 ID 等场景。
*
* 使用方式:`using Counters for Counters.Counter;`
* 由于 256 位整数以 1 为步长递增不可能溢出,`increment` 可跳过 {SafeMath}
* 溢出检查,从而节省能量。但这假设使用正确,即底层 `_value` 永远不会被直接访问。
*/
library Counters {
using SafeMath for uint256;
struct Counter {
// 该变量不应被库的使用者直接访问:交互必须限制在库的函数内。
// 截至 Solidity v0.5.2,这无法强制执行,但已有相关改进提议:
// 见 https://github.com/ethereum/solidity/issues/4637
uint256 _value; // 默认:0
}
function current(Counter storage counter) internal view returns (uint256) {
return counter._value;
}
function increment(Counter storage counter) internal {
// 此处可跳过 {SafeMath} 溢出检查,见顶部注释
counter._value += 1;
}
function decrement(Counter storage counter) internal {
counter._value = counter._value.sub(1);
}
}
contract MinterRole is Context {
using Roles for Roles.Role;
event MinterAdded(address indexed account);
event MinterRemoved(address indexed account);
Roles.Role private _minters;
constructor () internal {
_addMinter(_msgSender());
}
modifier onlyMinter() {
require(isMinter(_msgSender()), "MinterRole: caller does not have the Minter role");
_;
}
function isMinter(address account) public view returns (bool) {
return _minters.has(account);
}
function addMinter(address account) public onlyMinter {
_addMinter(account);
}
function renounceMinter() public {
_removeMinter(_msgSender());
}
function _addMinter(address account) internal {
_minters.add(account);
emit MinterAdded(account);
}
function _removeMinter(address account) internal {
_minters.remove(account);
emit MinterRemoved(account);
}
}
/**
* @dev TRC165 标准接口。
*
* 实现者可以声明对合约接口的支持,然后其他人({TRC165Checker})可以查询。
*
* 具体实现请参见 {TRC165}。
*/
interface ITRC165 {
/**
* @dev 如果本合约实现了 `interfaceId` 定义的接口,则返回 true。
*
* 该函数调用消耗 gas 必须少于 30,000。
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
/**
* @dev TRC721 合规合约的必要接口。
*/
contract ITRC721 is ITRC165 {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev 返回 `owner` 账户中的 NFT 数量。
*/
function balanceOf(address owner) public view returns (uint256 balance);
/**
* @dev 返回 `tokenId` 指定的 NFT 的所有者。
*/
function ownerOf(uint256 tokenId) public view returns (address owner);
/**
* @dev 将特定 NFT(`tokenId`)从一个账户(`from`)转移到另一个账户(`to`)。
*
* 要求:
* - `from`、`to` 不能为零地址。
* - `tokenId` 必须由 `from` 持有。
* - 若调用方不是 `from`,则必须通过 {approve} 或 {setApprovalForAll} 获得授权。
*/
function safeTransferFrom(address from, address to, uint256 tokenId) public;
/**
* @dev 将特定 NFT(`tokenId`)从一个账户(`from`)转移到另一个账户(`to`)。
*
* 要求:
* - 若调用方不是 `from`,则必须通过 {approve} 或 {setApprovalForAll} 获得授权。
*/
function transferFrom(address from, address to, uint256 tokenId) public;
function approve(address to, uint256 tokenId) public;
function getApproved(uint256 tokenId) public view returns (address operator);
function setApprovalForAll(address operator, bool _approved) public;
function isApprovedForAll(address owner, address operator) public view returns (bool);
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public;
}
/**
* @title TRC-721 非同质化Token标准,可选元数据扩展
*/
contract ITRC721Metadata is ITRC721 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function tokenURI(uint256 tokenId) external view returns (string memory);
}
/**
* @title TRC721 Token接收者接口
* @dev 任何希望支持来自 TRC721 资产合约的安全转账的合约均需实现此接口。
*/
contract ITRC721Receiver {
/**
* @notice 处理 NFT 的接收
* @dev 在 {ITRC721-safeTransferFrom} 之后,TRC721 智能合约在接收方调用此函数。
* 该函数必须返回函数选择器,否则调用方将 revert 交易。
* 返回的选择器可通过 `this.onTRC721Received.selector` 获取。
* 该函数可以抛出异常以 revert 并拒绝转账。
* 注意:TRC721 合约地址始终是消息发送方。
* @param operator 调用 `safeTransferFrom` 函数的地址
* @param from 先前持有Token的地址
* @param tokenId 正在转移的 NFT 标识符
* @param data 无指定格式的附加数据
* @return bytes4 `bytes4(keccak256("onTRC721Received(address,address,uint256,bytes)"))`
*/
function onTRC721Received(address operator, address from, uint256 tokenId, bytes memory data)
public returns (bytes4);
}
/**
* @dev {ITRC165} 接口的实现。
*
* 合约可继承此合约并调用 {_registerInterface} 来声明对某接口的支持。
*/
contract TRC165 is ITRC165 {
/*
* bytes4(keccak256('supportsInterface(bytes4)')) == 0x01ffc9a7
*/
bytes4 private constant _INTERFACE_ID_TRC165 = 0x01ffc9a7;
/**
* @dev 接口 id 到是否受支持的映射。
*/
mapping(bytes4 => bool) private _supportedInterfaces;
constructor () internal {
// 派生合约只需注册对自身接口的支持,
// 此处注册对 TRC165 本身的支持
_registerInterface(_INTERFACE_ID_TRC165);
}
/**
* @dev 参见 {ITRC165-supportsInterface}。
*
* 时间复杂度 O(1),保证始终消耗少于 30,000 gas。
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool) {
return _supportedInterfaces[interfaceId];
}
/**
* @dev 将合约注册为 `interfaceId` 定义接口的实现者。
* 对 TRC165 接口本身的支持是自动的,无需注册其接口 id。
*
* 参见 {ITRC165-supportsInterface}。
*
* 要求:
*
* - `interfaceId` 不能是 TRC165 的无效接口(`0xffffffff`)。
*/
function _registerInterface(bytes4 interfaceId) internal {
require(interfaceId != 0xffffffff, "TRC165: invalid interface id");
_supportedInterfaces[interfaceId] = true;
}
}
/**
* @title TRC721 非同质化Token标准基础实现
*/
contract TRC721 is Context, TRC165, ITRC721 {
using SafeMath for uint256;
using Address for address;
using Counters for Counters.Counter;
// 等于 `bytes4(keccak256("onTRC721Received(address,address,uint256,bytes)"))`
// 也可通过 `ITRC721Receiver(0).onTRC721Received.selector` 获得
//
// 注意:ERC721 使用 0x150b7a02,TRC721 使用 0x5175f878。
bytes4 private constant _TRC721_RECEIVED = 0x5175f878;
// token ID 到所有者的映射
mapping (uint256 => address) private _tokenOwner;
// token ID 到被授权地址的映射
mapping (uint256 => address) private _tokenApprovals;
// 所有者到持有Token数量的映射
mapping (address => Counters.Counter) private _ownedTokensCount;
// 所有者到操作员授权的映射
mapping (address => mapping (address => bool)) private _operatorApprovals;
/*
* bytes4(keccak256('balanceOf(address)')) == 0x70a08231
* bytes4(keccak256('ownerOf(uint256)')) == 0x6352211e
* bytes4(keccak256('approve(address,uint256)')) == 0x095ea7b3
* bytes4(keccak256('getApproved(uint256)')) == 0x081812fc
* bytes4(keccak256('setApprovalForAll(address,bool)')) == 0xa22cb465
* bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c5
* bytes4(keccak256('transferFrom(address,address,uint256)')) == 0x23b872dd
* bytes4(keccak256('safeTransferFrom(address,address,uint256)')) == 0x42842e0e
* bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) == 0xb88d4fde
*
* => 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^
* 0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde == 0x80ac58cd
*/
bytes4 private constant _INTERFACE_ID_TRC721 = 0x80ac58cd;
constructor () public {
// 注册受支持的接口以通过 TRC165 符合 TRC721
_registerInterface(_INTERFACE_ID_TRC721);
}
/**
* @dev 获取指定地址的余额。
* @param owner 要查询余额的地址
* @return uint256 表示传入地址持有数量的值
*/
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0), "TRC721: balance query for the zero address");
return _ownedTokensCount[owner].current();
}
/**
* @dev 获取指定 token ID 的所有者。
* @param tokenId 要查询所有者的 token 的 uint256 ID
* @return 当前标记为给定 token ID 所有者的地址
*/
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = _tokenOwner[tokenId];
require(owner != address(0), "TRC721: owner query for nonexistent token");
return owner;
}
/**
* @dev 授权另一个地址转移给定的 token ID。
* 零地址表示没有被授权的地址。
* 给定时间内每个 token 只能有一个被授权地址。
* 只能由 token 所有者或被授权的操作员调用。
* @param to 要被授权的地址(用于给定 token ID)
* @param tokenId 要被授权的 token 的 uint256 ID
*/
function approve(address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(to != owner, "TRC721: approval to current owner");
require(_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
"TRC721: approve caller is not owner nor approved for all"
);
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
/**
* @dev 获取 token ID 的被授权地址,若未设置则为零地址。
* 若 token ID 不存在则 revert。
* @param tokenId 要查询授权的 token 的 uint256 ID
* @return 当前被授权的给定 token ID 的地址
*/
function getApproved(uint256 tokenId) public view returns (address) {
require(_exists(tokenId), "TRC721: approved query for nonexistent token");
return _tokenApprovals[tokenId];
}
/**
* @dev 设置或取消给定操作员的授权。
* 操作员被允许代表发送方转移其所有Token。
* @param to 要设置授权的操作员地址
* @param approved 要设置的授权状态
*/
function setApprovalForAll(address to, bool approved) public {
require(to != _msgSender(), "TRC721: approve to caller");
_operatorApprovals[_msgSender()][to] = approved;
emit ApprovalForAll(_msgSender(), to, approved);
}
/**
* @dev 查询操作员是否被给定所有者授权。
* @param owner 要查询授权的所有者地址
* @param operator 要查询授权的操作员地址
* @return bool 给定操作员是否被给定所有者授权
*/
function isApprovedForAll(address owner, address operator) public view returns (bool) {
return _operatorApprovals[owner][operator];
}
/**
* @dev 将给定 token ID 的所有权转移到另一个地址。
* 不建议直接使用此方法,尽可能使用 {safeTransferFrom}。
* 要求 msg.sender 是所有者、被授权方或操作员。
* @param from token 的当前所有者
* @param to 接收给定 token ID 所有权的地址
* @param tokenId 要转移的 token 的 uint256 ID
*/
function transferFrom(address from, address to, uint256 tokenId) public {
//solhint-disable-next-line max-line-length
require(_isApprovedOrOwner(_msgSender(), tokenId), "TRC721: transfer caller is not owner nor approved");
_transferFrom(from, to, tokenId);
}
/**
* @dev 安全地将给定 token ID 的所有权转移到另一个地址。
* 若目标地址是合约,则必须实现 {ITRC721Receiver-onTRC721Received},
* 该函数在安全转账时被调用,并返回魔法值
* `bytes4(keccak256("onTRC721Received(address,address,uint256,bytes)"))`;否则转账将 revert。
* 要求 msg.sender 是所有者、被授权方或操作员。
* @param from token 的当前所有者
* @param to 接收给定 token ID 所有权的地址
* @param tokenId 要转移的 token 的 uint256 ID
*/
function safeTransferFrom(address from, address to, uint256 tokenId) public {
safeTransferFrom(from, to, tokenId, "");
}
/**
* @dev 安全地将给定 token ID 的所有权转移到另一个地址。
* 若目标地址是合约,则必须实现 {ITRC721Receiver-onTRC721Received},
* 该函数在安全转账时被调用,并返回魔法值
* `bytes4(keccak256("onTRC721Received(address,address,uint256,bytes)"))`;否则转账将 revert。
* 要求 _msgSender() 是所有者、被授权方或操作员。
* @param from token 的当前所有者
* @param to 接收给定 token ID 所有权的地址
* @param tokenId 要转移的 token 的 uint256 ID
* @param _data 要随安全转账检查一起发送的字节数据
*/
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public {
require(_isApprovedOrOwner(_msgSender(), tokenId), "TRC721: transfer caller is not owner nor approved");
_safeTransferFrom(from, to, tokenId, _data);
}
/**
* @dev 安全地将给定 token ID 的所有权转移到另一个地址。
* 若目标地址是合约,则必须实现 `onTRC721Received`,
* 该函数在安全转账时被调用,并返回魔法值
* `bytes4(keccak256("onTRC721Received(address,address,uint256,bytes)"))`;否则转账将 revert。
* 要求 msg.sender 是所有者、被授权方或操作员。
* @param from token 的当前所有者
* @param to 接收给定 token ID 所有权的地址
* @param tokenId 要转移的 token 的 uint256 ID
* @param _data 要随安全转账检查一起发送的字节数据
*/
function _safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) internal {
_transferFrom(from, to, tokenId);
require(_checkOnTRC721Received(from, to, tokenId, _data), "TRC721: transfer to non TRC721Receiver implementer");
}
/**
* @dev 返回指定 token 是否存在。
* @param tokenId 要查询是否存在的 token 的 uint256 ID
* @return bool token 是否存在
*/
function _exists(uint256 tokenId) internal view returns (bool) {
address owner = _tokenOwner[tokenId];
return owner != address(0);
}
/**
* @dev 返回给定支出方是否可以转移给定 token ID。
* @param spender 要查询的支出方地址
* @param tokenId 要转移的 token 的 uint256 ID
* @return bool msg.sender 是否被授权操作给定 token ID,
* 是否是所有者的操作员,或者是否是 token 的所有者
*/
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
require(_exists(tokenId), "TRC721: operator query for nonexistent token");
address owner = ownerOf(tokenId);
return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
}
/**
* @dev 安全铸造新 token 的内部函数。
* 若给定 token ID 已存在则 revert。
* 若目标地址是合约,则必须实现 `onTRC721Received`,
* 该函数在安全转账时被调用,并返回魔法值
* `bytes4(keccak256("onTRC721Received(address,address,uint256,bytes)"))`;否则转账将 revert。
* @param to 将拥有铸造 token 的地址
* @param tokenId 要铸造的 token 的 uint256 ID
*/
function _safeMint(address to, uint256 tokenId) internal {
_safeMint(to, tokenId, "");
}
/**
* @dev 安全铸造新 token 的内部函数。
* 若给定 token ID 已存在则 revert。
* 若目标地址是合约,则必须实现 `onTRC721Received`,
* 该函数在安全转账时被调用,并返回魔法值
* `bytes4(keccak256("onTRC721Received(address,address,uint256,bytes)"))`;否则转账将 revert。
* @param to 将拥有铸造 token 的地址
* @param tokenId 要铸造的 token 的 uint256 ID
* @param _data 要随安全转账检查一起发送的字节数据
*/
function _safeMint(address to, uint256 tokenId, bytes memory _data) internal {
_mint(to, tokenId);
require(_checkOnTRC721Received(address(0), to, tokenId, _data), "TRC721: transfer to non TRC721Receiver implementer");
}
/**
* @dev 铸造新 token 的内部函数。
* 若给定 token ID 已存在则 revert。
* @param to 将拥有铸造 token 的地址
* @param tokenId 要铸造的 token 的 uint256 ID
*/
function _mint(address to, uint256 tokenId) internal {
require(to != address(0), "TRC721: mint to the zero address");
require(!_exists(tokenId), "TRC721: token already minted");
_tokenOwner[tokenId] = to;
_ownedTokensCount[to].increment();
emit Transfer(address(0), to, tokenId);
}
/**
* @dev 燃烧特定 token 的内部函数。
* 若 token 不存在则 revert。
* 已废弃,请改用 {_burn}。
* @param owner 要燃烧 token 的所有者
* @param tokenId 正在被燃烧的 token 的 uint256 ID
*/
function _burn(address owner, uint256 tokenId) internal {
require(ownerOf(tokenId) == owner, "TRC721: burn of token that is not own");
_clearApproval(tokenId);
_ownedTokensCount[owner].decrement();
_tokenOwner[tokenId] = address(0);
emit Transfer(owner, address(0), tokenId);
}
/**
* @dev 燃烧特定 token 的内部函数。
* 若 token 不存在则 revert。
* @param tokenId 正在被燃烧的 token 的 uint256 ID
*/
function _burn(uint256 tokenId) internal {
_burn(ownerOf(tokenId), tokenId);
}
/**
* @dev 将给定 token ID 的所有权转移到另一个地址的内部函数。
* 与 {transferFrom} 不同,此函数不对 msg.sender 施加限制。
* @param from token 的当前所有者
* @param to 接收给定 token ID 所有权的地址
* @param tokenId 要转移的 token 的 uint256 ID
*/
function _transferFrom(address from, address to, uint256 tokenId) internal {
require(ownerOf(tokenId) == from, "TRC721: transfer of token that is not own");
require(to != address(0), "TRC721: transfer to the zero address");
_clearApproval(tokenId);
_ownedTokensCount[from].decrement();
_ownedTokensCount[to].increment();
_tokenOwner[tokenId] = to;
emit Transfer(from, to, tokenId);
}
/**
* @dev 在目标地址上调用 {ITRC721Receiver-onTRC721Received} 的内部函数。
* 若目标地址不是合约则不执行调用。
*
* 这是 `TRC721` 合约的内部实现细节,其使用已废弃。
* @param from 表示给定 token ID 先前所有者的地址
* @param to 将接收Token的目标地址
* @param tokenId 正在转移的 token 的 uint256 ID
* @param _data 可选的随调用一起发送的数据
* @return bool 调用是否正确返回了预期的魔法值
*/
function _checkOnTRC721Received(address from, address to, uint256 tokenId, bytes memory _data)
internal returns (bool)
{
if (!to.isContract()) {
return true;
}
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = to.call(abi.encodeWithSelector(
ITRC721Receiver(to).onTRC721Received.selector,
_msgSender(),
from,
tokenId,
_data
));
if (!success) {
if (returndata.length > 0) {
// solhint-disable-next-line no-inline-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert("TRC721: transfer to non TRC721Receiver implementer");
}
} else {
bytes4 retval = abi.decode(returndata, (bytes4));
return (retval == _TRC721_RECEIVED);
}
}
/**
* @dev 清除给定 token ID 当前授权的私有函数。
* @param tokenId 要转移的 token 的 uint256 ID
*/
function _clearApproval(uint256 tokenId) private {
if (_tokenApprovals[tokenId] != address(0)) {
_tokenApprovals[tokenId] = address(0);
}
}
}
contract TRC721Metadata is Context, TRC165, TRC721, ITRC721Metadata {
// Token名称
string private _name;
// Token符号
string private _symbol;
// 基础 URI
string private _baseURI;
// token URI 的可选映射
mapping(uint256 => string) private _tokenURIs;
/*
* bytes4(keccak256('name()')) == 0x06fdde03
* bytes4(keccak256('symbol()')) == 0x95d89b41
* bytes4(keccak256('tokenURI(uint256)')) == 0xc87b56dd
*
* => 0x06fdde03 ^ 0x95d89b41 ^ 0xc87b56dd == 0x5b5e139f
*/
bytes4 private constant _INTERFACE_ID_TRC721_METADATA = 0x5b5e139f;
/**
* @dev 构造函数
*/
constructor (string memory name, string memory symbol) public {
_name = name;
_symbol = symbol;
// 注册受支持的接口以通过 TRC165 符合 TRC721
_registerInterface(_INTERFACE_ID_TRC721_METADATA);
}
/**
* @dev 获取Token名称。
* @return 表示Token名称的字符串
*/
function name() external view returns (string memory) {
return _name;
}
/**
* @dev 获取Token符号。
* @return 表示Token符号的字符串
*/
function symbol() external view returns (string memory) {
return _symbol;
}
/**
* @dev 返回给定 token ID 的 URI。若为空字符串则可能返回空串。
*
* 若 token 的 URI 非空且已设置基础 URI(通过 {_setBaseURI}),
* 则将其作为前缀添加到 token ID 的 URI 前。
*
* 若 token ID 不存在则 revert。
*/
function tokenURI(uint256 tokenId) external view returns (string memory) {
require(_exists(tokenId), "TRC721Metadata: URI query for nonexistent token");
string memory _tokenURI = _tokenURIs[tokenId];
// 即使存在基础 URI,也仅在非空的 token 特定 URI 前追加
if (bytes(_tokenURI).length == 0) {
return "";
} else {
// 使用 abi.encodePacked 拼接字符串
return string(abi.encodePacked(_baseURI, _tokenURI));
}
}
/**
* @dev 为给定 token 设置 token URI 的内部函数。
*
* 若 token ID 不存在则 revert。
*
* 提示:若所有 token ID 共享同一前缀(例如 URI 形如
* `http://api.myproject.com/token/<id>`),可使用 {_setBaseURI} 存储,
* 以节省 gas。
*/
function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal {
require(_exists(tokenId), "TRC721Metadata: URI set of nonexistent token");
_tokenURIs[tokenId] = _tokenURI;
}
/**
* @dev 为所有 token ID 设置基础 URI 的内部函数。
* 将自动作为前缀添加到 {tokenURI} 返回的值前。
*
* _从 v2.5.0 起可用。_
*/
function _setBaseURI(string memory baseURI) internal {
_baseURI = baseURI;
}
/**
* @dev 返回通过 {_setBaseURI} 设置的基础 URI。
* 将自动作为前缀添加到 {tokenURI} 中每个 token 的 URI 前(当 URI 非空时)。
*
* _从 v2.5.0 起可用。_
*/
function baseURI() external view returns (string memory) {
return _baseURI;
}
/**
* @dev 燃烧特定 token 的内部函数。
* 若 token 不存在则 revert。
* 已废弃,请改用 _burn(uint256)。
* @param owner 要燃烧 token 的所有者
* @param tokenId 正在被 msg.sender 燃烧的 token 的 uint256 ID
*/
function _burn(address owner, uint256 tokenId) internal {
super._burn(owner, tokenId);
// 清除元数据(若有)
if (bytes(_tokenURIs[tokenId]).length != 0) {
delete _tokenURIs[tokenId];
}
}
}
/**
* @title TRC721MetadataMintable
* @dev 带元数据的 TRC721 铸造逻辑。
*/
contract TRC721MetadataMintable is TRC721, TRC721Metadata, MinterRole {
/**
* @dev 铸造Token的函数。
* @param to 接收铸造Token的地址。
* @param tokenId 要铸造的 token id。
* @param tokenURI 铸造Token的 token URI。
* @return 布尔值,指示操作是否成功。
*/
function mintWithTokenURI(address to, uint256 tokenId, string memory tokenURI) public onlyMinter returns (bool) {
_mint(to, tokenId);
_setTokenURI(tokenId, tokenURI);
return true;
}
}
/**
* @title TRC721Mintable
* @dev TRC721 铸造逻辑。
*/
contract TRC721Mintable is TRC721, MinterRole {
/**
* @dev 铸造Token的函数。
* @param to 接收铸造Token的地址。
* @param tokenId 要铸造的 token id。
* @return 布尔值,指示操作是否成功。
*/
function mint(address to, uint256 tokenId) public onlyMinter returns (bool) {
_mint(to, tokenId);
return true;
}
/**
* @dev 安全铸造Token的函数。
* @param to 接收铸造Token的地址。
* @param tokenId 要铸造的 token id。
* @return 布尔值,指示操作是否成功。
*/
function safeMint(address to, uint256 tokenId) public onlyMinter returns (bool) {
_safeMint(to, tokenId);
return true;
}
/**
* @dev 安全铸造Token的函数。
* @param to 接收铸造Token的地址。
* @param tokenId 要铸造的 token id。
* @param _data 要随安全转账检查一起发送的字节数据。
* @return 布尔值,指示操作是否成功。
*/
function safeMint(address to, uint256 tokenId, bytes memory _data) public onlyMinter returns (bool) {
_safeMint(to, tokenId, _data);
return true;
}
}
/**
* @title TRC-721 非同质化Token标准,可选枚举扩展
*/
contract ITRC721Enumerable is ITRC721 {
function totalSupply() public view returns (uint256);
function tokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256 tokenId);
function tokenByIndex(uint256 index) public view returns (uint256);
}
/**
* @title 带可选枚举扩展逻辑的 TRC-721 非同质化Token
*/
contract TRC721Enumerable is Context, TRC165, TRC721, ITRC721Enumerable {
// 所有者到持有 token ID 列表的映射
mapping(address => uint256[]) private _ownedTokens;
// token ID 到所有者Token列表中索引的映射
mapping(uint256 => uint256) private _ownedTokensIndex;
// 包含所有 token id 的数组,用于枚举
uint256[] private _allTokens;
// token id 到 allTokens 数组中位置的映射
mapping(uint256 => uint256) private _allTokensIndex;
/*
* bytes4(keccak256('totalSupply()')) == 0x18160ddd
* bytes4(keccak256('tokenOfOwnerByIndex(address,uint256)')) == 0x2f745c59
* bytes4(keccak256('tokenByIndex(uint256)')) == 0x4f6ccce7
*
* => 0x18160ddd ^ 0x2f745c59 ^ 0x4f6ccce7 == 0x780e9d63
*/
bytes4 private constant _INTERFACE_ID_TRC721_ENUMERABLE = 0x780e9d63;
/**
* @dev 构造函数。
*/
constructor () public {
// 注册受支持的接口以通过 TRC165 符合 TRC721Enumerable
_registerInterface(_INTERFACE_ID_TRC721_ENUMERABLE);
}
/**
* @dev 获取请求所有者Token列表中给定索引处的 token ID。
* @param owner 拥有要访问的Token列表的地址
* @param index 表示要访问的请求Token列表索引的 uint256
* @return uint256 请求地址所拥有的Token列表中给定索引处的 token ID
*/
function tokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256) {
require(index < balanceOf(owner), "TRC721Enumerable: owner index out of bounds");
return _ownedTokens[owner][index];
}
/**
* @dev 获取合约存储的Token总量。
* @return uint256 表示Token总量的值
*/
function totalSupply() public view returns (uint256) {
return _allTokens.length;
}
/**
* @dev 获取此合约中所有Token中给定索引处的 token ID。
* 若索引大于或等于Token总数则 revert。
* @param index 表示要访问的Token列表索引的 uint256
* @return uint256 Token列表中给定索引处的 token ID
*/
function tokenByIndex(uint256 index) public view returns (uint256) {
require(index < totalSupply(), "TRC721Enumerable: global index out of bounds");
return _allTokens[index];
}
/**
* @dev 将给定 token ID 的所有权转移到另一个地址的内部函数。
* 与 transferFrom 不同,此函数不对 msg.sender 施加限制。
* @param from token 的当前所有者
* @param to 接收给定 token ID 所有权的地址
* @param tokenId 要转移的 token 的 uint256 ID
*/
function _transferFrom(address from, address to, uint256 tokenId) internal {
super._transferFrom(from, to, tokenId);
_removeTokenFromOwnerEnumeration(from, tokenId);
_addTokenToOwnerEnumeration(to, tokenId);
}
/**
* @dev 铸造新 token 的内部函数。
* 若给定 token ID 已存在则 revert。
* @param to 将拥有铸造 token 的受益人地址
* @param tokenId 要铸造的 token 的 uint256 ID
*/
function _mint(address to, uint256 tokenId) internal {
super._mint(to, tokenId);
_addTokenToOwnerEnumeration(to, tokenId);
_addTokenToAllTokensEnumeration(tokenId);
}
/**
* @dev 燃烧特定 token 的内部函数。
* 若 token 不存在则 revert。
* 已废弃,请改用 {TRC721-_burn}。
* @param owner 要燃烧 token 的所有者
* @param tokenId 正在被燃烧的 token 的 uint256 ID
*/
function _burn(address owner, uint256 tokenId) internal {
super._burn(owner, tokenId);
_removeTokenFromOwnerEnumeration(owner, tokenId);
// 由于 tokenId 将被删除,可以清除其在 _ownedTokensIndex 中的槽位以触发 gas 退款
_ownedTokensIndex[tokenId] = 0;
_removeTokenFromAllTokensEnumeration(tokenId);
}
/**
* @dev 获取请求所有者的 token ID 列表。
* @param owner 拥有Token的地址
* @return uint256[] 请求地址所拥有的 token ID 列表
*/
function _tokensOfOwner(address owner) internal view returns (uint256[] storage) {
return _ownedTokens[owner];
}
/**
* @dev 将 token 添加到此扩展所有权跟踪数据结构的私有函数。
* @param to 表示给定 token ID 新所有者的地址
* @param tokenId 要添加到给定地址Token列表的 token 的 uint256 ID
*/
function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
_ownedTokensIndex[tokenId] = _ownedTokens[to].length;
_ownedTokens[to].push(tokenId);
}
/**
* @dev 将 token 添加到此扩展 token 跟踪数据结构的私有函数。
* @param tokenId 要添加到Token列表的 token 的 uint256 ID
*/
function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
_allTokensIndex[tokenId] = _allTokens.length;
_allTokens.push(tokenId);
}
/**
* @dev 从此扩展所有权跟踪数据结构中删除 token 的私有函数。注意
* 当 token 未分配新所有者时,`_ownedTokensIndex` 映射不会更新:
* 这允许在执行转账操作时进行 gas 优化(避免双重写入)。
* 时间复杂度为 O(1),但会改变 _ownedTokens 数组的顺序。
* @param from 表示给定 token ID 先前所有者的地址
* @param tokenId 要从给定地址Token列表中删除的 token 的 uint256 ID
*/
function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
// 为防止 from 的Token数组出现间隙,将最后一个 token 存储在要删除的 token 的索引处,
// 然后删除最后一个槽位(交换并弹出)。
uint256 lastTokenIndex = _ownedTokens[from].length.sub(1);
uint256 tokenIndex = _ownedTokensIndex[tokenId];
// 当要删除的 token 是最后一个 token 时,交换操作是不必要的
if (tokenIndex != lastTokenIndex) {
uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];
_ownedTokens[from][tokenIndex] = lastTokenId; // 将最后一个 token 移到要删除 token 的槽位
_ownedTokensIndex[lastTokenId] = tokenIndex; // 更新被移动 token 的索引
}
// 这也会删除数组最后位置的内容
_ownedTokens[from].length--;
// 注意 _ownedTokensIndex[tokenId] 未被清除:它仍然指向旧槽位
//(现在由 lastTokenId 占据,或者如果 token 是最后一个则超出数组末尾)。
}
/**
* @dev 从此扩展 token 跟踪数据结构中删除 token 的私有函数。
* 时间复杂度为 O(1),但会改变 _allTokens 数组的顺序。
* @param tokenId 要从Token列表中删除的 token 的 uint256 ID
*/
function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
// 为防止 tokens 数组出现间隙,将最后一个 token 存储在要删除的 token 的索引处,
// 然后删除最后一个槽位(交换并弹出)。
uint256 lastTokenIndex = _allTokens.length.sub(1);
uint256 tokenIndex = _allTokensIndex[tokenId];
// 当要删除的 token 是最后一个 token 时,交换操作是不必要的。
// 但由于这种情况发生得很少(当最后铸造的 token 被燃烧时),
// 我们仍然在这里进行交换,以避免添加 'if' 语句的 gas 成本
//(与 _removeTokenFromOwnerEnumeration 不同)
uint256 lastTokenId = _allTokens[lastTokenIndex];
_allTokens[tokenIndex] = lastTokenId; // 将最后一个 token 移到要删除 token 的槽位
_allTokensIndex[lastTokenId] = tokenIndex; // 更新被移动 token 的索引
// 这也会删除数组最后位置的内容
_allTokens.length--;
_allTokensIndex[tokenId] = 0;
}
}
contract TRC721Token is TRC721, TRC721Enumerable, TRC721MetadataMintable {
constructor() public TRC721Metadata("Your Token Name", "YTN") {
}
}相关资源
- TRC-721 —— 标准概览
- TRC-721 协议接口 —— 函数参考
- 发行 TRC-721 Token —— 部署演练
- TRC-721 合约交互 —— 读取和写入已部署合约
- 安全最佳实践 —— 审计检查清单
Updated 5 days ago