实现一个ERC721协议下的NFT合约。ERC271主要实现三个接口:IERC721、IERC721Metadata、IERC721Receiver.
上一章在ERC20协议下完成了一个代币合约,本章将实现一个ERC721协议下的NFT合约。 ERC271主要实现三个接口:IERC721、IERC721Metadata、IERC721Receiver,另外还需要实现IERC165。 IERC165只有一个方法,就是判断是该合约是否实现接口,其定义如下。
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)
pragma solidity ^0.8.0;
interface IERC165 {
//检查是否支持对应的接口实现
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol)
pragma solidity ^0.8.0;
import "./IERC721.sol";
interface IERC721Metadata is IERC721 {
/**
* 返回合约名称,如:BitCoin.
*/
function name() external view returns (string memory);
/**
* 返回合约简称,如:BTC.
*/
function symbol() external view returns (string memory);
/**
* 返回Token源数据的URI
*/
function tokenURI(uint256 tokenId) external view returns (string memory);
}
主要定义了三个方法:合约名称、合约简历、token元数据所在URI。 IERC721Receiver主要定义了合约之前互相操作的实现,定义如下。具体的使用将在下面实现时进行详细讲解。
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC721/IERC721Receiver.sol)
pragma solidity ^0.8.0;
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/IERC721.sol)
pragma solidity ^0.8.0;
import "./IERC165.sol";
interface IERC721 is IERC165 {
//交易转帐事件定义
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
//授权事件,对owner帐号下的指定tokenId授权
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
//授权事件,对owner帐号下的所有token授权
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
//返回ownre帐户下的资产数量。
function balanceOf(address owner) external view returns (uint256 balance);
//查询一个tokenId属于哪个帐户
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* 把`tokenId` token 从 `from` 转移至 `to`.
*
* 需满足:
*
* - `from` 不能为0地址.
* - `to` 不能为0地址.
* - `tokenId` token 必段存在并且被`from`拥有.
* - 如果调用都是不 `from`, 调用者必须被拥有者通过 {approve} 或 {setApprovalForAll}授权过.
* - 如果 `to` 是一个合约地址, 那么他必须实现过 {IERC721Receiver-onERC721Received}, onERC721Received可以实现接受token的逻辑,那么该token就可以转移至to的合约中。备注:data参数中可以放一些转移至新合约中的必要参数,比如:token在新合约中的address等
*
* - 最后触发一个 {Transfer} 事件.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
/**
* 把`tokenId` token 从 `from` 转移至 `to`.
*
* 需满足:
*
* - `from` 不能为0地址.
* - `to` 不能为0地址.
* - `tokenId` token 必段存在并且被`from`拥有.
* - 如果调用都是不 `from`, 调用者必须被拥有者通过 {approve} 或 {setApprovalForAll}授权过.
* - 如果 `to` 是一个合约地址, 那么他必须实现过 {IERC721Receiver-onERC721Received}, onERC721Received可以实现接受token的逻辑,那么该token就可以转移至to的合约中。备注:该方法其实并不能处理好转移至新合约的整个逻辑,因为新合约并不清楚该token需要转移至哪个地址上,当然在新合约中可以全都转移至一个默认地址,需要实现完整的逻辑还是需要使用
* safeTransferFrom(from, to, tokenId, data)函数。
* - 最后触发一个 {Transfer} 事件.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* 把`tokenId` token 从 `from` 转移至 `to`.
*
* 警告:不建议使用该函数,尽可能地使用 {safeTransferFrom}
*
* 需满足:
*
* - `from` 不能为0地址.
* - `to` 不能为0地址.
* - `tokenId` token 必须存在并且被`from`拥有.
* - 如果调用都是不 `from`, 调用者必须被拥有者通过 {approve} 或 {setApprovalForAll}授权过.
*
* - 最后触发一个 {Transfer} 事件.
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev 授要给 `to` 可以转移 `tokenId` token 给任意一个帐号.
*
* 该授权会在tokenId被转移后失效。
*
* 一个tokenId只能授权一个帐号,如果要清除授权可以再调用该方法授权给0地址。
*
* 需满足:
*
* - 调用者必须是token的拥有者 或是 被{isApprovedForAll}授权过。
* - `tokenId` 必须存在.
*
* 触发 {Approval} 事件.
*/
function approve(address to, uint256 tokenId) external;
/**
* 授权(取消授权)给 调用者 `operator`
* 当前调用者授权给 `operator` 可以调用 {transferFrom} or {safeTransferFrom} 对自己拥有的所有token进行处理
*
* 需满足:
*
* - 被授权者 `operator` 不能是调用者自己.
*
* 触发 {ApprovalForAll} 事件.
*/
function setApprovalForAll(address operator, bool _approved) external;
/**
* 返回tokenId 授权过的地址.
*
* 需满足:
*
* - `tokenId` 必须存在.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* 返回 `operator` 是否允许管理 `owner`的所有token.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/ERC721.sol)
// 本源码参考至 OpenZeppelin Contracts。
pragma solidity ^0.8.0;
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./IERC721Metadata.sol";
import "./IERC165.sol";
contract ERC721 is IERC165, IERC721, IERC721Metadata {
// 合约名称,如:BitCoin
string private _name;
// 合约简称,如:BTC
string private _symbol;
// TokenId的拥有者帐户地址
// 如:{1 => 0x123456, 2 => 0x123457, 3 => 0x123457} , tokenID 1 属于0x123456, tokenId 2和3 属于0x123457
mapping(uint256 => address) private _owners;
// 帐户地址的token数据量,在mint和转入时需增加,在转出和燃烧时需减少
// 如:{0x123457 => 1, ox123456 => 2} , 0x123456 有1个token, 0x123457 有2个token
mapping(address => uint256) private _balances;
// TokenID 授权帐户地址, 除tokenID的拥有者外,该帐户地址也可能对tokenID进行处理。
// {1 => 0x123457, 2 => 0x123456} tokenId 1 授权给0x123457, 0x123457就可以对它进行处理。
mapping(uint256 => address) private _tokenApprovals;
// 授权给另一个帐户全权处理自己名下的所有token
// {0x123457 => {0x123456 => true}}
mapping(address => mapping(address => bool)) private _operatorApprovals;
//设置合约的发布者为管理员, 管理员有比较高的权限,设置高权限管理员主要用Mint的时候做权限校验。
address private _admin;
/**
* 合约构造函数,初始化合约名称,简称和合约部署者
*/
constructor() {
_name = "Harry.NTF";
_symbol = "HNT";
_admin = msg.sender;
}
/**
* 判断实现了些什么接口.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165) returns (bool) {
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId ||
interfaceId == type(IERC165).interfaceId;
//super.supportsInterface(interfaceId);
}
/**
* 返回ownre帐户下的资产数量。
*/
function balanceOf(address owner) public view virtual override returns (uint256) {
require(owner != address(0), "ERC721: address zero is not a valid owner");
return _balances[owner];
}
/**
* 查询一个tokenId属于哪个帐户
*/
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "ERC721: invalid token ID");
return owner;
}
/**
* @dev See {IERC721Metadata-name}.
*/
function name() public view virtual override returns (string memory) {
return _name;
}
/**
* @dev See {IERC721Metadata-symbol}.
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/**
* @dev See {IERC721Metadata-tokenURI}.
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
_requireMinted(tokenId);
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? strConcat(string(abi.encodePacked(baseURI, toString(tokenId))), ".json"): "";
}
/**
* 返回base_URI
*/
function _baseURI() internal view virtual returns (string memory) {
return "https://qinxin.oss-cn-beijing.aliyuncs.com/token/";
}
/**
* 查看 {IERC721-approve}.
*/
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721.ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
"ERC721: approve caller is not token owner or approved for all"
);
_approve(to, tokenId);
}
/**
* 查看 {IERC721-getApproved}.
*/
function getApproved(uint256 tokenId) public view virtual override returns (address) {
_requireMinted(tokenId);
return _tokenApprovals[tokenId];
}
/**
* 查看 {IERC721-setApprovalForAll}.
*/
function setApprovalForAll(address operator, bool approved) public virtual override {
_setApprovalForAll(_msgSender(), operator, approved);
}
/**
* 查看 {IERC721-isApprovedForAll}.
*/
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _admin == _msgSender() || _operatorApprovals[owner][operator];
}
function mint(address to, uint256 tokenId) public {
if (msg.sender != _admin) return;
_safeMint(to, tokenId, "");
}
/**
* 查看 {IERC721-transferFrom}.
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) public virtual override {
//solhint-disable-next-line max-line-length
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_transfer(from, to, tokenId);
}
/**
* 查看 {IERC721-safeTransferFrom}.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) public virtual override {
safeTransferFrom(from, to, tokenId, "");
}
/**
* 查看 {IERC721-safeTransferFrom}.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public virtual override {
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_safeTransfer(from, to, tokenId, data);
}
/**
* 安全转移函数, 将`tokenId` 从 `from` 帐户转移至 `to` 帐户。 这里会检查当 `to` 是一个合约地址时,`to`必须实现ERC721协议中{IERC721Receiver-onERC721Received},并调用
* `to` 合约的onERC721Received方法,该方法可以实现怎么接受一个其它合约转过来的token. 如果没有正确处理,该token该放在一个合约地址下,永远都没法被正确使用了。
* `data` 是一个附加数据,没有特别指定格式,一般情况下,当`to`是一个合约地址时,`data`可以是真实的接收帐户地址,这样在`to`合约中也能知道,该token会转移至哪个帐户下。
*
* 需满足:
*
* - `from` 不能为空地址.
* - `to` 不能为空地址.
* - `tokenId` 必须存在而且被`from`拥有.
* - 如果 `to` 是合约, 它必须实现 {IERC721Receiver-onERC721Received}才能被正常转移.
*
* 触发 {Transfer} 事件.
*/
function _safeTransfer(
address from,
address to,
uint256 tokenId,
bytes memory data
) internal virtual {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}
/**
* 返回tokenId是否存在
*
* Tokens 被mint时开始存在,直到被燃烧掉,
*/
function _exists(uint256 tokenId) internal view virtual returns (bool) {
return _owners[tokenId] != address(0);
}
/**
* 返回 `spender` 是否被允许管理 `tokenId`.
*
* 需满足:
*
* - `tokenId` 必须存在.
*/
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
address owner = ERC721.ownerOf(tokenId);
return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}
/**
* 给`to`帐户铸造一个 `tokenId`
*
* 需满足:
*
* - `tokenId` 没有被铸造过.
* - 如果`to` 是一个合约地址, 它必须实现{IERC721Receiver-onERC721Received}才能正常完成.
*
* 触发 {Transfer} 事件.
*/
function _safeMint(address to, uint256 tokenId) internal virtual {
_safeMint(to, tokenId, "");
}
/**
* 查看 {ERC721-_safeMint}, 添加 `data` 参数,当 `to` 为合约时,发送给{IERC721Receiver-onERC721Received}
*/
function _safeMint(
address to,
uint256 tokenId,
bytes memory data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
/**
* 给`to`帐户铸造一个 `tokenId`
*
* WARNING:不推荐直接使用该方法,尽量使用 {_safeMint}
*
* 需满足:
*
* - `tokenId` 没有被铸造过.
* - 如果`to` 是一个合约地址, 它必须实现{IERC721Receiver-onERC721Received}才能正常完成.
*
* 触发 {Transfer} 事件.
*/
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId);
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId);
}
/**
* 销毁 `tokenId`. 这里只定义了一个private的方法,需要提供公开调用需要实现一个public方法
* 销毁 `tokenId`时,会同时清空授权
* 该方法并没有做安全性检查,实现public方法时需要注意对权限进行检查。
*
* 需满足:
*
* - `tokenId` 必须存在.
*
* 触发 {Transfer} 事件.
*/
function _burn(uint256 tokenId) internal virtual {
address owner = ERC721.ownerOf(tokenId);
_beforeTokenTransfer(owner, address(0), tokenId);
// Clear approvals
delete _tokenApprovals[tokenId];
_balances[owner] -= 1;
delete _owners[tokenId];
emit Transfer(owner, address(0), tokenId);
_afterTokenTransfer(owner, address(0), tokenId);
}
/**
* 将 `tokenId` 从 `from` 帐户转移至 `to` 帐户.
* 该方法主要给 {transferFrom} 提供转移功能, 所以没有对msg.sender进行安全性检查.
*
* 需满足:
*
* - `to` 不是空地址.
* - `tokenId` 必须存在,并且属于`from`.
*
* 触发 {Transfer} 事件.
*/
function _transfer(
address from,
address to,
uint256 tokenId
) internal virtual {
require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
require(to != address(0), "ERC721: transfer to the zero address");
_beforeTokenTransfer(from, to, tokenId);
// Clear approvals from the previous owner
delete _tokenApprovals[tokenId];
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
_afterTokenTransfer(from, to, tokenId);
}
/**
* @dev Approve `to` to operate on `tokenId`
*
* Emits an {Approval} event.
*/
function _approve(address to, uint256 tokenId) internal virtual {
_tokenApprovals[tokenId] = to;
emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}
/**
* @dev 授权 `operator` 可以管理 `owner` 的所有tokens, 如果要取消授权,可以将 `approved` 设置为 false
*
* 触发 {ApprovalForAll} 事件.
*/
function _setApprovalForAll(
address owner,
address operator,
bool approved
) internal virtual {
require(owner != operator, "ERC721: approve to caller");
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
/**
* @dev mint之前检查tokenId是否铸造过.
*/
function _requireMinted(uint256 tokenId) internal view virtual {
require(_exists(tokenId), "ERC721: invalid token ID");
}
/*
*
* 判断 `to` 地址是一个合约时,该合约需要实现{IERC721Receiver-onERC721Received},需要调用onERC721Received方法,通知`to`合约接受一个token.
* `to`地址的合约在onERC721Received方法通过参数实现接受token的逻辑,并返回一个固定的成功值IERC721Receiver.onERC721Received.selector。否则
* 该逻辑可以用于合约升级等功能,但同时也要注意多个合约之前tokenId的重复问题,同时要注意以调用都进行安全性检查,防止合约的恶意调用。
* 当前合约将tokenId放在一个合约地址名下,将没有用户可以正常使用该token,是否需要销毁本合约的token,开发者可根据实限情况决定.
* 如果 `to` 地址不是一个合约,将不执行任何逻辑。
*
* @param `from` address 转移前的owner
* @param `to` 转移后的owner或是一个合约地址
* @param tokenId
* @param data 发送给`to`合约的附加数据
* @return 执行是否成功
*/
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory data
) private returns (bool) {
if (isContract(to)) {
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
/**
*
* 自定义转移前需完成的任务,这里没有做任何实现
*
* Calling conditions:
*
* - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
* transferred to `to`.
* - When `from` is zero, `tokenId` will be minted for `to`.
* - When `to` is zero, ``from``'s `tokenId` will be burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual {}
/**
*
*自定义转移后需完成的任务,这里没有做任何实现
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _afterTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual {}
/**
* 判断地址是否是一个合约
*/
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize/address.code.length, which returns 0
// for contracts in construction, since the code is only stored at the end
// of the constructor execution.
return account.code.length > 0;
}
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function toString(uint256 value) internal pure returns (string memory) {
// Inspired by OraclizeAPI's implementation - MIT licence
// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
function strConcat(string memory _a, string memory _b) internal pure returns (string memory) {
bytes memory _ba = bytes(_a);
bytes memory _bb = bytes(_b);
string memory ret = new string(_ba.length + _bb.length);
bytes memory bret = bytes(ret);
uint k = 0;
for (uint i = 0; i < _ba.length; i++)bret[k++] = _ba[i];
for (uint i = 0; i < _bb.length; i++)bret[k++] = _bb[i];
return string(ret);
}
}
// 合约名称,如:BitCoin
string private _name;
// 合约简称,如:BTC
string private _symbol;
// TokenId的拥有者帐户地址
// 如:{1 => 0x123456, 2 => 0x123457, 3 => 0x123457} , tokenID 1 属于0x123456, tokenId 2和3 属于0x123457
mapping(uint256 => address) private _owners;
// 帐户地址的token数量,在mint和转入时需增加,在转出和燃烧时需减少
// 如:{0x123457 => 1, ox123456 => 2} , 0x123456 有1个token, 0x123457 有2个token
mapping(address => uint256) private _balances;
// TokenID 授权帐户地址, 除tokenID的拥有者外,该帐户地址也可能对tokenID进行处理。
// {1 => 0x123457, 2 => 0x123456} tokenId 1 授权给0x123457, 0x123457就可以对它进行处理。
mapping(uint256 => address) private _tokenApprovals;
// 授权给另一个帐户全权处理自己名下的所有token
// {0x123457 => {0x123456 => true}}
mapping(address => mapping(address => bool)) private _operatorApprovals;
//设置合约的发布者为管理员, 管理员有比较高的权限,设置高权限管理员主要用Mint的时候做权限校验。
address private _admin;
_name和_symbol是合约的名称,IERC721Metadata接口中的name()和symbol()使用,这些内容主要是方便一起通用的钱包和平台可以兼容显示相关的名称。 mapping(uint256 => address) private _owners; 是一个tokenId对应所属用用地址的map, 在transfer和mint,burn的时候都需要对它进行数据处理。 mapping(address => uint256) private _balances; 帐户地址拥有的token数量,方便查询使用。 mapping(uint256 => address) private _tokenApprovals; 针对一个token进行授权,授权的帐户可以调用transferFrom方法将token进行处理。一个tokenId只能对应一个授权帐户地址。 mapping(address => mapping(address => bool)) private _operatorApprovals; 将一个帐户授权给其它多个帐户,授权后这些帐户可以处理授权人名下的所有token。当然也可以取消授权,对应的授权值设置为false. address private _admin; 保存的是合约发布者的帐户地址,部署合约时在构造函数里赋值。正式合约是一个开放式的代码,只有自己对自己token才有处理权,并不推荐给管理员太多权限,这里方便测试,所以给了管理员很多权限。
/**
* @dev See {IERC721Metadata-tokenURI}.
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
_requireMinted(tokenId);
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? strConcat(string(abi.encodePacked(baseURI, toString(tokenId))), ".json"): "";
}
/**
* 返回base_URI
*/
function _baseURI() internal view virtual returns (string memory) {
return "https://qinxin.oss-cn-beijing.aliyuncs.com/token/";
}
IERC721Metadata接口中定义了tokenURI方法,该方法是通过tokenId获取token的元数据的URI,元数据的URI一般是一个中心化的API,当然也可以是去中心化的链,比如ipfs等。为了方便测试,笔者提供了几个URL,供大家在测试开发的时候使用: https://qinxin.oss-cn-beijing.aliyuncs.com/token/1.json https://qinxin.oss-cn-beijing.aliyuncs.com/token/2.json https://qinxin.oss-cn-beijing.aliyuncs.com/token/3.json https://qinxin.oss-cn-beijing.aliyuncs.com/token/4.json https://qinxin.oss-cn-beijing.aliyuncs.com/token/5.json 元数据的格式大家可以自定义,很多平台定义了自己的数据结构,遵循开放平台的数据结构,可以让平台识别我们的NTF,大家有兴趣可以去看看opensea的NTF数据结构。笔者测试定义的结构为:
{
"description": "一个测试用的NTF,ID为1", //NTF描述
"external_url": "https://token.bcspace.tech/activity/1", //NTF主页,发布者一般都会为一个NTF建一个项目首页
"image": "https://qinxin.oss-cn-beijing.aliyuncs.com/token/images/1.jpg", //NFT下载地址
"name": "NTF_TEST_1", //NTF名称
"attributes": [] //NTF属性值
}
/**
* 查看 {IERC721-approve}.
*/
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721.ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
"ERC721: approve caller is not token owner or approved for all"
);
_approve(to, tokenId);
}
/**
* 查看 {IERC721-getApproved}.
*/
function getApproved(uint256 tokenId) public view virtual override returns (address) {
_requireMinted(tokenId);
return _tokenApprovals[tokenId];
}
/**
* @dev 授权 `to` 可以管理 `tokenId`
*
* 触发 {Approval} 事件.
*/
function _approve(address to, uint256 tokenId) internal virtual {
_tokenApprovals[tokenId] = to;
emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}
approve方法授权一个帐户可以管理一个tokenId, 调用必须是tokenId的拥有者或是拥有者对调用者进行过setApprovalForAll授权。 调用者不能对自己进行授权。 getApproved方法可以查询一个tokenId是否有授权帐户地址。
/**
* 查看 {IERC721-setApprovalForAll}.
*/
function setApprovalForAll(address operator, bool approved) public virtual override {
_setApprovalForAll(_msgSender(), operator, approved);
}
/**
* 查看 {IERC721-isApprovedForAll}.
*/
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _admin == _msgSender() || _operatorApprovals[owner][operator];
}
setApprovalForAll调用者授权一个operator可以管理自己名下的所有token, approved参数为false时是取消授权,不同于approve是对单token和单用户授权, isApprovedForAll查询 {operator}是否对 {owner}的token有管理权。
function mint(address to, uint256 tokenId) public {
if (msg.sender != _admin) return;
_safeMint(to, tokenId, "");
}
function _safeMint(address to, uint256 tokenId) internal virtual {
_safeMint(to, tokenId, "");
}
function _safeMint(
address to,
uint256 tokenId,
bytes memory data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
/**
* 给`to`帐户铸造一个 `tokenId`
*
* WARNING:不推荐直接使用该方法,尽量使用 {_safeMint}
*
* 需满足:
*
* - `tokenId` 没有被铸造过.
* - 如果`to` 是一个合约地址, 它必须实现{IERC721Receiver-onERC721Received}才能正常完成.
*
* 触发 {Transfer} 事件.
*/
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId);
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId);
}
mint实现了铸造一个新的token功能,这里只允许admin可以铸造。铸造主要完成了_balances加, 新的tokenId在_owners归属谁的名下。同时也调用了_checkOnERC721Received方法。
function _burn(uint256 tokenId) internal virtual {
address owner = ERC721.ownerOf(tokenId);
_beforeTokenTransfer(owner, address(0), tokenId);
// Clear approvals
delete _tokenApprovals[tokenId];
_balances[owner] -= 1;
delete _owners[tokenId];
emit Transfer(owner, address(0), tokenId);
_afterTokenTransfer(owner, address(0), tokenId);
}
私有方法_burn,销毁 tokenId. 这里只定义了一个private的方法,需要提供公开调用需要实现一个public方法。销毁 tokenId时,会同时清空授权。该方法并没有做安全性检查,实现public方法时需要注意对权限进行检查。
/**
* 查看 {IERC721-transferFrom}.
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) public virtual override {
//solhint-disable-next-line max-line-length
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_transfer(from, to, tokenId);
}
/**
* 查看 {IERC721-safeTransferFrom}.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) public virtual override {
safeTransferFrom(from, to, tokenId, "");
}
/**
* 查看 {IERC721-safeTransferFrom}.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public virtual override {
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_safeTransfer(from, to, tokenId, data);
}
function _safeTransfer(
address from,
address to,
uint256 tokenId,
bytes memory data
) internal virtual {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}
function _transfer(
address from,
address to,
uint256 tokenId
) internal virtual {
require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
require(to != address(0), "ERC721: transfer to the zero address");
_beforeTokenTransfer(from, to, tokenId);
// Clear approvals from the previous owner
delete _tokenApprovals[tokenId];
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
_afterTokenTransfer(from, to, tokenId);
}
public 方法 transferFrom,safeTransferFrom。安全转移函数, 将tokenId
从 from
帐户转移至 to
帐户。 这里会检查当 to
是一个合约地址时,to
必须实现ERC721协议中{IERC721Receiver-onERC721Received},并调用to
合约的onERC721Received方法,该方法可以实现怎么接受一个其它合约转过来的token. 如果没有正确处理,该token该放在一个合约地址下,永远都没法被正确使用了。data
是一个附加数据,没有特别指定格式,一般情况下,当to
是一个合约地址时,data
可以是真实的接收帐户地址,这样在to
合约中也能知道,该token会转移至哪个帐户下。
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory data
) private returns (bool) {
if (isContract(to)) {
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
判断 to
地址是一个合约时,该合约需要实现{IERC721Receiver-onERC721Received},需要调用onERC721Received方法,通知to
合约接受一个token。to
地址的合约在onERC721Received方法通过参数实现接受token的逻辑,并返回一个固定的成功值IERC721Receiver.onERC721Received.selector。否则该逻辑可以用于合约升级等功能,但同时也要注意多个合约之前tokenId的重复问题,同时要注意以调用都进行安全性检查,防止合约的恶意调用。当前合约将tokenId放在一个合约地址名下,将没有用户可以正常使用该token,是否需要销毁本合约的token,开发者可根据实限情况决定。如果 to
地址不是一个合约,将不执行任何逻辑。
本合约中该部分的逻辑并不完整,当IERC721Receiver(to).onERC721Received返回不正常时,应该做一些回滚逻辑。
完成本合约后就可以编译部署了。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!