[Solidity] ERC-721, ERC-1155

jhcha·2023년 10월 3일
0

Solidity

목록 보기
17/17
post-thumbnail

ERC-721

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는 고유한 정보를 저장하는 토큰이기 때문에 같은 대체 불가능하다는 특징을 가지고 있다.

ERC-721 토큰 생성

mint 함수 인자 to에 토큰 owner address를 입력하고, id 값에 임의의 숫자로 토큰을 생성한다.

mint(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 1)
mint(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 777)

ERC-721 토큰 확인

balanceOf 함수를 사용해서 토큰 owner address를 입력해보면 아래와 같이 토큰 개수를 반환한다.
대체 가능 토큰, ERC-20 토큰과 동일하다.
하지만, ownerOf에서 토큰 생성 시 입력한 고유 id 값에 대한 토큰을 조회하면 해당 토큰을 식별하고, 식별한 토큰의 owner address를 반환하는 것을 확인할 수 있다.

  • 따라서, ERC-721 토큰은 ERC-20 토큰과 다르게 토큰이 가지고 있는 고유 정보에 따라 서로 다른 토큰 (대체 불가능 토큰)으로 기능하며, 토큰 가치가 달라지게 된다.

ERC-1155

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는 다양한 토큰 유형을 한 컨트랙트 안에 저장하기 때문에, 다양한 토큰을 다루는 애플리케이션에서 가스 비용을 절약할 수 있음.
  • 배치 - 여러 개의 토큰을 한 번의 트랜잭션으로 전송 가능하여 가스 비용을 줄여줌.
  • 기존 표준과의 호환성 - ERC-1155는 기존 ERC-20 및 ERC-721 함수와 유사한 함수를 제공하여, 개발자가 이전 표준과 호환되는 방식으로 새로운 토큰을 구현할 수 있도록 지원함.

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은 특정 토큰 ID에 대해 여러 개의 토큰을 발행하고 소유할 수 있음.
  • ERC-721은 고유한 특성을 가지기 때문에 특정 ID에 한 개의 토큰만 발행함

따라서, 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/

0개의 댓글