
Subscribe to KevinWang

Subscribe to KevinWang
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
Actually you can simply understand the ERC-721 is NFT.
This is the link of EIP-721.https://eips.ethereum.org/EIPS/eip-721
Official explanation of ERC-721 is “Non-Fungible Token”, which means every token is specially. That means you can use NFT to represent unique image, stock, or even real estate.
You can find plenty of tutorial of ERC-721. And this blog will show you the practical application.
Fist of all, let’s see the solidity code of a contract which implemented the ERC-721 protocol.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721URIStorage, ERC721} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract Merkamigos is ERC721URIStorage {
uint256 private _nextTokenId;
constructor() ERC721("Merkamigos", "MKG") {}
}
It’s very easy to implement the ERC-721 by inheriting the ERC721URIStorage, but we want to find out what is happening in it.
Let’s see the constructor first.
When the contract was deployed, the constructor() would be executed.
And you may confuse of the grammar.
In solidity, if a contract inherit from the parent contract, and it’s parent contract has a constructor function with parameters, it is necessary to call the parent contract constructor within the child contract constructor and provide parameters.
constructor() ERC721("Merkamigos", "MKG") {}
Now let’s see the constructor() of it’s father contract.
abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {
using Strings for uint256;
// Token name
string private _name;
// Token symbol
string private _symbol;
/**
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/// @inheritdoc IERC721Metadata
function name() public view virtual returns (string memory) {
return _name;
}
/// @inheritdoc IERC721Metadata
function symbol() public view virtual returns (string memory) {
return _symbol;
}
/**
* ......
*/
}
We can find the private parameters: _name and _symbol.
_name is the name of NTF and _symbol is the symbol of NTF.
In OpenSea, you can find all NTF have the name and symbol, it is achieved by these two parameters’s getter function, name() and symbol().
And our example contract is named as ‘Merkamigos’ and symboled as ‘MKG’.
We find that there is no other function of parameter in our example contract. It is caused by
father contract-ERC721URIStorage and ERC-721.
We can explicitly find the public function on REMIX.
!image.png
abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {
/// @inheritdoc IERC721
function approve(address to, uint256 tokenId) public virtual {
_approve(to, tokenId, _msgSender());
}
/**
* @dev Approve `to` to operate on `tokenId`
*
* The `auth` argument is optional. If the value passed is non 0, then this function will check that `auth` is
* either the owner of the token, or approved to operate on all tokens held by this owner.
*
* Emits an {Approval} event.
*
* Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
*/
function _approve(address to, uint256 tokenId, address auth) internal {
_approve(to, tokenId, auth, true);
}
/**
* @dev Variant of `_approve` with an optional flag to enable or disable the {Approval} event. The event is not
* emitted in the context of transfers.
*/
function _approve(address to, uint256 tokenId, address auth, bool emitEvent) internal virtual {
// Avoid reading the owner unless necessary
if (emitEvent || auth != address(0)) {
address owner = _requireOwned(tokenId);
// We do not use _isAuthorized because single-token approvals should not be able to call approve
if (auth != address(0) && owner != auth && !isApprovedForAll(owner, auth)) {
revert ERC721InvalidApprover(auth);
}
if (emitEvent) {
emit Approval(owner, to, tokenId);
}
}
_tokenApprovals[tokenId] = to;
}
}
The caller can use approve() to specify another address to own the auth of controlling the token. _tokenApprovals records the mapping of tokenId to approvals. Notice that there is only an address can be approved to a token.
abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {
/// @inheritdoc IERC721
function safeTransferFrom(address from, address to, uint256 tokenId) public {
safeTransferFrom(from, to, tokenId, "");
}
/// @inheritdoc IERC721
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual {
transferFrom(from, to, tokenId);
ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data);
}
}
The safeTransferFrom apply four parameters.
In the safeTransferFrom function, it calls transferFrom first.
/// @inheritdoc IERC721
function transferFrom(address from, address to, uint256 tokenId) public virtual {
if (to == address(0)) {
revert ERC721InvalidReceiver(address(0));
}
// Setting an "auth" arguments enables the `_isAuthorized` check which verifies that the token exists
// (from != 0). Therefore, it is not needed to verify that the return value is not 0 here.
address previousOwner = _update(to, tokenId, _msgSender());
if (previousOwner != from) {
revert ERC721IncorrectOwner(from, tokenId, previousOwner);
}
}
function _update(address to, uint256 tokenId, address auth) internal virtual returns (address) {
address from = _ownerOf(tokenId);
// Perform (optional) operator check
if (auth != address(0)) {
_checkAuthorized(from, auth, tokenId);
}
// Execute the update
if (from != address(0)) {
// Clear approval. No need to re-authorize or emit the Approval event
_approve(address(0), tokenId, address(0), false);
unchecked {
_balances[from] -= 1;
}
}
if (to != address(0)) {
unchecked {
_balances[to] += 1;
}
}
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
return from;
}
function _checkAuthorized(address owner, address spender, uint256 tokenId) internal view virtual {
if (!_isAuthorized(owner, spender, tokenId)) {
if (owner == address(0)) {
revert ERC721NonexistentToken(tokenId);
} else {
revert ERC721InsufficientApproval(spender, tokenId);
}
}
}
/**
* @dev Returns whether `spender` is allowed to manage `owner`'s tokens, or `tokenId` in
* particular (ignoring whether it is owned by `owner`).
*
* WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this
* assumption.
*/
function _isAuthorized(address owner, address spender, uint256 tokenId) internal view virtual returns (bool) {
return
spender != address(0) &&
(owner == spender || isApprovedForAll(owner, spender) || _getApproved(tokenId) == spender);
}
In transferFrom, it first checks if the to address is 0.
Then it calls update() to check if the caller has auth to control the NTF.
In _checkAuthorized() , the function check if the caller is the owner of NTF, or if the caller has been approved by the owner of NFT.
Then it calls _approve(address(0), tokenId, address(0), false); to clear the approve of the NFT. Because when the NTF has been transferred, the old approvers should be cleared to avoid old approvers can still control the NFT without the permission from new owner.
Then, it add 1 on the new owner’s balance, and reduce 1 on the old owner’s balance. Then it declares the NTF’s owner has been changed.
Finally, it calls ERC721Utils.checkOnERC721Received to check if the to address can accept the NFT. Notion that only the contract address should be checked.
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) {
if (retval != IERC721Receiver.onERC721Received.selector) {
// Token rejected
revert IERC721Errors.ERC721InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
// non-IERC721Receiver implementer
revert IERC721Errors.ERC721InvalidReceiver(to);
} else {
assembly ("memory-safe") {
revert(add(reason, 0x20), mload(reason))
}
}
}
}
Every contract which can receive NFT should achieve onERC721Received function and return the specific data- bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
If the contract does not achieve the function, it will throw an error.
/// @inheritdoc IERC721
function setApprovalForAll(address operator, bool approved) public virtual {
_setApprovalForAll(_msgSender(), operator, approved);
}
/**
* @dev Approve `operator` to operate on all of `owner` tokens
*
* Requirements:
* - operator can't be the address zero.
*
* Emits an {ApprovalForAll} event.
*/
function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
if (operator == address(0)) {
revert ERC721InvalidOperator(operator);
}
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
When some address x calls the setApprovalForAll, it can specify an address to control all NFTs of the owner address x.
This function do not need to check the ownership cause the owner is the caller, the caller do not have the motivation to attack itself.
In this article, we introduce all state-changing ****function in ERC-721.
We will introduce the view/pure function in ERC-721 in next article.
Actually you can simply understand the ERC-721 is NFT.
This is the link of EIP-721.https://eips.ethereum.org/EIPS/eip-721
Official explanation of ERC-721 is “Non-Fungible Token”, which means every token is specially. That means you can use NFT to represent unique image, stock, or even real estate.
You can find plenty of tutorial of ERC-721. And this blog will show you the practical application.
Fist of all, let’s see the solidity code of a contract which implemented the ERC-721 protocol.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721URIStorage, ERC721} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract Merkamigos is ERC721URIStorage {
uint256 private _nextTokenId;
constructor() ERC721("Merkamigos", "MKG") {}
}
It’s very easy to implement the ERC-721 by inheriting the ERC721URIStorage, but we want to find out what is happening in it.
Let’s see the constructor first.
When the contract was deployed, the constructor() would be executed.
And you may confuse of the grammar.
In solidity, if a contract inherit from the parent contract, and it’s parent contract has a constructor function with parameters, it is necessary to call the parent contract constructor within the child contract constructor and provide parameters.
constructor() ERC721("Merkamigos", "MKG") {}
Now let’s see the constructor() of it’s father contract.
abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {
using Strings for uint256;
// Token name
string private _name;
// Token symbol
string private _symbol;
/**
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/// @inheritdoc IERC721Metadata
function name() public view virtual returns (string memory) {
return _name;
}
/// @inheritdoc IERC721Metadata
function symbol() public view virtual returns (string memory) {
return _symbol;
}
/**
* ......
*/
}
We can find the private parameters: _name and _symbol.
_name is the name of NTF and _symbol is the symbol of NTF.
In OpenSea, you can find all NTF have the name and symbol, it is achieved by these two parameters’s getter function, name() and symbol().
And our example contract is named as ‘Merkamigos’ and symboled as ‘MKG’.
We find that there is no other function of parameter in our example contract. It is caused by
father contract-ERC721URIStorage and ERC-721.
We can explicitly find the public function on REMIX.
!image.png
abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {
/// @inheritdoc IERC721
function approve(address to, uint256 tokenId) public virtual {
_approve(to, tokenId, _msgSender());
}
/**
* @dev Approve `to` to operate on `tokenId`
*
* The `auth` argument is optional. If the value passed is non 0, then this function will check that `auth` is
* either the owner of the token, or approved to operate on all tokens held by this owner.
*
* Emits an {Approval} event.
*
* Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
*/
function _approve(address to, uint256 tokenId, address auth) internal {
_approve(to, tokenId, auth, true);
}
/**
* @dev Variant of `_approve` with an optional flag to enable or disable the {Approval} event. The event is not
* emitted in the context of transfers.
*/
function _approve(address to, uint256 tokenId, address auth, bool emitEvent) internal virtual {
// Avoid reading the owner unless necessary
if (emitEvent || auth != address(0)) {
address owner = _requireOwned(tokenId);
// We do not use _isAuthorized because single-token approvals should not be able to call approve
if (auth != address(0) && owner != auth && !isApprovedForAll(owner, auth)) {
revert ERC721InvalidApprover(auth);
}
if (emitEvent) {
emit Approval(owner, to, tokenId);
}
}
_tokenApprovals[tokenId] = to;
}
}
The caller can use approve() to specify another address to own the auth of controlling the token. _tokenApprovals records the mapping of tokenId to approvals. Notice that there is only an address can be approved to a token.
abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {
/// @inheritdoc IERC721
function safeTransferFrom(address from, address to, uint256 tokenId) public {
safeTransferFrom(from, to, tokenId, "");
}
/// @inheritdoc IERC721
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual {
transferFrom(from, to, tokenId);
ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data);
}
}
The safeTransferFrom apply four parameters.
In the safeTransferFrom function, it calls transferFrom first.
/// @inheritdoc IERC721
function transferFrom(address from, address to, uint256 tokenId) public virtual {
if (to == address(0)) {
revert ERC721InvalidReceiver(address(0));
}
// Setting an "auth" arguments enables the `_isAuthorized` check which verifies that the token exists
// (from != 0). Therefore, it is not needed to verify that the return value is not 0 here.
address previousOwner = _update(to, tokenId, _msgSender());
if (previousOwner != from) {
revert ERC721IncorrectOwner(from, tokenId, previousOwner);
}
}
function _update(address to, uint256 tokenId, address auth) internal virtual returns (address) {
address from = _ownerOf(tokenId);
// Perform (optional) operator check
if (auth != address(0)) {
_checkAuthorized(from, auth, tokenId);
}
// Execute the update
if (from != address(0)) {
// Clear approval. No need to re-authorize or emit the Approval event
_approve(address(0), tokenId, address(0), false);
unchecked {
_balances[from] -= 1;
}
}
if (to != address(0)) {
unchecked {
_balances[to] += 1;
}
}
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
return from;
}
function _checkAuthorized(address owner, address spender, uint256 tokenId) internal view virtual {
if (!_isAuthorized(owner, spender, tokenId)) {
if (owner == address(0)) {
revert ERC721NonexistentToken(tokenId);
} else {
revert ERC721InsufficientApproval(spender, tokenId);
}
}
}
/**
* @dev Returns whether `spender` is allowed to manage `owner`'s tokens, or `tokenId` in
* particular (ignoring whether it is owned by `owner`).
*
* WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this
* assumption.
*/
function _isAuthorized(address owner, address spender, uint256 tokenId) internal view virtual returns (bool) {
return
spender != address(0) &&
(owner == spender || isApprovedForAll(owner, spender) || _getApproved(tokenId) == spender);
}
In transferFrom, it first checks if the to address is 0.
Then it calls update() to check if the caller has auth to control the NTF.
In _checkAuthorized() , the function check if the caller is the owner of NTF, or if the caller has been approved by the owner of NFT.
Then it calls _approve(address(0), tokenId, address(0), false); to clear the approve of the NFT. Because when the NTF has been transferred, the old approvers should be cleared to avoid old approvers can still control the NFT without the permission from new owner.
Then, it add 1 on the new owner’s balance, and reduce 1 on the old owner’s balance. Then it declares the NTF’s owner has been changed.
Finally, it calls ERC721Utils.checkOnERC721Received to check if the to address can accept the NFT. Notion that only the contract address should be checked.
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) {
if (retval != IERC721Receiver.onERC721Received.selector) {
// Token rejected
revert IERC721Errors.ERC721InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
// non-IERC721Receiver implementer
revert IERC721Errors.ERC721InvalidReceiver(to);
} else {
assembly ("memory-safe") {
revert(add(reason, 0x20), mload(reason))
}
}
}
}
Every contract which can receive NFT should achieve onERC721Received function and return the specific data- bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
If the contract does not achieve the function, it will throw an error.
/// @inheritdoc IERC721
function setApprovalForAll(address operator, bool approved) public virtual {
_setApprovalForAll(_msgSender(), operator, approved);
}
/**
* @dev Approve `operator` to operate on all of `owner` tokens
*
* Requirements:
* - operator can't be the address zero.
*
* Emits an {ApprovalForAll} event.
*/
function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
if (operator == address(0)) {
revert ERC721InvalidOperator(operator);
}
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
When some address x calls the setApprovalForAll, it can specify an address to control all NFTs of the owner address x.
This function do not need to check the ownership cause the owner is the caller, the caller do not have the motivation to attack itself.
In this article, we introduce all state-changing ****function in ERC-721.
We will introduce the view/pure function in ERC-721 in next article.
No activity yet