ERC 토큰 알아보기 (ERC20, ERC721, ERC1155)

EIP란

EIP(Ethereum Improvement Proposal)은 ‘이더리움 개선 제안’이라는 뜻으로 이더리움 커뮤니티에 정보를 제공하거나 프로세스 환경에 대한 새로운 기능을 설명하는 설계 문서로 생각할 수 있다.

EIP 뒤에 숫자는 제안된 번호를 뜻한다.

ERC란

ERC(Ethereum Request for Comment)는 이더리움 블록체인 네트워크에서 발생되는 token 표준을 듯한다.

즉, ERC는 EIP에서 관리하는 공식 프로토콜로 이더리움 생태계에서 애플리케이션과 컨트랙트를 쉽게 상호작용할 수 있도록 규약을 만드는 것을 목표로한다.



ERC-20란

ERC-20 토큰은 대체 가능하며, 각 단위는 상호 호환한다.

대체 가능 토큰이란, 자산의 해당 단위과 서로 교환할 수 있는 경우 즉, 구별할 수 없는 경우 대체 가능한 것으로 간주된다.

자산의 각 단위가 동릴한 유효성과 시장 가치를 가질 때 자산 클래스는 대체 가능하게 된다.

이더리움 자체 암호화폐인 이더(ETH)와 달리, ERC-20토큰은 계정에 보관 되지 않는다.

토큰은 컨트랙트 내부에만 존재하고, 독집적인 데이터베이스와 같다.

컨트랙트에 토큰 규칙(ex 이름, 심볼, 가분성) 들을 구체적으로 명시하며 사용자 계정에 상응하는 목록을 이더리움 주소에 보관한다.

토큰을 이동하려면 사용자는 자신의 잔고 일부를 다른 곳에로 전달 요청하는 트랜잭션을 컨트랙트에 전송해서 컨트랙트 내부의 기능을 호출하여 이를 요청한다.

ERC-20 표준 기능

totalSupply

function totalSupply() public view returns (uint256)

이를 호출하면, 컨트랙트가 보유하고 있는 토큰의 전체 공급량 전달

BalanceOf

function balanceOf(address _owner) public view returns (uint256 balance)

totalSupply와 달리 balanceOf는 매개 변수(주소)를 파라미터로 받고 함수가 호출되면 해당 주소의 토큰 잔고가 출력

이더리움 네트워크의 계정은 공개적이기 때문에 주소를 알면 모든 사용자가 해당 잔고를 요청할 수 있다.

transfer

function transfer(address _to, uint256 _value) public returns (bool success)

transfer는 다른 사용자에게 토큰을 전송하는 기능으로 파라미터로 전송하고자 하는 주소와 금액을 제공해야한다.

transferFrom

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)

transferFrom함수는 transfer와 마찬가지로 토큰을 전송하는데에 사용하지만 해당 토큰이 반드시 컨트랙트를 호출하는 이의 소유일 필요가 없다.

즉, 다른 누군가 또는 컨트랙트에서 전송하려는 사람을 대신하여 자금을 전송하도록 허락 할 수있다.

해당 함수는 transfer와 동일한 이벤트를 호출한다.

approve

function approve(address _spender, uint256 _value) public returns (bool success)

이 함수를 사용해서 컨트랙트가 잔고에서 인출할 수 있는 토큰 수량을 제안할 수 있다.

이 함수가 없으면 함수 오작동(또는 부정한 이용)과 자금 탈취의 위험이 있다.

allwance

function allowance(address _owner, address _spender) public view returns (uint256 remaining)

allwanceapprove와 함계 사용할 수 있다.

특정 컨트랙트에 토큰 관리 기능을 부여했다면, 이를 통해 얼마나 많은 토큰을 인출 할 수 있는지 확인하는데에 사용한다.

예를 들어, 승인된 20개의 토큰중 12개를 사용했다면 allowance함수를 호출 시 8개 토큰을 사용할 수 있다는 메시지가 출력된다.

부가적인 함수

앞의 함수들은 필수 적인 함수지만, name, symbol, decimal을 반드시 포함하지 않아도 되지만 이를 추가할 경우 다른 사람들이 읽을 수 있는 이름과, 심볼, 소수점 몇자리 까지 토큰이 불할 될 수 있는지를 지정할 수 있다.

예를 들면, 통화로 사용되는 토큰은 소유권을 나타내는 토큰보다 더 많이 분할 되는게 좋다.

ERC20 전체 코드

pragma solidity ^0.8.0;

import "./IERC20.sol";
import "./extensions/IERC20Metadata.sol";
import "../../utils/Context.sol";

contract ERC20 is Context, IERC20, IERC20Metadata {
		// 주소당 토큰 수량 {주소 : 수량}
    mapping(address => uint256) private _balances;
		// approve 저장 {owner :{spender : amount }}
    mapping(address => mapping(address => uint256)) private _allowances;
		// 전체 토큰량
    uint256 private _totalSupply;

		// token 명 및 심볼
    string private _name;
    string private _symbol;

    constructor(string memory name_, string memory symbol_) {
				// token 명 및 심볼 정의
        _name = name_;
        _symbol = symbol_;
    }

		// 토큰명 반환
    function name() public view virtual override returns (string memory) {
        return _name;
    }

		// 토큰 심볼 반환
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

		// token 단위
    function decimals() public view virtual override returns (uint8) {
        return 18;
    }

		// 토큰 수량 반환
    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }

		// 주소값 받아서 해당 주소의 토큰 수량 반환
    function balanceOf(address account) public view virtual override returns (uint256) {
        return _balances[account];
    }

		// 토큰 전송
    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

		// 토큰 소유자(owner)를 대신하여 spender가 지출할 수 있는 토큰 수를 반환
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

		// spender가 사용할 수 있는 토큰의 양을 amount로 설정
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

		// from이 to 에게 approve된 amount 만큼 토큰 전송
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

		// 호출자가 spender에게 부여한 허용량을 원자적으로 증가시킵니다.
		// 기존에 approve 보완
		// -> approve로 수당을 변경할 경우 거래 순서로 인해서 누군가가 이전 수당과 새로운 수당을 모두 사용할 수 있는 위험이 있다.
		// -> 그렇기 때문에 spender의 수량을 0으로 줄이고 나중에 원하는 값을 설정한다.
    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, allowance(owner, spender) + addedValue);
        return true;
    }

		// 호출자가 spender에게 부여한 허용량을 원자적으로 감소시킨다.
    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        address owner = _msgSender();
        uint256 currentAllowance = allowance(owner, spender);
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");

				// 저장용량의 한계로 인해 발생할 수 잇는 overflow를 체크않함 (가스비 절감용)
				// unchecked : 큰수 - 작은수 할때 무조건 양수이기 때문엔 unchecked 사용
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }

        return true;
    }
		/** increaseAllowance & decreaseAllowance example
		 * A가 B에게 100토큰을 사용하도록 approve
		 * 나중에 A가 B에게 50토큰을 사용하도록 approve시, A는 두번 전송
		 * -> B가 이 두 트랜잭션으로 100토큰을 더 사용하도록 만들 수 있다.
		 * A -> B 100 approve
		 * A -> B 100 transfer
		 * A -> B 50 approve
		 * A -> B 50 transfer
		 * 따라서 A는 B에게 50만큼 전송 승인하기를 원하지만 B는 150 토큰을 얻음
 		 */

		// from이 to에게 amount만큼 전송
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

			  // from의 토큰 수가 transfer 요청 수량보다 클 경우만 실행
        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
            _balances[to] += amount;
        }

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }

		// account에게 amount 토큰 주조
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

				// 전체 토큰량 증가
        _totalSupply += amount;
        unchecked {
            _balances[account] += amount;
        }
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

		// account의 토큰을 amount만큼 소각
    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");

        _beforeTokenTransfer(account, address(0), amount);

				// account의 토큰 amount만큼 수량 감소 및 전체 수량 감소
        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
            _totalSupply -= amount;
        }

        emit Transfer(account, address(0), amount);

        _afterTokenTransfer(account, address(0), amount);
    }

	  // owner가 spender에게 amount만큼 전송 할 수 있도록 승인
    function _approve(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

		// owner가 spender에게 사용할 수 있게 approve한 수량 확인
    function _spendAllowance(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
						// approve된 토큰의 수량 이하로 사용시에만 실행
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
								// 사용한 수량 만큼 approve만큼 amount 제외
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

		// 재정의용
		// 전송 기능 전,후로 추가로 실행할 용도
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}

    function _afterTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}
}


ERC-721란

ERC-721은 NFT(Non-Fungible Token)으로 대체가 불가능한 토큰이다.

NFT는 블록체인 기술을 이용하여 고유의 일련번호를 부여하여 대체 불가능한 컨텐츠로 만들어 주어서 희소성을 가진 고유한 자산으로 인식된다.

ERC-20 표준 기능

IERC721

balanceOf

function balanceOf(address _owner) external view returns (uint256);

호출시 주소를 파라미터로 받아서 해당 주소가 보유한 NFT 토큰의 개수를 반환한다.

ownerOf

function ownerOf(uint256 _tokenId) external view returns (address);

호출시 토큰 고유 번호를 파라미터로 받아서 해당 토큰을 소유하고 있는 주소를 반환한다.

transferFrom

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

transferFrom함수는 transfer와 마찬가지로 토큰을 전송하는데에 사용하지만 해당 토큰이 반드시 컨트랙트를 호출하는 이의 소유일 필요가 없다.

즉, 다른 누군가 또는 컨트랙트에서 전송하려는 사람을 대신하여 자금을 전송하도록 허락 할 수있다.

safeTransferFrom

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

NFT 토큰 소유자로부터 해당 NFT 토큰을 다른 주소로 전송한다.

safeTransferFrom

function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

전송받은 to 주소가 토큰을 받을 수 있는지 확인하고 전송한다.

approve

function approve(address _approved, uint256 _tokenId) external payable;

이 함수를 사용해서 컨트랙트가 잔고에서 인출할 수 있는 토큰 수량을 제안할 수 있다.

getApproved

function getApproved(uint256 _tokenId) external view returns (address);

호출시 토큰 고유 번호를 파라미터를 받아서 해당 토큰의 전송 권한을 가지고 있는 주소를 반환한다.

setApprovalForAll

function setApprovalForAll(address _operator, bool _approved) external;

NFT 토큰 소유자가 해당 주소에게 모든 NFT 토큰에 대한 권한을 부여한다.

operator로 주소, _approved로 true값을 부여 할 경우 해당 주소에 권한을 부여한다.

isApprovedForAll

function isApprovedForAll(address _owner, address _operator) external view returns (bool);

위의 setApprovalForAll 권한이 참/거짓 인지를 반환하다.

ERC721TokenReceiver

onERC721Received

function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);

ERC721Metadata

ERC721에는 선택 사항으로 스마트 계약의 이름과 NFT가 나타내는 자산에 대한 세부 정보를 사용할 수 있습니다.

name

function name() external view returns (string _name);

symbol

function symbol() external view returns (string _symbol);

tokenURI

function tokenURI(uint256 _tokenId) external view returns (string);

NFT의 고유 고유 번호마다 고유한 URI를 리턴하는 함수입니다.

ERC721 전체 코드

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/ERC721.sol)

pragma solidity ^0.8.0;

import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./extensions/IERC721Metadata.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/Strings.sol";
import "../../utils/introspection/ERC165.sol";

contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
    using Address for address;
    using Strings for uint256;

    // 토큰명
    string private _name;

    // 토큰 심볼
    string private _symbol;

    // 토큰ID당 소유 주소 {토큰 ID : 소유 주소}
    mapping(uint256 => address) private _owners;

    // 주소당 토큰 개수 {토큰을 소유한 주소 : 토큰 개수}
    mapping(address => uint256) private _balances;

    // 토큰 Id에서 approve된 주소 {토큰ID : approve된 주소}
    mapping(uint256 => address) private _tokenApprovals;

		// approve 값
    mapping(address => mapping(address => bool)) private _operatorApprovals;

		// contract 생성시 name, symbol정의
    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

		// interface(=메서드)가 있는지 확인한다.
		// 함수를 bytes4 형으로 변환하여 매핑해두고 해당 값으로 IERC721 값들이 있는지 확인한다.
    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
        return
            interfaceId == type(IERC721).interfaceId ||
            interfaceId == type(IERC721Metadata).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    // 파라미터로 주소를 받아서 해당 주소의 토큰 수량 반환
    function balanceOf(address owner) public view virtual override returns (uint256) {
        require(owner != address(0), "ERC721: address zero is not a valid owner");
        return _balances[owner];
    }

		// token 코유 번호를 받아서 해당 소유주의 주소 반환
    function ownerOf(uint256 tokenId) public view virtual override returns (address) {
        address owner = _ownerOf(tokenId);
        require(owner != address(0), "ERC721: invalid token ID");
        return owner;
    }

		// 토큰 명 반환
    function name() public view virtual override returns (string memory) {
        return _name;
    }

		// 토큰 심볼 반환
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

		// token ID값을 받아서 해당 토큰의 메타데이터 uri반환
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        _requireMinted(tokenId);

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
    }

		// token의 메타데이터에 기본이 되는 uri
    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }

		// 주소 to의 tokenId를 사용할 수 있도록 승인
    function approve(address to, uint256 tokenId) public virtual override {
        address owner = ERC721.ownerOf(tokenId);
				// to가 tokenID의 소유자가 아닐 경우 거부
        require(to != owner, "ERC721: approval to current owner");

        require(
				// 해당 메서드 실행자가 tokenID의 소유자가 아닐 경우 거부
            _msgSender() == owner || isApprovedForAll(owner, _msgSender()),
            "ERC721: approve caller is not token owner or approved for all"
        );

        _approve(to, tokenId);
    }

		// 파라미터로 받은 tokenID가 approve되어 있는지 확인
    function getApproved(uint256 tokenId) public view virtual override returns (address) {
        _requireMinted(tokenId);

        return _tokenApprovals[tokenId];
    }

		// 파라미터 operator
    function setApprovalForAll(address operator, bool approved) public virtual override {
        _setApprovalForAll(_msgSender(), operator, approved);
    }

		// owner의 토큰을 operator가 사용할 수 있도록 approve확인
    function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[owner][operator];
    }

		// 파라미터로 받은 from의 tokenID번째 토큰를 to에게 전송
		// from은 tokenID를 전송할 수 있도록 approve 되어 있어야함
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
				// approve 확인
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
        _transfer(from, to, tokenId);
    }


    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        safeTransferFrom(from, to, tokenId, "");
    }


    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) public virtual override {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
        _safeTransfer(from, to, tokenId, data);
    }
}


ERC-1155란

ERC-20과 ERC-721의 각 특징을 조합

거래소 없이도 서로 다른 코인의 교환을 가능하게 하는 Atomic Swap 기술을 사용했고, ERC-20과 ERC-721에 비해 적은 트랜잭션을 통한 자산 교환이 가능하다.

한번의 트랜잭션으로 한명의 수신자에게 원하는 수량만큼의 아이템을 보낼 수 있는 멀티 전송 기능을 제공하기 때문에 전송 가스 요금이 줄어드는 장점이 있다.

ERC-1155 표준 기능

IERC-1155

safeTransferFrom

function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;

from 에서 to로 토큰의 id와 일치하는 번호를 value만큼 전송

safeBatchTransferFrom

function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;

일괄 전송 id 값을 array로 받아서 해당 토큰 번호에 대한 각각 수량을 한번의 트랜잭션으로 일괄 전송 (ex safeBatchTransferFrom(deployerAddress, playerAddress, [0,1,3,4], [50,100,1,1], "0x0"))

balanceOf

function balanceOf(address _owner, uint256 _id) external view returns (uint256);

owner가 소유한 id의 토큰의 수량을 반환

balanceOfBatch

function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory);

사용자별 각 토큰에 대해 일괄적으로 수량을 반환

(ex balanceOfBatch([playerAddress,playerAddress,playerAddress,playerAddress,playerAddress], [0,1,2,3,4]) [50,100,1,1,1] )

setApprovalForAll

function setApprovalForAll(address _operator, bool _approved) external;

NFT 토큰 소유자가 해당 주소에게 모든 NFT 토큰에 대한 권한을 부여한다.

isApprovedForAll

function isApprovedForAll(address _owner, address _operator) external view returns (bool);

위의 setApprovalForAll 권한이 참/거짓 인지를 반환하다.

ERC1155 전체 코드

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC1155/ERC1155.sol)

pragma solidity ^0.8.0;

import "./IERC1155.sol";
import "./IERC1155Receiver.sol";
import "./extensions/IERC1155MetadataURI.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/introspection/ERC165.sol";

contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI {
    using Address for address;

  	// 소유자가 가지고 있는 tokenID의 해당 수량 매핑 { tokenID : { 소유 주소 : 수량 } }
    mapping(uint256 => mapping(address => uint256)) private _balances;

		// approve 매핑 { 주소A : { 주소B : approve여부 } }
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    // token 메타데이터 uri  e.g. https://token-cdn-domain/{id}.json
    string private _uri;

		// 토큰 메타데이터 uri
    constructor(string memory uri_) {
        _setURI(uri_);
    }

		// interface(=메서드)가 있는지 확인한다.
		// 함수를 bytes4 형으로 변환하여 매핑해두고 해당 값으로 IERC1155 값들이 있는지 확인한다.
    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
        return
            interfaceId == type(IERC1155).interfaceId ||
            interfaceId == type(IERC1155MetadataURI).interfaceId ||
            super.supportsInterface(interfaceId);
    }

		// 토큰 메타데이터 uri 반환
    function uri(uint256) public view virtual override returns (string memory) {
        return _uri;
    }

		// account의 토큰 id번의 수량 반환
    function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
        require(account != address(0), "ERC1155: address zero is not a valid owner");
        return _balances[id][account];
    }

		// address와 토큰 id를 일괄로 받아서 단일 트랜잭션으로 여러 잔액을 반환
    function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
        public
        view
        virtual
        override
        returns (uint256[] memory)
    {
				// 주소와 토큰번호가 일치할 경우에만 실행
        require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");

        uint256[] memory batchBalances = new uint256[](accounts.length);

        for (uint256 i = 0; i < accounts.length; ++i) {
            batchBalances[i] = balanceOf(accounts[i], ids[i]);
        }

        return batchBalances;
    }

		// operator가 해당 메서드를 호출한 주소의 토큰을 approve
    function setApprovalForAll(address operator, bool approved) public virtual override {
        _setApprovalForAll(_msgSender(), operator, approved);
    }

		// account 주소가 operator approve되어 있는지 boolean값 반환
    function isApprovedForAll(address account, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[account][operator];
    }

		//
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) public virtual override {
        require(
            from == _msgSender() || isApprovedForAll(from, _msgSender()),
            "ERC1155: caller is not token owner or approved"
        );
        _safeTransferFrom(from, to, id, amount, data);
    }


    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public virtual override {
        require(
            from == _msgSender() || isApprovedForAll(from, _msgSender()),
            "ERC1155: caller is not token owner or approved"
        );
        _safeBatchTransferFrom(from, to, ids, amounts, data);
    }
    function _safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) internal virtual {
        require(to != address(0), "ERC1155: transfer to the zero address");

        address operator = _msgSender();
        uint256[] memory ids = _asSingletonArray(id);
        uint256[] memory amounts = _asSingletonArray(amount);

        _beforeTokenTransfer(operator, from, to, ids, amounts, data);

        uint256 fromBalance = _balances[id][from];
        require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
        unchecked {
            _balances[id][from] = fromBalance - amount;
        }
        _balances[id][to] += amount;

        emit TransferSingle(operator, from, to, id, amount);

        _afterTokenTransfer(operator, from, to, ids, amounts, data);

        _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
    }


    function _safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual {
        require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
        require(to != address(0), "ERC1155: transfer to the zero address");

        address operator = _msgSender();

        _beforeTokenTransfer(operator, from, to, ids, amounts, data);

        for (uint256 i = 0; i < ids.length; ++i) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];

            uint256 fromBalance = _balances[id][from];
            require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
            unchecked {
                _balances[id][from] = fromBalance - amount;
            }
            _balances[id][to] += amount;
        }

        emit TransferBatch(operator, from, to, ids, amounts);

        _afterTokenTransfer(operator, from, to, ids, amounts, data);

        _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);
    }


    function _setURI(string memory newuri) internal virtual {
        _uri = newuri;
    }

    function _mint(
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) internal virtual {
        require(to != address(0), "ERC1155: mint to the zero address");

        address operator = _msgSender();
        uint256[] memory ids = _asSingletonArray(id);
        uint256[] memory amounts = _asSingletonArray(amount);

        _beforeTokenTransfer(operator, address(0), to, ids, amounts, data);

        _balances[id][to] += amount;
        emit TransferSingle(operator, address(0), to, id, amount);

        _afterTokenTransfer(operator, address(0), to, ids, amounts, data);

        _doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
    }

    function _mintBatch(
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual {
        require(to != address(0), "ERC1155: mint to the zero address");
        require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");

        address operator = _msgSender();

        _beforeTokenTransfer(operator, address(0), to, ids, amounts, data);

        for (uint256 i = 0; i < ids.length; i++) {
            _balances[ids[i]][to] += amounts[i];
        }

        emit TransferBatch(operator, address(0), to, ids, amounts);

        _afterTokenTransfer(operator, address(0), to, ids, amounts, data);

        _doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data);
    }


    function _burn(
        address from,
        uint256 id,
        uint256 amount
    ) internal virtual {
        require(from != address(0), "ERC1155: burn from the zero address");

        address operator = _msgSender();
        uint256[] memory ids = _asSingletonArray(id);
        uint256[] memory amounts = _asSingletonArray(amount);

        _beforeTokenTransfer(operator, from, address(0), ids, amounts, "");

        uint256 fromBalance = _balances[id][from];
        require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
        unchecked {
            _balances[id][from] = fromBalance - amount;
        }

        emit TransferSingle(operator, from, address(0), id, amount);

        _afterTokenTransfer(operator, from, address(0), ids, amounts, "");
    }

    function _burnBatch(
        address from,
        uint256[] memory ids,
        uint256[] memory amounts
    ) internal virtual {
        require(from != address(0), "ERC1155: burn from the zero address");
        require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");

        address operator = _msgSender();

        _beforeTokenTransfer(operator, from, address(0), ids, amounts, "");

        for (uint256 i = 0; i < ids.length; i++) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];

            uint256 fromBalance = _balances[id][from];
            require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
            unchecked {
                _balances[id][from] = fromBalance - amount;
            }
        }

        emit TransferBatch(operator, from, address(0), ids, amounts);

        _afterTokenTransfer(operator, from, address(0), ids, amounts, "");
    }

    function _setApprovalForAll(
        address owner,
        address operator,
        bool approved
    ) internal virtual {
        require(owner != operator, "ERC1155: setting approval status for self");
        _operatorApprovals[owner][operator] = approved;
        emit ApprovalForAll(owner, operator, approved);
    }


    function _beforeTokenTransfer(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual {}


    function _afterTokenTransfer(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual {}

    function _doSafeTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) private {
        if (to.isContract()) {
            try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
                if (response != IERC1155Receiver.onERC1155Received.selector) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non-ERC1155Receiver implementer");
            }
        }
    }

    function _doSafeBatchTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) private {
        if (to.isContract()) {
            try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns (
                bytes4 response
            ) {
                if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non-ERC1155Receiver implementer");
            }
        }
    }

    function _asSingletonArray(uint256 element) private pure returns (uint256[] memory) {
        uint256[] memory array = new uint256[](1);
        array[0] = element;

        return array;
    }
}