//tips
//smart contract
構造理解継続。
_mintの代わりに使用が推奨されている_safeMintとは、_mintの後に_checkOnERC721Receivedのrequire文の処理を追加している。
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"
);
}
_checkOnERC721Receivedについて詳しく見ていく。返り値はboolなのでyesかno。
最初に実施されている分岐のisContract()は、もしtoがコントラクトだった場合にtrueを返す。
https://docs.openzeppelin.com/contracts/2.x/api/utils#Address-isContract-address-
It is unsafe to assume that an address for which this function returns false is an externally-owned account (EOA) and not a contract.
toがコントラクトアドレスのとき、ERC721Receiverに従ってonERC721Received()を呼ぶ。これはbyte4で返し、_ERC721_RECEIVEDと等しいかどうか確認するもの。
_ERC721_RECEIVEDはbytes4 private constant _ERC721_RECEIVED = 0x150b7a02;
と定数で定義されている。
そもそもこれが何を行っているかわからなかったので調べてみると、ERC721ではトークンが行方不明になるのを防ぐため、コントラクトがトークンを受け取る場合にはコントラクトがonERC721Received(_from, _tokenId, _data) を呼びだされた時に受け取りを許可するならば戻り値としてマジックナンバー 0xf0b9e5ba を返すことを決めているよう。
このマジックナンバーをきちんと返すかの確認。
https://qiita.com/shora_kujira16/items/6efd834e6a3a3eb8a98c
つまり、 return retval == IERC721Receiver.onERC721Received.selector;の部分で0xf0b9e5baで一致しているかの判断をおこない、その値を返している。
Selector部分がよくわからないのでもう少し突っ込む。
シンプルな説明は見つけた。
If is usually called after a token has been transferred (in same tx).
If the receiver of the token is a contract, it checks if the contract implements the onERC721Received interface.
If no, it reverts the transaction.
If yes, the receiver contract has a onERC721Received method. The ERC721 calls this method, and now execution goes to the receiver contract to do whatever he wants. For example - staking the received token. [or more dangerously - reenter the ERC721 contarct.]
After the receiver token's onERC721Received finishes, the execution resumes in the ERC721 contract.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721Receiver.sol
The ERC721 smart contract calls this function on the recipient after a IERC721.safeTransferFrom. This function MUST return the function selector, otherwise the caller will revert the transaction. The selector to be returned can be obtained as this.onERC721Received.selector. This function MAY throw to revert and reject the transfer. Note: the ERC721 contract address is always the message sender.
https://docs.openzeppelin.com/contracts/2.x/api/token/erc721
やはりselectorがいまいちわからない。
https://ethereum.stackexchange.com/questions/72363/what-is-a-function-selector
たどり着いた。コードをコンパイルした時に生成されるabiの内容のよう。
https://docs.soliditylang.org/en/v0.8.12/abi-spec.html
web3.eth.contract#methodsなどを使って、Contractの関数を呼び出すtransactionを作成した場合は、必ずtransactionのdata部の先頭4byteに対象となる関数のfunctionIdが埋め込まれ、
EVMではこのdata部を引数として受け取り、解析することで実行すべき関数を探索しているとのこと。
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory data
) private returns (bool) {
if (to.isContract()) {
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.isContract()がtureの場合、つまりコントラクトの場合でもstakingの処理を行うことにつなげられるよう。一般的には変なコントラクトにトークンを送らないためのgox対策のよう。
Requireでエラーになるとは_mintは取り消される。この場合は_mintを実行した際にかかったgas代は戻ってこない。
次はapproveを確認。これは指定されたトークンIDをtoに転送することの許可。
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 nor approved for all"
);
_approve(to, tokenId);
}