Sample ERC1155 Contract
This is an example of an ERC-1155 smart contract, structured to support minting and transferring either single or multiple types of tokens, each identified by a unique ID.
This example is adapted from OpenZeppelin ERC1155.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "@openzeppelin/contracts/access/Ownable.sol"; // Ownership control
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Utils.sol"; // Safe transfer verification utils
import "@openzeppelin/contracts/utils/Strings.sol"; // String conversion utils
import "@openzeppelin/contracts/utils/Arrays.sol"; // Array utils (includes unsafeMemoryAccess)
/**
* @title Sample1155Token
* @dev A custom ERC1155 implementation with minimal features and custom error handling
*/
contract Sample1155Token is Ownable {
using Strings for uint256;
using Arrays for uint256[];
using Arrays for address[];
// ----------------------
// ░░░ Error Definitions ░░░
// ----------------------
error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); // Insufficient balance for transfer or burn
error ERC1155InvalidSender(address sender); // Invalid sender (e.g., zero address)
error ERC1155InvalidReceiver(address receiver); // Invalid receiver (e.g., zero address)
error ERC1155MissingApprovalForAll(address operator, address owner); // Unauthorized attempt to operate on another's tokens
error ERC1155InvalidApprover(address approver); // Address cannot be set as approver
error ERC1155InvalidOperator(address operator); // Address cannot be set as operator
error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); // Length of ID array and amount array do not match
// ----------------------
// ░░░ Events ░░░
// ----------------------
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); // Single token transfer event
event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values); // Batch token transfer event
event ApprovalForAll(address indexed account, address indexed operator, bool approved); // Operator approval or revocation event
// ----------------------
// ░░░ Metadata ░░░
// ----------------------
string public name; // Collection name (e.g., "MyGameItems")
string public symbol; // Collection symbol (e.g., "MGI")
string private _baseURI = ""; // Base prefix path for metadata URI
// ----------------------
// ░░░ Token Storage ░░░
// ----------------------
mapping(uint256 id => uint256) private _totalSupply; // Per-token supply
uint256 private _totalSupplyAll; // Total supply of all tokens
mapping(uint256 id => mapping(address => uint256)) private _balances; // User balances
mapping(address => mapping(address => bool)) private _operatorApprovals; // Operator approvals
// ----------------------
// ░░░ Constructor ░░░
// ----------------------
/// @notice Initializes the contract with a base metadata URI
/// @param uri_ The base URI string used for all token metadata
constructor(string memory uri_) Ownable(_msgSender()) {
_setURI(uri_);
}
// ----------------------
// ░░░ Read Functions ░░░
// ----------------------
/// @notice Returns the balance of a specific token ID for a given account
/// @param account The address to query
/// @param id The token ID to query
/// @return The amount of tokens owned by the account
function balanceOf(address account, uint256 id) public view returns (uint256) {
return _balances[id][account];
}
/// @notice Returns the balances of multiple token IDs for multiple accounts
/// @param accounts Array of addresses to query
/// @param ids Array of token IDs to query (must match accounts array length)
/// @return An array containing the balances of each (account, token ID) pair
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory) {
if (accounts.length != ids.length) revert ERC1155InvalidArrayLength(ids.length, accounts.length);
uint256[] memory batchBalances = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; ++i) {
batchBalances[i] = balanceOf(accounts.unsafeMemoryAccess(i), ids.unsafeMemoryAccess(i));
}
return batchBalances;
}
/// @notice Returns the total minted supply of a specific token ID
/// @param id The token ID to query
/// @return The total number of tokens in existence for the given ID
function totalSupply(uint256 id) external view returns (uint256) {
return _totalSupply[id];
}
/// @notice Returns the total supply across all token IDs
/// @return The aggregate number of tokens minted (excluding burns)
function totalSupply() external view returns (uint256) {
return _totalSupplyAll;
}
/// @notice Returns whether a specific token ID exists (i.e., has non-zero supply)
/// @param id The token ID to check
/// @return True if the token exists, false otherwise
function exists(uint256 id) external view returns (bool) {
return _totalSupply[id] > 0;
}
/// @notice Returns the metadata URI for a specific token ID
/// @param id The token ID to query
/// @return The metadata URI derived from baseURI + hex-encoded token ID
function uri(uint256 id) public view returns (string memory) {
return string(abi.encodePacked(_baseURI, Strings.toHexString(id, 32)));
}
/// @notice Indicates which interfaces the contract supports (ERC165, ERC1155, ERC1155MetadataURI)
/// @param interfaceId The interface identifier to check
/// @return True if supported, false otherwise
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return interfaceId == 0x01ffc9a7 || interfaceId == 0xd9b67a26 || interfaceId == 0x0e89341c;
}
// ----------------------
// ░░░ Approval Logic ░░░
// ----------------------
/// @notice Grants or revokes operator approval for all of the caller's tokens
/// @param operator The address to grant or revoke approval for
/// @param approved True to approve, false to revoke
function setApprovalForAll(address operator, bool approved) external {
_setApprovalForAll(_msgSender(), operator, approved);
}
/// @notice Checks whether an operator is approved to manage all tokens of a given account
/// @param account The owner of the tokens
/// @param operator The address to check for approval
/// @return True if approved, false otherwise
function isApprovedForAll(address account, address operator) public view returns (bool) {
return _operatorApprovals[account][operator];
}
/// @dev Internal function to update approval status of an operator for a given owner
/// @param owner The owner granting the approval
/// @param operator The operator address
/// @param approved True to approve, false to revoke
function _setApprovalForAll(address owner, address operator, bool approved) internal {
if (operator == address(0)) revert ERC1155InvalidOperator(address(0));
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
// ----------------------
// ░░░ Transfer Logic ░░░
// ----------------------
/// @notice Transfers a single token ID from one address to another
/// @dev Caller must be the token owner or an approved operator
/// @param from Source address
/// @param to Target address
/// @param id Token ID to transfer
/// @param value Amount of tokens to transfer
/// @param data Additional data with no specified format
function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) external {
address sender = _msgSender();
if (from != sender && !isApprovedForAll(from, sender)) revert ERC1155MissingApprovalForAll(sender, from);
_safeTransferFrom(from, to, id, value, data);
}
/// @notice Transfers multiple token IDs from one address to another
/// @dev Caller must be the token owner or an approved operator
/// @param from Source address
/// @param to Target address
/// @param ids List of token IDs to transfer
/// @param values List of token amounts to transfer (must match ids array length)
/// @param data Additional data with no specified format
function safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data) external {
address sender = _msgSender();
if (from != sender && !isApprovedForAll(from, sender)) revert ERC1155MissingApprovalForAll(sender, from);
_safeBatchTransferFrom(from, to, ids, values, data);
}
/// @dev Internal function to handle single token transfer with receiver check
/// @param from Source address
/// @param to Target address
/// @param id Token ID to transfer
/// @param value Amount of tokens to transfer
/// @param data Additional data to pass to the receiver
function _safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) internal {
if (to == address(0)) revert ERC1155InvalidReceiver(address(0));
if (from == address(0)) revert ERC1155InvalidSender(address(0));
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
_updateWithAcceptanceCheck(from, to, ids, values, data);
}
/// @dev Internal function to handle batch token transfer with receiver check
/// @param from Source address
/// @param to Target address
/// @param ids List of token IDs to transfer
/// @param values List of token amounts to transfer
/// @param data Additional data to pass to the receiver
function _safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
if (to == address(0)) revert ERC1155InvalidReceiver(address(0));
if (from == address(0)) revert ERC1155InvalidSender(address(0));
_updateWithAcceptanceCheck(from, to, ids, values, data);
}
// ----------------------
// ░░░ Mint/Burn (Owner-only) ░░░
// ----------------------
/// @notice Mints a specified amount of a token to a given address
/// @dev Only the contract owner can call this function
/// @param account The recipient of the minted tokens
/// @param id The token ID to mint
/// @param amount The number of tokens to mint
/// @param data Additional data to pass to receiver (if contract)
function mint(address account, uint256 id, uint256 amount, bytes memory data) external onlyOwner {
_mint(account, id, amount, data);
}
/// @notice Mints multiple token IDs and amounts to a given address
/// @dev Only the contract owner can call this function
/// @param to The recipient of the minted tokens
/// @param ids Array of token IDs to mint
/// @param amounts Array of amounts to mint (must match ids length)
/// @param data Additional data to pass to receiver (if contract)
function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) external onlyOwner {
_mintBatch(to, ids, amounts, data);
}
/// @notice Burns a specific amount of a token from an account
/// @dev Caller must be the token owner or approved operator
/// @param account The address from which the token will be burned
/// @param id The token ID to burn
/// @param value The amount of tokens to burn
function burn(address account, uint256 id, uint256 value) external {
if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) {
revert ERC1155MissingApprovalForAll(_msgSender(), account);
}
_burn(account, id, value);
}
/// @notice Burns multiple token IDs and amounts from an account
/// @dev Caller must be the token owner or approved operator
/// @param account The address from which the tokens will be burned
/// @param ids Array of token IDs to burn
/// @param values Array of amounts to burn (must match ids length)
function burnBatch(address account, uint256[] memory ids, uint256[] memory values) external {
if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) {
revert ERC1155MissingApprovalForAll(_msgSender(), account);
}
_burnBatch(account, ids, values);
}
/// @dev Internal minting logic for a single token ID
/// @param to The recipient address
/// @param id The token ID to mint
/// @param value The amount of tokens to mint
/// @param data Additional data for receiver checks
function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
if (to == address(0)) revert ERC1155InvalidReceiver(address(0));
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
_updateWithAcceptanceCheck(address(0), to, ids, values, data);
}
/// @dev Internal minting logic for multiple token IDs
/// @param to The recipient address
/// @param ids Array of token IDs to mint
/// @param values Array of amounts to mint
/// @param data Additional data for receiver checks
function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
if (to == address(0)) revert ERC1155InvalidReceiver(address(0));
_updateWithAcceptanceCheck(address(0), to, ids, values, data);
}
/// @dev Internal burning logic for a single token ID
/// @param from The address from which to burn tokens
/// @param id The token ID to burn
/// @param value The amount of tokens to burn
function _burn(address from, uint256 id, uint256 value) internal {
if (from == address(0)) revert ERC1155InvalidSender(address(0));
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
_updateWithAcceptanceCheck(from, address(0), ids, values, "");
}
/// @dev Internal burning logic for multiple token IDs
/// @param from The address from which to burn tokens
/// @param ids Array of token IDs to burn
/// @param values Array of amounts to burn
function _burnBatch(address from, uint256[] memory ids, uint256[] memory values) internal {
if (from == address(0)) revert ERC1155InvalidSender(address(0));
_updateWithAcceptanceCheck(from, address(0), ids, values, "");
}
// ----------------------
// ░░░ Internal Helpers ░░░
// ----------------------
/// @dev Handles core transfer/mint/burn logic and verifies receiver acceptance if applicable
/// @param from Source address (zero address for mint)
/// @param to Target address (zero address for burn)
/// @param ids Array of token IDs
/// @param values Array of token amounts
/// @param data Additional data forwarded to receiving contract (if any)
function _updateWithAcceptanceCheck(
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) internal {
_update(from, to, ids, values);
if (to != address(0)) {
address operator = _msgSender();
if (ids.length == 1) {
// Check acceptance of single token transfer
ERC1155Utils.checkOnERC1155Received(operator, from, to, ids[0], values[0], data);
} else {
// Check acceptance of batch transfer
ERC1155Utils.checkOnERC1155BatchReceived(operator, from, to, ids, values, data);
}
}
}
/// @dev Updates token balances, emits events, and tracks total supply changes
/// @param from Address to subtract balance from (zero for mint)
/// @param to Address to add balance to (zero for burn)
/// @param ids Array of token IDs
/// @param values Array of token amounts
function _update(
address from,
address to,
uint256[] memory ids,
uint256[] memory values
) internal {
if (ids.length != values.length) revert ERC1155InvalidArrayLength(ids.length, values.length);
address operator = _msgSender();
// Update balances
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids.unsafeMemoryAccess(i);
uint256 value = values.unsafeMemoryAccess(i);
if (from != address(0)) {
uint256 fromBalance = _balances[id][from];
if (fromBalance < value) revert ERC1155InsufficientBalance(from, fromBalance, value, id);
unchecked {
_balances[id][from] = fromBalance - value;
}
}
if (to != address(0)) {
_balances[id][to] += value;
}
}
// Emit appropriate transfer event
if (ids.length == 1) {
emit TransferSingle(operator, from, to, ids[0], values[0]);
} else {
emit TransferBatch(operator, from, to, ids, values);
}
// Mint: update totalSupply and totalSupplyAll
if (from == address(0)) {
uint256 totalMintValue = 0;
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids.unsafeMemoryAccess(i);
uint256 value = values.unsafeMemoryAccess(i);
_totalSupply[id] += value;
totalMintValue += value;
}
_totalSupplyAll += totalMintValue;
}
// Burn: decrease totalSupply and totalSupplyAll
if (to == address(0)) {
uint256 totalBurnValue = 0;
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids.unsafeMemoryAccess(i);
uint256 value = values.unsafeMemoryAccess(i);
unchecked {
_totalSupply[id] -= value;
totalBurnValue += value;
}
}
unchecked {
_totalSupplyAll -= totalBurnValue;
}
}
}
/// @dev Helper to wrap two uint256 values into single-element arrays
/// @param element1 The first value (used as token ID)
/// @param element2 The second value (used as amount)
/// @return array1 Single-element array containing element1
/// @return array2 Single-element array containing element2
function _asSingletonArrays(
uint256 element1,
uint256 element2
) private pure returns (uint256[] memory array1, uint256[] memory array2) {
assembly ("memory-safe") {
// Create array1 at free memory pointer
array1 := mload(0x40)
mstore(array1, 1) // length = 1
mstore(add(array1, 0x20), element1)
// Create array2 right after array1
array2 := add(array1, 0x40)
mstore(array2, 1)
mstore(add(array2, 0x20), element2)
// Advance free memory pointer
mstore(0x40, add(array2, 0x40))
}
}
}
Updated 14 days ago