目标:向大家解释一下erc721中safeMint与mint的区别(safeTransferFrom与transferFrom同理)
章节流程:
- 理论为先
- 猜测预期
- 验证猜测
- 结论
一、理论为先
首先,我们看一下示例代码(文件ERC721Upgradeable.sol
和IERC721ReceiverUpgradeable.sol
)
示例代码地址
- 文件
ERC721Upgradeable.sol
中的关键内容:
/**
* @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
* forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
*/
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"
);
}
/**
* @dev Mints `tokenId` and transfers it to `to`.
*
* WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible
*
* Requirements:
*
* - `tokenId` must not exist.
* - `to` cannot be the zero address.
*
* Emits a {Transfer} event.
*/
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);
}
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory _data
) private returns (bool) {
if (to.isContract()) {
try IERC721ReceiverUpgradeable(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
return retval == IERC721ReceiverUpgradeable.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
- 文件
IERC721ReceiverUpgradeable.sol
中的内容:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title ERC721 token receiver interface
* @dev Interface for any contract that wants to support safeTransfers
* from ERC721 asset contracts.
*/
interface IERC721ReceiverUpgradeable {
/**
* @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
* by `operator` from `from`, this function is called.
*
* It must return its Solidity selector to confirm the token transfer.
* If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
*
* The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`.
*/
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
二、猜测预期
通过上述示例源码我们可以发现:其实方法safeTransferFrom中额外校验了一下IERC721ReceiverUpgradeable(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval)
的执行结果是否通过(注:通过则返回正确的方法签名-IERC721.onERC721Received.selector),通过交易成功,如果不通过交易则回滚。
那接下来,我们提出一个猜测:“接受方为合约账户时,可以通过重写方法onERC721Received
进行交易的安全校验”。
三、验证猜测
我们来准备两个简单的合约,一个为自定义的721合约MyToken721UUPSV1.sol
,另一个为实现onERC721Received方法的自定义接收方合约MyToken721UUPSV1_Holder.sol
。
接收方合约验证来自721合约mint方法传递的参数data是否为预期数据,是则交易通过,不是则交易不通过使其回滚,达到安全的效果。
具体代码如下:
- 文件
MyToken721UUPSV1.sol
中的内容:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract MyToken721UUPSV1 is ERC721Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}
function initialize() initializer public {
__ERC721_init("MyToken", "MTK");
__Ownable_init();
__UUPSUpgradeable_init();
}
// 传递data为“bsn”
// 注:to需要为合约账户
function safeMint(address to, uint256 tokenId) public onlyOwner {
bytes memory data = new bytes(3);
data[0]="b";
data[1]="s";
data[2]="n";
_safeMint(to, tokenId,data);
}
// 传递data为“zxl”
// 注:to需要为合约账户
function safeMintZxl(address to, uint256 tokenId) public onlyOwner {
bytes memory data = new bytes(3);
data[0]="z";
data[1]="x";
data[2]="l";
_safeMint(to, tokenId,data);
}
function _authorizeUpgrade(address newImplementation)
internal
onlyOwner
override
{}
function GetInitializeData() public pure returns(bytes memory){
return abi.encodeWithSignature("initialize()");
}
function myName() public view virtual returns (string memory){
return "zxlv1";
}
}
- 文件
MyToken721UUPSV1_Holder .sol
中的内容:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract MyToken721UUPSV1_Holder is OwnableUpgradeable
, IERC721Receiver
{
event ERC721Received( address operator,
address from,
uint256 tokenId,
bytes data);
// 用于接收合约发来的数据
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) public override returns (bytes4) {
emit ERC721Received(operator,from,tokenId,data);
//safeMint 传递的参数数据
bytes memory safedata = new bytes(3);
safedata[0]="b";
safedata[1]="s";
safedata[2]="n";
// 如果为safeMint则返回正确的结果,否则返回错误的结果。
if (equal(safedata,data)) {
return this.onERC721Received.selector;
} else {
return this.myName.selector;
}
}
function myName() public view virtual returns (string memory){
return "MyToken721UUPSV1_Holder";
}
function equal( bytes memory self_rep, bytes memory other_rep) internal pure returns(bool){
if(self_rep.length != other_rep.length){
return false;
}
uint selfLen = self_rep.length;
for(uint i=0;i<selfLen;i++){
if(self_rep[i] != other_rep[i]) return false;
}
return true;
}
}
使用工具MetaMask和Remix部署合约至Rinkeby 测试网络:
- holder: 0xe9E8F76524aE41C93Dd2066dFEd3B41fbf21f59B
- 721:0x376bAfBE6619233b8E4536f2f7CAb611d35BFb79
- proxy:0x4955db0b2E5C437A5C9e431118967C3699e0Dc43
接下来我们开始测试,分别调用safeMint
方法和safeMintZxl
方法
- 执行721合约的
safeMint
方法,执行成功,详情如下:
(注:传递数据为"bsn"
)
交易输出截图:
交易hash:https://rinkeby.etherscan.io/tx/0x4f6fdacb3a2221103da71126820d5309ae19341d787a77e9cfede26479eb5c56
结果验证: 调用方法ownerof
验证1
是否mint至账户0xe9E8F76524aE41C93Dd2066dFEd3B41fbf21f59B
,截图如下:
在etherscan中查看输出日志: 我在合约里打印了下传递过来的参数
- 执行721合约的
safeMintZxl
方法,执行失败,详情如下:
(注:传递数据"zxl"
)
我们发现已经提示交易异常,如果执意执行发送交易,结果如下:
也可以多余的验证下2
没有mint至账户0xe9E8F76524aE41C93Dd2066dFEd3B41fbf21f59B
,调用ownerOf
返回ERC721: owner query for nonexistent token
,调用balanceOf
返回1
,截图如下:
符合预期猜测!!!
四、结论
当接收方为合约账户时,可以使用safeXXX方法(safeMint或者safeTransferFrom)进行交易的安全校验。如果是普通账户的话,两者的执行没有区别。
顺便提一下erc1155也是同样的道理。
以上为个人的观点,欢迎大家一块交流。