url: https://solidity-by-example.org/app/erc721/
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC165 {
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
interface IERC721 is IERC165 {
function balanceOf(address owner) external view returns (uint balance);
function ownerOf(uint tokenId) external view returns (address owner);
function safeTransferFrom(address from, address to, uint tokenId) external;
function safeTransferFrom(
address from,
address to,
uint tokenId,
bytes calldata data
) external;
function transferFrom(address from, address to, uint tokenId) external;
function approve(address to, uint tokenId) external;
function getApproved(uint tokenId) external view returns (address operator);
function setApprovalForAll(address operator, bool _approved) external;
function isApprovedForAll(
address owner,
address operator
) external view returns (bool);
}
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint tokenId,
bytes calldata data
) external returns (bytes4);
}
contract ERC721 is IERC721 {
event Transfer(address indexed from, address indexed to, uint indexed id);
event Approval(address indexed owner, address indexed spender, uint indexed id);
event ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
);
// Mapping from token ID to owner address
mapping(uint => address) internal _ownerOf;
// Mapping owner address to token count
mapping(address => uint) internal _balanceOf;
// Mapping from token ID to approved address
mapping(uint => address) internal _approvals;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) public isApprovedForAll;
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}
function ownerOf(uint id) external view returns (address owner) {
owner = _ownerOf[id];
require(owner != address(0), "token doesn't exist");
}
function balanceOf(address owner) external view returns (uint) {
require(owner != address(0), "owner = zero address");
return _balanceOf[owner];
}
function setApprovalForAll(address operator, bool approved) external {
isApprovedForAll[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function approve(address spender, uint id) external {
address owner = _ownerOf[id];
require(
msg.sender == owner || isApprovedForAll[owner][msg.sender],
"not authorized"
);
_approvals[id] = spender;
emit Approval(owner, spender, id);
}
function getApproved(uint id) external view returns (address) {
require(_ownerOf[id] != address(0), "token doesn't exist");
return _approvals[id];
}
function _isApprovedOrOwner(
address owner,
address spender,
uint id
) internal view returns (bool) {
return (spender == owner ||
isApprovedForAll[owner][spender] ||
spender == _approvals[id]);
}
function transferFrom(address from, address to, uint id) public {
require(from == _ownerOf[id], "from != owner");
require(to != address(0), "transfer to zero address");
require(_isApprovedOrOwner(from, msg.sender, id), "not authorized");
_balanceOf[from]--;
_balanceOf[to]++;
_ownerOf[id] = to;
delete _approvals[id];
emit Transfer(from, to, id);
}
function safeTransferFrom(address from, address to, uint id) external {
transferFrom(from, to, id);
require(
to.code.length == 0 ||
IERC721Receiver(to).onERC721Received(msg.sender, from, id, "") ==
IERC721Receiver.onERC721Received.selector,
"unsafe recipient"
);
}
function safeTransferFrom(
address from,
address to,
uint id,
bytes calldata data
) external {
transferFrom(from, to, id);
require(
to.code.length == 0 ||
IERC721Receiver(to).onERC721Received(msg.sender, from, id, data) ==
IERC721Receiver.onERC721Received.selector,
"unsafe recipient"
);
}
function _mint(address to, uint id) internal {
require(to != address(0), "mint to zero address");
require(_ownerOf[id] == address(0), "already minted");
_balanceOf[to]++;
_ownerOf[id] = to;
emit Transfer(address(0), to, id);
}
function _burn(uint id) internal {
address owner = _ownerOf[id];
require(owner != address(0), "not minted");
_balanceOf[owner] -= 1;
delete _ownerOf[id];
delete _approvals[id];
emit Transfer(owner, address(0), id);
}
}
contract MyNFT is ERC721 {
function mint(address to, uint id) external {
_mint(to, id);
}
function burn(uint id) external {
require(msg.sender == _ownerOf[id], "not owner");
_burn(id);
}
}
ERC-721은 Fungible 특성을 가지고 있는 ERC-20과 다르게 Non-Fungible Token (대체 불가능 토큰, NFT)을 뜻하는 이더리움 토큰 표준이다.
대체 불가능은 단어의 뜻 그대로 대체가 불가능하다는 의미로 사용된다. 기존의 ERC-20 토큰은 실생활에 사용되는 아날로그 화폐와 동일하게 다른 화폐와 같은 가치를 가지고 교환이 가능하다.
하지만, NFT는 고유한 정보를 저장하는 토큰이기 때문에 같은 대체 불가능하다는 특징을 가지고 있다.
mint 함수 인자 to에 토큰 owner address를 입력하고, id 값에 임의의 숫자로 토큰을 생성한다.
mint(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 1)
mint(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 777)
balanceOf 함수를 사용해서 토큰 owner address를 입력해보면 아래와 같이 토큰 개수를 반환한다.
대체 가능 토큰, ERC-20 토큰과 동일하다.
하지만, ownerOf에서 토큰 생성 시 입력한 고유 id 값에 대한 토큰을 조회하면 해당 토큰을 식별하고, 식별한 토큰의 owner address를 반환하는 것을 확인할 수 있다.
url: https://solidity-by-example.org/app/erc1155/
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC1155 {
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 value,
bytes calldata data
) external;
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external;
function balanceOf(address owner, uint256 id) external view returns (uint256);
function balanceOfBatch(
address[] calldata owners,
uint256[] calldata ids
) external view returns (uint256[] memory);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(
address owner,
address operator
) external view returns (bool);
}
interface IERC1155TokenReceiver {
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
) external returns (bytes4);
function onERC1155BatchReceived(
address operator,
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external returns (bytes4);
}
contract ERC1155 is IERC1155 {
event TransferSingle(
address indexed operator,
address indexed from,
address indexed to,
uint256 id,
uint256 value
);
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] values
);
event ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
);
event URI(string value, uint256 indexed id);
// owner => id => balance
mapping(address => mapping(uint256 => uint256)) public balanceOf;
// owner => operator => approved
mapping(address => mapping(address => bool)) public isApprovedForAll;
function balanceOfBatch(
address[] calldata owners,
uint256[] calldata ids
) external view returns (uint256[] memory balances) {
require(owners.length == ids.length, "owners length != ids length");
balances = new uint[](owners.length);
unchecked {
for (uint256 i = 0; i < owners.length; i++) {
balances[i] = balanceOf[owners[i]][ids[i]];
}
}
}
function setApprovalForAll(address operator, bool approved) external {
isApprovedForAll[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 value,
bytes calldata data
) external {
require(
msg.sender == from || isApprovedForAll[from][msg.sender],
"not approved"
);
require(to != address(0), "to = 0 address");
balanceOf[from][id] -= value;
balanceOf[to][id] += value;
emit TransferSingle(msg.sender, from, to, id, value);
if (to.code.length > 0) {
require(
IERC1155TokenReceiver(to).onERC1155Received(
msg.sender,
from,
id,
value,
data
) == IERC1155TokenReceiver.onERC1155Received.selector,
"unsafe transfer"
);
}
}
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external {
require(
msg.sender == from || isApprovedForAll[from][msg.sender],
"not approved"
);
require(to != address(0), "to = 0 address");
require(ids.length == values.length, "ids length != values length");
for (uint256 i = 0; i < ids.length; i++) {
balanceOf[from][ids[i]] -= values[i];
balanceOf[to][ids[i]] += values[i];
}
emit TransferBatch(msg.sender, from, to, ids, values);
if (to.code.length > 0) {
require(
IERC1155TokenReceiver(to).onERC1155BatchReceived(
msg.sender,
from,
ids,
values,
data
) == IERC1155TokenReceiver.onERC1155BatchReceived.selector,
"unsafe transfer"
);
}
}
// ERC165
function supportsInterface(bytes4 interfaceId) external view returns (bool) {
return
interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
interfaceId == 0xd9b67a26 || // ERC165 Interface ID for ERC1155
interfaceId == 0x0e89341c; // ERC165 Interface ID for ERC1155MetadataURI
}
// ERC1155 Metadata URI
function uri(uint256 id) public view virtual returns (string memory) {}
// Internal functions
function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
require(to != address(0), "to = 0 address");
balanceOf[to][id] += value;
emit TransferSingle(msg.sender, address(0), to, id, value);
if (to.code.length > 0) {
require(
IERC1155TokenReceiver(to).onERC1155Received(
msg.sender,
address(0),
id,
value,
data
) == IERC1155TokenReceiver.onERC1155Received.selector,
"unsafe transfer"
);
}
}
function _batchMint(
address to,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) internal {
require(to != address(0), "to = 0 address");
require(ids.length == values.length, "ids length != values length");
for (uint256 i = 0; i < ids.length; i++) {
balanceOf[to][ids[i]] += values[i];
}
emit TransferBatch(msg.sender, address(0), to, ids, values);
if (to.code.length > 0) {
require(
IERC1155TokenReceiver(to).onERC1155BatchReceived(
msg.sender,
address(0),
ids,
values,
data
) == IERC1155TokenReceiver.onERC1155BatchReceived.selector,
"unsafe transfer"
);
}
}
function _burn(address from, uint256 id, uint256 value) internal {
require(from != address(0), "from = 0 address");
balanceOf[from][id] -= value;
emit TransferSingle(msg.sender, from, address(0), id, value);
}
function _batchBurn(
address from,
uint256[] calldata ids,
uint256[] calldata values
) internal {
require(from != address(0), "from = 0 address");
require(ids.length == values.length, "ids length != values length");
for (uint256 i = 0; i < ids.length; i++) {
balanceOf[from][ids[i]] -= values[i];
}
emit TransferBatch(msg.sender, from, address(0), ids, values);
}
}
contract MyMultiToken is ERC1155 {
function mint(uint256 id, uint256 value, bytes memory data) external {
_mint(msg.sender, id, value, data);
}
function batchMint(
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external {
_batchMint(msg.sender, ids, values, data);
}
function burn(uint256 id, uint256 value) external {
_burn(msg.sender, id, value);
}
function batchBurn(uint256[] calldata ids, uint256[] calldata values) external {
_batchBurn(msg.sender, ids, values);
}
}
ERC-1155는 기존의 ERC-20과 ERC-721 (Non-Fungible Token, NFT)에 대한 표준을 보완하고 확장한 토큰 표준이다.
ERC-1155의 주요 특징은 다음과 같다.
ERC-1155는 주로 게임 및 디지털 아트와 같은 분야에서 활용되며, 다양한 토큰을 한 컨트랙트 내에서 관리하려는 경우에 매우 유용하게 사용할 수 있다.
해당 예제 코드에서 확인할 수 있는 Mint 함수를 살펴보면, 고유한 토큰 ID, 수량 (value), data를 입력으로 받는다. ERC-1155에서는 ERC-20과 ERC-721을 모두 한 컨트랙트에 저장하는 다중 토큰 표준이기 때문에, Mint 함수로 ERC-20과 ERC-721 발행의 차이를 살펴본다.
먼저, 해당 예제 코드에서 ERC-20과 ERC-721의 차이를 위해 다음과 같이 정리했다.
따라서, ERC-20은 Mint(1, 1000000, 0x0001)와 같이 발행할 수 있고 ERC-721은 Mint(2, 1, 0xaabb)와 같이 해당 ID에 대한 수량을 1로 제한하여 고유성을 보장할 수 있다.
Mint 함수 인자 Data의 타입은 Bytes로 선언되어 있는데, Bytes 타입은 Ethereum의 데이터 표현 방식에 의해 고정된 크기의 바이트 배열을 의미한다. 이 때, 각 바이트는 8비트로 구성되며, 16진수에서는 2자리료 표현된다.
따라서, 1을 bytes로 입력하는 경우 0x1 을 입력하면 오류가 발생하기 때문에 0x01과 같이 완전한 바이트 단위로 제공해야 한다.
토큰 생성 Mint 함수 사용 예시
ERC-1155에서 지원하는 다중 토큰 생성, BatchMint 함수 사용 예시
추후에 NFT는 IPFS에 이미지를 업로드하고, 고유 CID를 NFT에 저장하는 기능을 통해 NFT Market 프로젝트에서 다룰 예정입니다.
참고자료: https://ethereum.org/ko/developers/docs/standards/tokens/erc-1155/