diff --git a/src/token/ERC1155/Approve/ERC1155ApproveFacet.sol b/src/token/ERC1155/Approve/ERC1155ApproveFacet.sol new file mode 100644 index 00000000..043d12e1 --- /dev/null +++ b/src/token/ERC1155/Approve/ERC1155ApproveFacet.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Approve Facet + * @notice Provides approval functionality for ERC-1155 tokens. + */ +contract ERC1155ApproveFacet { + /** + * @notice Error indicating the operator address is invalid. + * @param _operator Invalid operator address. + */ + error ERC1155InvalidOperator(address _operator); + + /** + * @notice Emitted when `account` grants or revokes permission to `operator` to transfer their tokens. + * @param _account The token owner granting/revoking approval. + * @param _operator The address being approved/revoked. + * @param _approved True if approval is granted, false if revoked. + */ + event ApprovalForAll(address indexed _account, address indexed _operator, bool _approved); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + + /** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ + struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; + } + + /** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ + function getStorage() internal pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Grants or revokes permission to `operator` to transfer the caller's tokens. + * @dev Emits an {ApprovalForAll} event. + * @param _operator The address to grant/revoke approval to. + * @param _approved True to approve, false to revoke. + */ + function setApprovalForAll(address _operator, bool _approved) external { + if (_operator == address(0)) { + revert ERC1155InvalidOperator(address(0)); + } + getStorage().isApprovedForAll[msg.sender][_operator] = _approved; + emit ApprovalForAll(msg.sender, _operator, _approved); + } + + /** + * @notice Exports the function selectors of the ERC1155ApproveFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC1155ApproveFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.setApprovalForAll.selector); + } +} diff --git a/src/token/ERC1155/Approve/ERC1155ApproveMod.sol b/src/token/ERC1155/Approve/ERC1155ApproveMod.sol new file mode 100644 index 00000000..4dc592ca --- /dev/null +++ b/src/token/ERC1155/Approve/ERC1155ApproveMod.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Approve Module + * @notice Provides internal approval functionality for ERC-1155 tokens. + */ + +/** + * @notice Error indicating the operator address is invalid. + * @param _operator Invalid operator address. + */ +error ERC1155InvalidOperator(address _operator); + +/** + * @notice Emitted when `account` grants or revokes permission to `operator` to transfer their tokens. + * @param _account The token owner granting/revoking approval. + * @param _operator The address being approved/revoked. + * @param _approved True if approval is granted, false if revoked. + */ +event ApprovalForAll(address indexed _account, address indexed _operator, bool _approved); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + +/** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ +struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; +} + +/** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ +function getStorage() pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Grants or revokes permission to `operator` to transfer the user's tokens. + * @dev Emits an {ApprovalForAll} event. + * @param _user The address of the token owner. + * @param _operator The address to grant/revoke approval to. + * @param _approved True to approve, false to revoke. + */ +function setApprovalForAll(address _user, address _operator, bool _approved) { + if (_operator == address(0)) { + revert ERC1155InvalidOperator(address(0)); + } + getStorage().isApprovedForAll[_user][_operator] = _approved; + emit ApprovalForAll(_user, _operator, _approved); +} diff --git a/src/token/ERC1155/Burn/ERC1155BurnFacet.sol b/src/token/ERC1155/Burn/ERC1155BurnFacet.sol new file mode 100644 index 00000000..29583513 --- /dev/null +++ b/src/token/ERC1155/Burn/ERC1155BurnFacet.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Burn Facet + * @notice Provides burn functionality for ERC-1155 tokens. + */ +contract ERC1155BurnFacet { + /** + * @notice Error indicating insufficient balance for a burn operation. + * @param _sender Address attempting the burn. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + * @param _tokenId The token ID involved. + */ + error ERC1155InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _tokenId); + + /** + * @notice Error indicating the sender address is invalid. + * @param _sender Invalid sender address. + */ + error ERC1155InvalidSender(address _sender); + + /** + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ + error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + + /** + * @notice Error indicating missing approval for an operator. + * @param _operator Address attempting the operation. + * @param _owner The token owner. + */ + error ERC1155MissingApprovalForAll(address _operator, address _owner); + + /** + * @notice Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. + * @param _operator The address which initiated the transfer. + * @param _from The address which previously owned the token. + * @param _to The address which now owns the token. + * @param _id The token type being transferred. + * @param _value The amount of tokens transferred. + */ + event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value + ); + + /** + * @notice Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers. + * @param _operator The address which initiated the batch transfer. + * @param _from The address which previously owned the tokens. + * @param _to The address which now owns the tokens. + * @param _ids The token types being transferred. + * @param _values The amounts of tokens transferred. + */ + event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values + ); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + + /** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ + struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; + } + + /** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ + function getStorage() internal pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Burns a single token type from an address. + * @dev Emits a {TransferSingle} event. + * Caller must be the owner or an approved operator. + * @param _from The address whose tokens will be burned. + * @param _id The token type to burn. + * @param _value The amount of tokens to burn. + */ + function burn(address _from, uint256 _id, uint256 _value) external { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + + ERC1155Storage storage s = getStorage(); + + if (_from != msg.sender && !s.isApprovedForAll[_from][msg.sender]) { + revert ERC1155MissingApprovalForAll(msg.sender, _from); + } + + uint256 fromBalance = s.balanceOf[_id][_from]; + + if (fromBalance < _value) { + revert ERC1155InsufficientBalance(_from, fromBalance, _value, _id); + } + + unchecked { + s.balanceOf[_id][_from] = fromBalance - _value; + } + + emit TransferSingle(msg.sender, _from, address(0), _id, _value); + } + + /** + * @notice Burns multiple token types from an address in a single transaction. + * @dev Emits a {TransferBatch} event. + * Caller must be the owner or an approved operator. + * @param _from The address whose tokens will be burned. + * @param _ids The token types to burn. + * @param _values The amounts of tokens to burn for each type. + */ + function burnBatch(address _from, uint256[] calldata _ids, uint256[] calldata _values) external { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + if (_ids.length != _values.length) { + revert ERC1155InvalidArrayLength(_ids.length, _values.length); + } + + ERC1155Storage storage s = getStorage(); + + if (_from != msg.sender && !s.isApprovedForAll[_from][msg.sender]) { + revert ERC1155MissingApprovalForAll(msg.sender, _from); + } + + for (uint256 i = 0; i < _ids.length; i++) { + uint256 id = _ids[i]; + uint256 value = _values[i]; + uint256 fromBalance = s.balanceOf[id][_from]; + + if (fromBalance < value) { + revert ERC1155InsufficientBalance(_from, fromBalance, value, id); + } + + unchecked { + s.balanceOf[id][_from] = fromBalance - value; + } + } + + emit TransferBatch(msg.sender, _from, address(0), _ids, _values); + } + + /** + * @notice Exports the function selectors of the ERC1155BurnFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC1155BurnFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.burn.selector, this.burnBatch.selector); + } +} diff --git a/src/token/ERC1155/Burn/ERC1155BurnMod.sol b/src/token/ERC1155/Burn/ERC1155BurnMod.sol new file mode 100644 index 00000000..cfe42fc7 --- /dev/null +++ b/src/token/ERC1155/Burn/ERC1155BurnMod.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Burn Module + * @notice Provides internal burn functionality for ERC-1155 tokens. + */ + +/** + * @notice Error indicating insufficient balance for a burn operation. + * @param _sender Address attempting the burn. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + * @param _tokenId The token ID involved. + */ +error ERC1155InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _tokenId); + +/** + * @notice Error indicating the sender address is invalid. + * @param _sender Invalid sender address. + */ +error ERC1155InvalidSender(address _sender); + +/** + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ +error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + +/** + * @notice Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. + * @param _operator The address which initiated the transfer. + * @param _from The address which previously owned the token. + * @param _to The address which now owns the token. + * @param _id The token type being transferred. + * @param _value The amount of tokens transferred. + */ +event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value +); + +/** + * @notice Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers. + * @param _operator The address which initiated the batch transfer. + * @param _from The address which previously owned the tokens. + * @param _to The address which now owns the tokens. + * @param _ids The token types being transferred. + * @param _values The amounts of tokens transferred. + */ +event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values +); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + +/** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ +struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; +} + +/** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ +function getStorage() pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Burns a single token type from an address. + * @dev Decreases the balance and emits a TransferSingle event. + * Reverts if the account has insufficient balance. + * This module does not perform approval checks. Ensure proper ownership or approval validation before calling this function. + * @param _from The address whose tokens will be burned. + * @param _id The token type to burn. + * @param _value The amount of tokens to burn. + */ +function burn(address _from, uint256 _id, uint256 _value) { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + + ERC1155Storage storage s = getStorage(); + uint256 fromBalance = s.balanceOf[_id][_from]; + + if (fromBalance < _value) { + revert ERC1155InsufficientBalance(_from, fromBalance, _value, _id); + } + + unchecked { + s.balanceOf[_id][_from] = fromBalance - _value; + } + + emit TransferSingle(msg.sender, _from, address(0), _id, _value); +} + +/** + * @notice Burns multiple token types from an address in a single transaction. + * @dev Decreases balances for each token type and emits a TransferBatch event. + * Reverts if the account has insufficient balance for any token type. + * This module does not perform approval checks. Ensure proper ownership or approval validation before calling this function. + * @param _from The address whose tokens will be burned. + * @param _ids The token types to burn. + * @param _values The amounts of tokens to burn for each type. + */ +function burnBatch(address _from, uint256[] memory _ids, uint256[] memory _values) { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + if (_ids.length != _values.length) { + revert ERC1155InvalidArrayLength(_ids.length, _values.length); + } + + ERC1155Storage storage s = getStorage(); + + for (uint256 i = 0; i < _ids.length; i++) { + uint256 id = _ids[i]; + uint256 value = _values[i]; + uint256 fromBalance = s.balanceOf[id][_from]; + + if (fromBalance < value) { + revert ERC1155InsufficientBalance(_from, fromBalance, value, id); + } + + unchecked { + s.balanceOf[id][_from] = fromBalance - value; + } + } + + emit TransferBatch(msg.sender, _from, address(0), _ids, _values); +} diff --git a/src/token/ERC1155/ERC1155DataFacet.sol b/src/token/ERC1155/Data/ERC1155DataFacet.sol similarity index 86% rename from src/token/ERC1155/ERC1155DataFacet.sol rename to src/token/ERC1155/Data/ERC1155DataFacet.sol index 2a61230f..7547f2ee 100644 --- a/src/token/ERC1155/ERC1155DataFacet.sol +++ b/src/token/ERC1155/Data/ERC1155DataFacet.sol @@ -6,8 +6,7 @@ pragma solidity >=0.8.30; */ /** - * @title ERC-1155 Multi Token Standard - * + * @title ERC-1155 Data Facet */ contract ERC1155DataFacet { /** @@ -29,9 +28,6 @@ contract ERC1155DataFacet { struct ERC1155Storage { mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; mapping(address account => mapping(address operator => bool)) isApprovedForAll; - string uri; - string baseURI; - mapping(uint256 tokenId => string) tokenURIs; } /** @@ -88,4 +84,13 @@ contract ERC1155DataFacet { function isApprovedForAll(address _account, address _operator) external view returns (bool) { return getStorage().isApprovedForAll[_account][_operator]; } + + /** + * @notice Exports the function selectors of the ERC1155DataFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC1155DataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.balanceOf.selector, this.balanceOfBatch.selector, this.isApprovedForAll.selector); + } } diff --git a/src/token/ERC1155/Metadata/ERC1155MetadataFacet.sol b/src/token/ERC1155/Metadata/ERC1155MetadataFacet.sol new file mode 100644 index 00000000..382919fa --- /dev/null +++ b/src/token/ERC1155/Metadata/ERC1155MetadataFacet.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Metadata Facet + * @notice Provides URI metadata functionality for ERC-1155 tokens. + */ +contract ERC1155MetadataFacet { + /** + * @notice Emitted when the URI for token type `_id` changes to `_value`. + * @param _value The new URI for the token type. + * @param _id The token type whose URI changed. + */ + event URI(string _value, uint256 indexed _id); + + /** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ + bytes32 constant STORAGE_POSITION = keccak256("erc1155.metadata"); + + /** + * @dev ERC-8042 compliant storage struct for ERC-1155 metadata. + * @custom:storage-location erc8042:erc1155.metadata + */ + struct ERC1155MetadataStorage { + string uri; + string baseURI; + mapping(uint256 tokenId => string) tokenURIs; + } + + /** + * @notice Returns the ERC-1155 metadata storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 metadata storage struct reference. + */ + function getStorage() internal pure returns (ERC1155MetadataStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the URI for token type `_id`. + * @dev If a token-specific URI is set in tokenURIs[_id], returns the concatenation of baseURI and tokenURIs[_id]. + * Note that baseURI is empty by default and must be set explicitly if concatenation is desired. + * If no token-specific URI is set, returns the default URI which applies to all token types. + * The default URI may contain the substring `{id}` which clients should replace with the actual token ID. + * @param _id The token ID to query. + * @return The URI for the token type. + */ + function uri(uint256 _id) external view returns (string memory) { + ERC1155MetadataStorage storage s = getStorage(); + string memory tokenURI = s.tokenURIs[_id]; + + return bytes(tokenURI).length > 0 ? string.concat(s.baseURI, tokenURI) : s.uri; + } + + /** + * @notice Exports the function selectors of the ERC1155MetadataFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC1155MetadataFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.uri.selector); + } +} diff --git a/src/token/ERC1155/Metadata/ERC1155MetadataMod.sol b/src/token/ERC1155/Metadata/ERC1155MetadataMod.sol new file mode 100644 index 00000000..76997eb6 --- /dev/null +++ b/src/token/ERC1155/Metadata/ERC1155MetadataMod.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Metadata Module + * @notice Provides internal metadata functionality for ERC-1155 tokens. + */ + +/** + * @notice Emitted when the URI for token type `_id` changes to `_value`. + * @param _value The new URI for the token type. + * @param _id The token type whose URI changed. + */ +event URI(string _value, uint256 indexed _id); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc1155.metadata"); + +/** + * @dev ERC-8042 compliant storage struct for ERC-1155 metadata. + * @custom:storage-location erc8042:erc1155.metadata + */ +struct ERC1155MetadataStorage { + string uri; + string baseURI; + mapping(uint256 tokenId => string) tokenURIs; +} + +/** + * @notice Returns the ERC-1155 metadata storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 metadata storage struct reference. + */ +function getStorage() pure returns (ERC1155MetadataStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Sets the token-specific URI for a given token ID. + * @dev Sets tokenURIs[_tokenId] to the provided string and emits a URI event with the full computed URI. + * The emitted URI is the concatenation of baseURI and the token-specific URI. + * @param _tokenId The token ID to set the URI for. + * @param _tokenURI The token-specific URI string to be concatenated with baseURI. + */ +function setTokenURI(uint256 _tokenId, string memory _tokenURI) { + ERC1155MetadataStorage storage s = getStorage(); + s.tokenURIs[_tokenId] = _tokenURI; + + string memory fullURI = bytes(_tokenURI).length > 0 ? string.concat(s.baseURI, _tokenURI) : s.uri; + emit URI(fullURI, _tokenId); +} + +/** + * @notice Sets the base URI prefix for token-specific URIs. + * @dev The base URI is concatenated with token-specific URIs set via setTokenURI. + * Does not affect the default URI used when no token-specific URI is set. + * @param _baseURI The base URI string to prepend to token-specific URIs. + */ +function setBaseURI(string memory _baseURI) { + ERC1155MetadataStorage storage s = getStorage(); + s.baseURI = _baseURI; +} + +/** + * @notice Sets the default URI for all token types. + * @dev This URI is used when no token-specific URI is set. + * @param _uri The default URI string. + */ +function setURI(string memory _uri) { + ERC1155MetadataStorage storage s = getStorage(); + s.uri = _uri; +} diff --git a/src/token/ERC1155/Mint/ERC1155MintMod.sol b/src/token/ERC1155/Mint/ERC1155MintMod.sol new file mode 100644 index 00000000..2392536a --- /dev/null +++ b/src/token/ERC1155/Mint/ERC1155MintMod.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +/** + * @title ERC-1155 Token Receiver Interface + * @notice Interface that must be implemented by smart contracts in order to receive ERC-1155 token transfers. + */ +interface IERC1155Receiver { + /** + * @notice Handles the receipt of a single ERC-1155 token type. + * @dev This function is called at the end of a `safeTransferFrom` after the balance has been updated. + * + * IMPORTANT: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param _operator The address which initiated the transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _id The ID of the token being transferred. + * @param _value The amount of tokens being transferred. + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed. + */ + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) + external + returns (bytes4); + + /** + * @notice Handles the receipt of multiple ERC-1155 token types. + * @dev This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. + * + * IMPORTANT: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param _operator The address which initiated the batch transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _ids An array containing ids of each token being transferred (order and length must match _values array). + * @param _values An array containing amounts of each token being transferred (order and length must match _ids array). + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed. + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external returns (bytes4); +} + +/** + * @title ERC-1155 Mint Module + * @notice Provides internal mint functionality for ERC-1155 tokens. + */ + +/** + * @notice Error indicating the receiver address is invalid. + * @param _receiver Invalid receiver address. + */ +error ERC1155InvalidReceiver(address _receiver); + +/** + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ +error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + +/** + * @notice Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. + * @param _operator The address which initiated the transfer. + * @param _from The address which previously owned the token. + * @param _to The address which now owns the token. + * @param _id The token type being transferred. + * @param _value The amount of tokens transferred. + */ +event TransferSingle( + address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value +); + +/** + * @notice Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers. + * @param _operator The address which initiated the batch transfer. + * @param _from The address which previously owned the tokens. + * @param _to The address which now owns the tokens. + * @param _ids The token types being transferred. + * @param _values The amounts of tokens transferred. + */ +event TransferBatch( + address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values +); + +/** + * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + */ +bytes32 constant STORAGE_POSITION = keccak256("erc1155"); + +/** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:erc1155 + */ +struct ERC1155Storage { + mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; + mapping(address account => mapping(address operator => bool)) isApprovedForAll; +} + +/** + * @notice Returns the ERC-1155 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC-1155 storage struct reference. + */ +function getStorage() pure returns (ERC1155Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } +} + +/** + * @notice Mints a single token type to an address. + * @dev Increases the balance and emits a TransferSingle event. + * Performs receiver validation if recipient is a contract. + * @param _to The address that will receive the tokens. + * @param _id The token type to mint. + * @param _value The amount of tokens to mint. + * @param _data Additional data with no specified format. + */ +function mint(address _to, uint256 _id, uint256 _value, bytes memory _data) { + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + + ERC1155Storage storage s = getStorage(); + s.balanceOf[_id][_to] += _value; + + emit TransferSingle(msg.sender, address(0), _to, _id, _value); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155Received(msg.sender, address(0), _id, _value, _data) returns ( + bytes4 response + ) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert ERC1155InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(_to); + } else { + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } +} + +/** + * @notice Mints multiple token types to an address in a single transaction. + * @dev Increases balances for each token type and emits a TransferBatch event. + * Performs receiver validation if recipient is a contract. + * @param _to The address that will receive the tokens. + * @param _ids The token types to mint. + * @param _values The amounts of tokens to mint for each type. + * @param _data Additional data with no specified format. + */ +function mintBatch(address _to, uint256[] memory _ids, uint256[] memory _values, bytes memory _data) { + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (_ids.length != _values.length) { + revert ERC1155InvalidArrayLength(_ids.length, _values.length); + } + + ERC1155Storage storage s = getStorage(); + + for (uint256 i = 0; i < _ids.length; i++) { + s.balanceOf[_ids[i]][_to] += _values[i]; + } + + emit TransferBatch(msg.sender, address(0), _to, _ids, _values); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155BatchReceived(msg.sender, address(0), _ids, _values, _data) returns ( + bytes4 response + ) { + if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { + revert ERC1155InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(_to); + } else { + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } +} diff --git a/src/token/ERC1155/ERC1155Facet.sol b/src/token/ERC1155/Transfer/ERC1155TransferFacet.sol similarity index 68% rename from src/token/ERC1155/ERC1155Facet.sol rename to src/token/ERC1155/Transfer/ERC1155TransferFacet.sol index fb551c81..ccd1129b 100644 --- a/src/token/ERC1155/ERC1155Facet.sol +++ b/src/token/ERC1155/Transfer/ERC1155TransferFacet.sol @@ -54,15 +54,10 @@ interface IERC1155Receiver { } /** - * @title ERC-1155 Multi Token Standard - * @notice A complete, dependency-free ERC-1155 implementation using the diamond storage pattern. - * @dev This facet provides balance queries, approvals, safe transfers, and URI management for multi-token contracts. - * - * For developers creating custom facets that need to interact with ERC-1155 storage (e.g., custom minting logic), - * use the LibERC1155 library which provides helper functions to access this facet's storage. - * This facet does NOT depend on LibERC1155 - both access the same storage at keccak256("erc1155"). + * @title ERC-1155 Transfer Facet + * @notice Provides transfer functionality for ERC-1155 tokens. */ -contract ERC1155Facet { +contract ERC1155TransferFacet { /** * @notice Error indicating insufficient balance for a transfer. * @param _sender Address attempting the transfer. @@ -91,18 +86,6 @@ contract ERC1155Facet { */ error ERC1155MissingApprovalForAll(address _operator, address _owner); - /** - * @notice Error indicating the approver address is invalid. - * @param _approver Invalid approver address. - */ - error ERC1155InvalidApprover(address _approver); - - /** - * @notice Error indicating the operator address is invalid. - * @param _operator Invalid operator address. - */ - error ERC1155InvalidOperator(address _operator); - /** * @notice Error indicating array length mismatch in batch operations. * @param _idsLength Length of the ids array. @@ -134,21 +117,6 @@ contract ERC1155Facet { address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values ); - /** - * @notice Emitted when `account` grants or revokes permission to `operator` to transfer their tokens. - * @param _account The token owner granting/revoking approval. - * @param _operator The address being approved/revoked. - * @param _approved True if approval is granted, false if revoked. - */ - event ApprovalForAll(address indexed _account, address indexed _operator, bool _approved); - - /** - * @notice Emitted when the URI for token type `id` changes to `value`. - * @param _value The new URI for the token type. - * @param _id The token type whose URI changed. - */ - event URI(string _value, uint256 indexed _id); - /** * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. */ @@ -161,9 +129,6 @@ contract ERC1155Facet { struct ERC1155Storage { mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; mapping(address account => mapping(address operator => bool)) isApprovedForAll; - string uri; - string baseURI; - mapping(uint256 tokenId => string) tokenURIs; } /** @@ -178,79 +143,6 @@ contract ERC1155Facet { } } - /** - * @notice Returns the URI for token type `_id`. - * @dev If a token-specific URI is set in tokenURIs[_id], returns the concatenation of baseURI and tokenURIs[_id]. - * Note that baseURI is empty by default and must be set explicitly if concatenation is desired. - * If no token-specific URI is set, returns the default URI which applies to all token types. - * The default URI may contain the substring `{id}` which clients should replace with the actual token ID. - * @param _id The token ID to query. - * @return The URI for the token type. - */ - function uri(uint256 _id) external view returns (string memory) { - ERC1155Storage storage s = getStorage(); - string memory tokenURI = s.tokenURIs[_id]; - - return bytes(tokenURI).length > 0 ? string.concat(s.baseURI, tokenURI) : s.uri; - } - - /** - * @notice Returns the amount of tokens of token type `id` owned by `account`. - * @param _account The address to query the balance of. - * @param _id The token type to query. - * @return The balance of the token type. - */ - function balanceOf(address _account, uint256 _id) external view returns (uint256) { - return getStorage().balanceOf[_id][_account]; - } - - /** - * @notice Batched version of {balanceOf}. - * @param _accounts The addresses to query the balances of. - * @param _ids The token types to query. - * @return balances The balances of the token types. - */ - function balanceOfBatch(address[] calldata _accounts, uint256[] calldata _ids) - external - view - returns (uint256[] memory balances) - { - if (_accounts.length != _ids.length) { - revert ERC1155InvalidArrayLength(_ids.length, _accounts.length); - } - - ERC1155Storage storage s = getStorage(); - balances = new uint256[](_accounts.length); - - for (uint256 i = 0; i < _accounts.length; i++) { - balances[i] = s.balanceOf[_ids[i]][_accounts[i]]; - } - } - - /** - * @notice Grants or revokes permission to `operator` to transfer the caller's tokens. - * @dev Emits an {ApprovalForAll} event. - * @param _operator The address to grant/revoke approval to. - * @param _approved True to approve, false to revoke. - */ - function setApprovalForAll(address _operator, bool _approved) external { - if (_operator == address(0)) { - revert ERC1155InvalidOperator(address(0)); - } - getStorage().isApprovedForAll[msg.sender][_operator] = _approved; - emit ApprovalForAll(msg.sender, _operator, _approved); - } - - /** - * @notice Returns true if `operator` is approved to transfer `account`'s tokens. - * @param _account The token owner. - * @param _operator The operator to query. - * @return True if the operator is approved, false otherwise. - */ - function isApprovedForAll(address _account, address _operator) external view returns (bool) { - return getStorage().isApprovedForAll[_account][_operator]; - } - /** * @notice Transfers `value` amount of token type `id` from `from` to `to`. * @dev Emits a {TransferSingle} event. @@ -379,4 +271,13 @@ contract ERC1155Facet { } } } + + /** + * @notice Exports the function selectors of the ERC1155TransferFacet + * @dev This function is use as a selector discovery mechanism for diamonds + * @return selectors The exported function selectors of the ERC1155TransferFacet + */ + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.safeTransferFrom.selector, this.safeBatchTransferFrom.selector); + } } diff --git a/src/token/ERC1155/ERC1155Mod.sol b/src/token/ERC1155/Transfer/ERC1155TransferMod.sol similarity index 56% rename from src/token/ERC1155/ERC1155Mod.sol rename to src/token/ERC1155/Transfer/ERC1155TransferMod.sol index 82452b33..bcfb0e70 100644 --- a/src/token/ERC1155/ERC1155Mod.sol +++ b/src/token/ERC1155/Transfer/ERC1155TransferMod.sol @@ -5,12 +5,12 @@ pragma solidity >=0.8.30; * https://compose.diamonds */ -/* +/** * @title ERC-1155 Token Receiver Interface - * @notice Interface for contracts that want to handle safe transfers of ERC-1155 tokens. + * @notice Interface that must be implemented by smart contracts in order to receive ERC-1155 token transfers. */ interface IERC1155Receiver { - /* + /** * @notice Handles the receipt of a single ERC-1155 token type. * @dev This function is called at the end of a `safeTransferFrom` after the balance has been updated. * @@ -54,15 +54,13 @@ interface IERC1155Receiver { } /** - * @title LibERC1155 — ERC-1155 Library - * @notice Provides internal functions and storage layout for ERC-1155 multi-token logic. - * @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions. - * This library is intended to be used by custom facets to integrate with ERC-1155 functionality. + * @title ERC-1155 Transfer Module + * @notice Provides internal transfer functionality for ERC-1155 tokens. */ /** - * @notice Thrown when insufficient balance for a transfer or burn operation. - * @param _sender Address attempting the operation. + * @notice Error indicating insufficient balance for a transfer. + * @param _sender Address attempting the transfer. * @param _balance Current balance of the sender. * @param _needed Amount required to complete the operation. * @param _tokenId The token ID involved. @@ -70,33 +68,33 @@ interface IERC1155Receiver { error ERC1155InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _tokenId); /** - * @notice Thrown when the sender address is invalid. + * @notice Error indicating the sender address is invalid. * @param _sender Invalid sender address. */ error ERC1155InvalidSender(address _sender); /** - * @notice Thrown when the receiver address is invalid. + * @notice Error indicating the receiver address is invalid. * @param _receiver Invalid receiver address. */ error ERC1155InvalidReceiver(address _receiver); /** - * @notice Thrown when array lengths don't match in batch operations. - * @param _idsLength Length of the ids array. - * @param _valuesLength Length of the values array. - */ -error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); - -/** - * @notice Thrown when missing approval for an operator. + * @notice Error indicating missing approval for an operator. * @param _operator Address attempting the operation. * @param _owner The token owner. */ error ERC1155MissingApprovalForAll(address _operator, address _owner); /** - * @notice Emitted when a single token type is transferred. + * @notice Error indicating array length mismatch in batch operations. + * @param _idsLength Length of the ids array. + * @param _valuesLength Length of the values array. + */ +error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + +/** + * @notice Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`. * @param _operator The address which initiated the transfer. * @param _from The address which previously owned the token. * @param _to The address which now owns the token. @@ -108,7 +106,7 @@ event TransferSingle( ); /** - * @notice Emitted when multiple token types are transferred. + * @notice Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all transfers. * @param _operator The address which initiated the batch transfer. * @param _from The address which previously owned the tokens. * @param _to The address which now owns the tokens. @@ -119,13 +117,6 @@ event TransferBatch( address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values ); -/** - * @notice Emitted when the URI for token type `_id` changes to `_value`. - * @param _value The new URI for the token type. - * @param _id The token type whose URI changed. - */ -event URI(string _value, uint256 indexed _id); - /** * @dev Storage position determined by the keccak256 hash of the diamond storage identifier. */ @@ -138,9 +129,6 @@ bytes32 constant STORAGE_POSITION = keccak256("erc1155"); struct ERC1155Storage { mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; mapping(address account => mapping(address operator => bool)) isApprovedForAll; - string uri; - string baseURI; - mapping(uint256 tokenId => string) tokenURIs; } /** @@ -155,148 +143,6 @@ function getStorage() pure returns (ERC1155Storage storage s) { } } -/** - * @notice Mints a single token type to an address. - * @dev Increases the balance and emits a TransferSingle event. - * Performs receiver validation if recipient is a contract. - * @param _to The address that will receive the tokens. - * @param _id The token type to mint. - * @param _value The amount of tokens to mint. - */ -function mint(address _to, uint256 _id, uint256 _value, bytes memory _data) { - if (_to == address(0)) { - revert ERC1155InvalidReceiver(address(0)); - } - - ERC1155Storage storage s = getStorage(); - s.balanceOf[_id][_to] += _value; - - emit TransferSingle(msg.sender, address(0), _to, _id, _value); - - if (_to.code.length > 0) { - try IERC1155Receiver(_to).onERC1155Received(msg.sender, address(0), _id, _value, _data) returns ( - bytes4 response - ) { - if (response != IERC1155Receiver.onERC1155Received.selector) { - revert ERC1155InvalidReceiver(_to); - } - } catch (bytes memory reason) { - if (reason.length == 0) { - revert ERC1155InvalidReceiver(_to); - } else { - assembly ("memory-safe") { - revert(add(reason, 0x20), mload(reason)) - } - } - } - } -} - -/** - * @notice Mints multiple token types to an address in a single transaction. - * @dev Increases balances for each token type and emits a TransferBatch event. - * Performs receiver validation if recipient is a contract. - * @param _to The address that will receive the tokens. - * @param _ids The token types to mint. - * @param _values The amounts of tokens to mint for each type. - */ -function mintBatch(address _to, uint256[] memory _ids, uint256[] memory _values, bytes memory _data) { - if (_to == address(0)) { - revert ERC1155InvalidReceiver(address(0)); - } - if (_ids.length != _values.length) { - revert ERC1155InvalidArrayLength(_ids.length, _values.length); - } - - ERC1155Storage storage s = getStorage(); - - for (uint256 i = 0; i < _ids.length; i++) { - s.balanceOf[_ids[i]][_to] += _values[i]; - } - - emit TransferBatch(msg.sender, address(0), _to, _ids, _values); - - if (_to.code.length > 0) { - try IERC1155Receiver(_to).onERC1155BatchReceived(msg.sender, address(0), _ids, _values, _data) returns ( - bytes4 response - ) { - if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { - revert ERC1155InvalidReceiver(_to); - } - } catch (bytes memory reason) { - if (reason.length == 0) { - revert ERC1155InvalidReceiver(_to); - } else { - assembly ("memory-safe") { - revert(add(reason, 0x20), mload(reason)) - } - } - } - } -} - -/** - * @notice Burns a single token type from an address. - * @dev Decreases the balance and emits a TransferSingle event. - * Reverts if the account has insufficient balance. - * @param _from The address whose tokens will be burned. - * @param _id The token type to burn. - * @param _value The amount of tokens to burn. - */ -function burn(address _from, uint256 _id, uint256 _value) { - if (_from == address(0)) { - revert ERC1155InvalidSender(address(0)); - } - - ERC1155Storage storage s = getStorage(); - uint256 fromBalance = s.balanceOf[_id][_from]; - - if (fromBalance < _value) { - revert ERC1155InsufficientBalance(_from, fromBalance, _value, _id); - } - - unchecked { - s.balanceOf[_id][_from] = fromBalance - _value; - } - - emit TransferSingle(msg.sender, _from, address(0), _id, _value); -} - -/** - * @notice Burns multiple token types from an address in a single transaction. - * @dev Decreases balances for each token type and emits a TransferBatch event. - * Reverts if the account has insufficient balance for any token type. - * @param _from The address whose tokens will be burned. - * @param _ids The token types to burn. - * @param _values The amounts of tokens to burn for each type. - */ -function burnBatch(address _from, uint256[] memory _ids, uint256[] memory _values) { - if (_from == address(0)) { - revert ERC1155InvalidSender(address(0)); - } - if (_ids.length != _values.length) { - revert ERC1155InvalidArrayLength(_ids.length, _values.length); - } - - ERC1155Storage storage s = getStorage(); - - for (uint256 i = 0; i < _ids.length; i++) { - uint256 id = _ids[i]; - uint256 value = _values[i]; - uint256 fromBalance = s.balanceOf[id][_from]; - - if (fromBalance < value) { - revert ERC1155InsufficientBalance(_from, fromBalance, value, id); - } - - unchecked { - s.balanceOf[id][_from] = fromBalance - value; - } - } - - emit TransferBatch(msg.sender, _from, address(0), _ids, _values); -} - /** * @notice Safely transfers a single token type from one address to another. * @dev Validates ownership, approval, and receiver address before updating balances. @@ -427,29 +273,3 @@ function safeBatchTransferFrom( } } } - -/** - * @notice Sets the token-specific URI for a given token ID. - * @dev Sets tokenURIs[_tokenId] to the provided string and emits a URI event with the full computed URI. - * The emitted URI is the concatenation of baseURI and the token-specific URI. - * @param _tokenId The token ID to set the URI for. - * @param _tokenURI The token-specific URI string to be concatenated with baseURI. - */ -function setTokenURI(uint256 _tokenId, string memory _tokenURI) { - ERC1155Storage storage s = getStorage(); - s.tokenURIs[_tokenId] = _tokenURI; - - string memory fullURI = bytes(_tokenURI).length > 0 ? string.concat(s.baseURI, _tokenURI) : s.uri; - emit URI(fullURI, _tokenId); -} - -/** - * @notice Sets the base URI prefix for token-specific URIs. - * @dev The base URI is concatenated with token-specific URIs set via setTokenURI. - * Does not affect the default URI used when no token-specific URI is set. - * @param _baseURI The base URI string to prepend to token-specific URIs. - */ -function setBaseURI(string memory _baseURI) { - ERC1155Storage storage s = getStorage(); - s.baseURI = _baseURI; -}