From 17aca0e1ad2aee64678882139e899828b9df47b2 Mon Sep 17 00:00:00 2001 From: aapsi Date: Tue, 28 Oct 2025 14:43:18 +0530 Subject: [PATCH 01/29] Add ERC-1155 standard interface and facet implementation --- src/interfaces/IERC1155.sol | 155 +++++++++++++ src/token/ERC1155/ERC1155Facet.sol | 339 +++++++++++++++++++++++++++++ 2 files changed, 494 insertions(+) create mode 100644 src/interfaces/IERC1155.sol create mode 100644 src/token/ERC1155/ERC1155Facet.sol diff --git a/src/interfaces/IERC1155.sol b/src/interfaces/IERC1155.sol new file mode 100644 index 00000000..4a6cc456 --- /dev/null +++ b/src/interfaces/IERC1155.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/// @title ERC-1155 Multi Token Standard Interface +/// @notice Interface for ERC-1155 token contracts with custom errors +/// @dev This interface includes all custom errors used by ERC-1155 implementations (ERC-6093) +interface IERC1155 { + /// @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. + 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 the receiver address is invalid. + /// @param _receiver Invalid receiver address. + error ERC1155InvalidReceiver(address _receiver); + + /// @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 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. + /// @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 + ); + + /// @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); + + /// @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); + + /// @notice Batched version of {balanceOf}. + /// @param _accounts The addresses to query the balances of. + /// @param _ids The token types to query. + /// @return The balances of the token types. + function balanceOfBatch(address[] calldata _accounts, uint256[] calldata _ids) + external + view + returns (uint256[] memory); + + /// @notice Grants or revokes permission to `operator` to transfer the caller's tokens. + /// @param _operator The address to grant/revoke approval to. + /// @param _approved True to approve, false to revoke. + function setApprovalForAll(address _operator, bool _approved) external; + + /// @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); + + /// @notice Transfers `value` amount of token type `id` from `from` to `to`. + /// @param _from The address to transfer from. + /// @param _to The address to transfer to. + /// @param _id The token type to transfer. + /// @param _value The amount to transfer. + /// @param _data Additional data with no specified format. + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external; + + /// @notice Batched version of {safeTransferFrom}. + /// @param _from The address to transfer from. + /// @param _to The address to transfer to. + /// @param _ids The token types to transfer. + /// @param _values The amounts to transfer. + /// @param _data Additional data with no specified format. + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external; + + /// @notice Returns the URI for token type `id`. + /// @param _id The token type to query. + /// @return The URI for the token type. + function uri(uint256 _id) external view returns (string memory); +} + +/// @title ERC-1155 Token Receiver Interface +/// @notice Interface for contracts that want to support safe transfers from ERC-1155 token contracts. +/// @dev Contracts implementing this must return the correct selector to confirm token receipt. +interface IERC1155Receiver { + /// @notice Handles the receipt of a single ERC-1155 token type. + /// @param _operator The address which initiated the transfer. + /// @param _from The address which previously owned the token. + /// @param _id The token type being transferred. + /// @param _value The amount of tokens being transferred. + /// @param _data Additional data with no specified format. + /// @return The selector to confirm the token transfer. + 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. + /// @param _operator The address which initiated the batch transfer. + /// @param _from The address which previously owned the tokens. + /// @param _ids The token types being transferred. + /// @param _values The amounts of tokens being transferred. + /// @param _data Additional data with no specified format. + /// @return The selector to confirm the batch token transfer. + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external returns (bytes4); +} diff --git a/src/token/ERC1155/ERC1155Facet.sol b/src/token/ERC1155/ERC1155Facet.sol new file mode 100644 index 00000000..9ad8b506 --- /dev/null +++ b/src/token/ERC1155/ERC1155Facet.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/// @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 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("compose.erc1155"). +contract ERC1155Facet { + /// @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. + 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 the receiver address is invalid. + /// @param _receiver Invalid receiver address. + error ERC1155InvalidReceiver(address _receiver); + + /// @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 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. + /// @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 + ); + + /// @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. + bytes32 constant STORAGE_POSITION = keccak256("compose.erc1155"); + + /** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:compose.erc1155 + */ + struct ERC1155Storage { + string uri; + 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 Returns the URI for token type `id`. + * @dev This implementation returns the same URI for all token types. It relies + * on the token type ID substitution mechanism defined in the ERC-1155 standard. + * Clients calling this function must replace the `{id}` substring with the + * actual token type ID in hexadecimal form. + * @return The URI for the token type. + */ + function uri(uint256 /* _id */ ) external view returns (string memory) { + return getStorage().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. + * @param _from The address to transfer from. + * @param _to The address to transfer to. + * @param _id The token type to transfer. + * @param _value The amount to transfer. + * @param _data Additional data with no specified format. + */ + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external { + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + + ERC1155Storage storage s = getStorage(); + + // Check authorization + 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; + } + s.balanceOf[_id][_to] += _value; + + emit TransferSingle(msg.sender, _from, _to, _id, _value); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _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 Batched version of {safeTransferFrom}. + * @dev Emits a {TransferBatch} event. + * @param _from The address to transfer from. + * @param _to The address to transfer to. + * @param _ids The token types to transfer. + * @param _values The amounts to transfer. + * @param _data Additional data with no specified format. + */ + function safeBatchTransferFrom( + address _from, + address _to, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external { + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + if (_ids.length != _values.length) { + revert ERC1155InvalidArrayLength(_ids.length, _values.length); + } + + ERC1155Storage storage s = getStorage(); + + // Check authorization + 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; + } + s.balanceOf[id][_to] += value; + } + + emit TransferBatch(msg.sender, _from, _to, _ids, _values); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155BatchReceived(msg.sender, _from, _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)) + } + } + } + } + } +} From d917ba378cfc4f74fd88d0ebcdfe50410b560098 Mon Sep 17 00:00:00 2001 From: SuperFranky Date: Tue, 28 Oct 2025 14:34:58 +0100 Subject: [PATCH 02/29] Add LibERC1155 library for ERC-1155 --- src/token/ERC1155/LibERC1155.sol | 179 +++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/token/ERC1155/LibERC1155.sol diff --git a/src/token/ERC1155/LibERC1155.sol b/src/token/ERC1155/LibERC1155.sol new file mode 100644 index 00000000..b09072e4 --- /dev/null +++ b/src/token/ERC1155/LibERC1155.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/// @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. +library LibERC1155 { + /// @notice Thrown when insufficient balance for a transfer or burn operation. + /// @param _sender Address attempting the operation. + /// @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 Thrown when the sender address is invalid. + /// @param _sender Invalid sender address. + error ERC1155InvalidSender(address _sender); + + /// @notice Thrown when 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 Emitted when a single token type is transferred. + /// @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 Emitted when multiple token types are transferred. + /// @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("compose.erc1155"); + + /** + * @dev ERC-8042 compliant storage struct for ERC-1155 token data. + * @custom:storage-location erc8042:compose.erc1155 + */ + struct ERC1155Storage { + string uri; + 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 Mints a single token type to an address. + * @dev Increases the balance and emits a TransferSingle event. + * Does NOT perform receiver validation - use with caution. + * @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) internal { + 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); + } + + /** + * @notice Mints multiple token types to an address in a single transaction. + * @dev Increases balances for each token type and emits a TransferBatch event. + * Does NOT perform receiver validation - use with caution. + * @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) internal { + 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); + } + + /** + * @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) internal { + 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) internal { + 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); + } +} \ No newline at end of file From 0322f9c061958aeb4fab543b088be7a1a45f9536 Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 11:10:45 -0400 Subject: [PATCH 03/29] Improved README --- README.md | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d4a30888..34115e82 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## What is Compose? -Compose is a smart contract library that helps developers create their own smart contract systems using the [ERC-2535 Diamond Standard](https://eips.ethereum.org/EIPS/eip-2535). +Compose is a smart contract library that helps developers create smart contract systems using [ERC-2535 Diamonds](https://eips.ethereum.org/EIPS/eip-2535). **Compose provides:** @@ -58,8 +58,9 @@ Compose uses two complementary patterns for smart contract development: **Facets** are standalone contracts that contain all the logic needed to implement specific functionality. They're designed to be: - **Self-contained**: All code needed for the feature is in one file -- **Readable top-to-bottom**: No jumping between files to understand the logic +- **Readable top-to-bottom**: No jumping between files (or within the same file) to understand the logic - **Deployed once**: Reused across multiple diamonds on-chain +- **Deployed everywhere**: Our facets will be deployed on many blockchains at the same addresses **Example**: `ERC20Facet.sol` contains the complete ERC-20 token implementation. @@ -82,14 +83,13 @@ Compose uses two complementary patterns for smart contract development: **Use a Facet when you want:** - The complete, standard implementation (e.g., full ERC-20 functionality) -- To deploy once and reuse across multiple diamonds +- To reuse it (onchain) across multiple diamonds - A verified, audited implementation **Use a Library when you're:** - Building a custom facet that needs to interact with Compose features - Extending standard functionality with custom logic -- Composing multiple Compose features together ### Practical Example @@ -122,20 +122,9 @@ contract GameNFTFacet { // Both facets operate on the SAME NFT collection! ``` -## Available Facets +## Available Facets & Libraries -Compose currently provides these production-ready facets: - -### Access Control - -- **OwnerFacet**: Single-step ownership (ERC-173) -- **OwnerTwoStepsFacet**: Two-step ownership transfer for added security - -### Token Standards - -- **ERC20Facet**: Complete ERC-20 implementation with permit support -- **ERC721Facet**: Full ERC-721 NFT implementation -- **ERC721EnumerableFacet**: ERC-721 with enumeration capabilities +Look in the [src directory](https://github.com/Perfect-Abstractions/Compose/tree/main/src) to see the currently provided functionality, facets and libraries. ### Diamond Infrastructure From f5ecec57a3670db5597e33c9db7a477141193c20 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Mon, 27 Oct 2025 16:51:30 -0400 Subject: [PATCH 04/29] add royalty tests --- test/token/Royalty/LibRoyalty.t.sol | 547 ++++++++++++++++++ test/token/Royalty/RoyaltyFacet.t.sol | 441 ++++++++++++++ .../Royalty/harnesses/LibRoyaltyHarness.sol | 59 ++ .../Royalty/harnesses/RoyaltyFacetHarness.sol | 38 ++ 4 files changed, 1085 insertions(+) create mode 100644 test/token/Royalty/LibRoyalty.t.sol create mode 100644 test/token/Royalty/RoyaltyFacet.t.sol create mode 100644 test/token/Royalty/harnesses/LibRoyaltyHarness.sol create mode 100644 test/token/Royalty/harnesses/RoyaltyFacetHarness.sol diff --git a/test/token/Royalty/LibRoyalty.t.sol b/test/token/Royalty/LibRoyalty.t.sol new file mode 100644 index 00000000..095a9ea2 --- /dev/null +++ b/test/token/Royalty/LibRoyalty.t.sol @@ -0,0 +1,547 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {LibRoyaltyHarness} from "./harnesses/LibRoyaltyHarness.sol"; +import {LibRoyalty} from "../../../src/token/Royalty/LibRoyalty.sol"; + +contract LibRoyaltyTest is Test { + LibRoyaltyHarness public harness; + + address public alice; + address public bob; + address public charlie; + address public royaltyReceiver; + + uint256 constant FEE_DENOMINATOR = 10000; + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + charlie = makeAddr("charlie"); + royaltyReceiver = makeAddr("royaltyReceiver"); + + harness = new LibRoyaltyHarness(); + } + + // ============================================ + // royaltyInfo Tests + // ============================================ + + function test_RoyaltyInfo_NoRoyaltySet() public view { + uint256 tokenId = 1; + uint256 salePrice = 1 ether; + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, address(0)); + assertEq(royaltyAmount, 0); + } + + function test_RoyaltyInfo_DefaultRoyaltyOnly() public { + uint256 tokenId = 1; + uint96 feeNumerator = 500; // 5% + uint256 salePrice = 100 ether; + + harness.setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, salePrice * feeNumerator / FEE_DENOMINATOR); + } + + function test_RoyaltyInfo_5PercentRoyalty() public { + uint256 tokenId = 1; + uint96 feeNumerator = 500; // 5% + uint256 salePrice = 100 ether; + // Calculation: (100 ether * 500) / 10000 = 5 ether + uint256 expectedRoyalty = 5 ether; + + harness.setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalty); + } + + function test_RoyaltyInfo_10PercentRoyalty() public { + uint96 feeNumerator = 1000; // 10% + uint256 salePrice = 50 ether; + // Calculation: (50 ether * 1000) / 10000 = 5 ether + uint256 expectedRoyalty = 5 ether; + + harness.setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalty); + } + + function test_RoyaltyInfo_50PercentRoyalty() public { + uint96 feeNumerator = 5000; // 50% + uint256 salePrice = 10 ether; + // Calculation: (10 ether * 5000) / 10000 = 5 ether + uint256 expectedRoyalty = 5 ether; + + harness.setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalty); + } + + function test_RoyaltyInfo_100PercentRoyalty() public { + uint96 feeNumerator = 10000; // 100% + uint256 salePrice = 1 ether; + // Calculation: (1 ether * 10000) / 10000 = 1 ether + uint256 expectedRoyalty = 1 ether; + + harness.setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalty); + } + + function test_RoyaltyInfo_TokenSpecificRoyalty() public { + uint256 tokenId = 5; + uint96 defaultFeeNumerator = 500; // 5% + uint96 tokenFeeNumerator = 750; // 7.5% + uint256 salePrice = 100 ether; + + harness.setDefaultRoyalty(alice, defaultFeeNumerator); + harness.setTokenRoyalty(tokenId, bob, tokenFeeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, bob); + // Calculation: (100 ether * 750) / 10000 = 7.5 ether + assertEq(royaltyAmount, 7.5 ether); + } + + function test_RoyaltyInfo_TokenSpecificOverridesDefault() public { + uint256 tokenId = 10; + uint96 defaultFeeNumerator = 1000; // 10% + uint96 tokenFeeNumerator = 250; // 2.5% + uint256 salePrice = 100 ether; + + harness.setDefaultRoyalty(alice, defaultFeeNumerator); + harness.setTokenRoyalty(tokenId, bob, tokenFeeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, bob); + // Calculation: (100 ether * 250) / 10000 = 2.5 ether + assertEq(royaltyAmount, 2.5 ether); + } + + function test_RoyaltyInfo_ZeroSalePrice() public { + uint96 feeNumerator = 500; // 5% + uint256 salePrice = 0; + + harness.setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, 0); + } + + function test_RoyaltyInfo_ZeroRoyaltyPercentage() public { + uint96 feeNumerator = 0; // 0% + uint256 salePrice = 100 ether; + + harness.setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, 0); + } + + function testFuzz_RoyaltyInfo_WithValidFee(uint96 feeNumerator, uint256 salePrice) public { + vm.assume(feeNumerator <= FEE_DENOMINATOR); + vm.assume(salePrice <= 1000000 ether); // Prevent overflow with reasonable maximum + + harness.setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, (salePrice * feeNumerator) / FEE_DENOMINATOR); + } + + function testFuzz_RoyaltyInfo_WithTokenRoyalty( + uint256 tokenId, + uint96 feeNumerator, + uint256 salePrice + ) public { + vm.assume(feeNumerator <= FEE_DENOMINATOR); + vm.assume(salePrice <= 1000000 ether); // Prevent overflow with reasonable maximum + + harness.setTokenRoyalty(tokenId, bob, feeNumerator); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, bob); + assertEq(royaltyAmount, (salePrice * feeNumerator) / FEE_DENOMINATOR); + } + + function test_RoyaltyInfo_MultipleTokensDifferentRoyalties() public { + uint256 token1 = 1; + uint256 token2 = 2; + uint256 token3 = 3; + uint256 salePrice = 100 ether; + + harness.setDefaultRoyalty(alice, 500); // 5% + harness.setTokenRoyalty(token1, bob, 1000); // 10% + harness.setTokenRoyalty(token2, charlie, 250); // 2.5% + + (address receiver1, uint256 royalty1) = harness.royaltyInfo(token1, salePrice); + (address receiver2, uint256 royalty2) = harness.royaltyInfo(token2, salePrice); + (address receiver3, uint256 royalty3) = harness.royaltyInfo(token3, salePrice); + + assertEq(receiver1, bob); + assertEq(royalty1, 10 ether); + + assertEq(receiver2, charlie); + assertEq(royalty2, 2.5 ether); + + assertEq(receiver3, alice); + assertEq(royalty3, 5 ether); + } + + function test_RoyaltyInfo_AfterResetTokenRoyalty() public { + uint256 tokenId = 1; + uint96 defaultFee = 500; // 5% + uint96 tokenFee = 1000; // 10% + uint256 salePrice = 100 ether; + + harness.setDefaultRoyalty(alice, defaultFee); + harness.setTokenRoyalty(tokenId, bob, tokenFee); + + (address receiver1, uint256 royalty1) = harness.royaltyInfo(tokenId, salePrice); + assertEq(receiver1, bob); + assertEq(royalty1, 10 ether); + + harness.resetTokenRoyalty(tokenId); + + (address receiver2, uint256 royalty2) = harness.royaltyInfo(tokenId, salePrice); + assertEq(receiver2, alice); + assertEq(royalty2, 5 ether); + } + + // ============================================ + // setDefaultRoyalty Tests + // ============================================ + + function test_SetDefaultRoyalty() public { + uint96 feeNumerator = 500; // 5% + + harness.setDefaultRoyalty(royaltyReceiver, feeNumerator); + + assertEq(harness.getDefaultRoyaltyReceiver(), royaltyReceiver); + assertEq(harness.getDefaultRoyaltyFraction(), feeNumerator); + } + + function test_SetDefaultRoyalty_UpdatesExisting() public { + harness.setDefaultRoyalty(alice, 500); + harness.setDefaultRoyalty(bob, 1000); + + assertEq(harness.getDefaultRoyaltyReceiver(), bob); + assertEq(harness.getDefaultRoyaltyFraction(), 1000); + } + + function test_SetDefaultRoyalty_ZeroPercentage() public { + harness.setDefaultRoyalty(royaltyReceiver, 0); + + assertEq(harness.getDefaultRoyaltyReceiver(), royaltyReceiver); + assertEq(harness.getDefaultRoyaltyFraction(), 0); + } + + function test_SetDefaultRoyalty_MaxPercentage() public { + uint96 maxFee = 10000; // 100% + + harness.setDefaultRoyalty(royaltyReceiver, maxFee); + + assertEq(harness.getDefaultRoyaltyReceiver(), royaltyReceiver); + assertEq(harness.getDefaultRoyaltyFraction(), maxFee); + } + + function testFuzz_SetDefaultRoyalty(address receiver, uint96 feeNumerator) public { + vm.assume(feeNumerator <= FEE_DENOMINATOR); + vm.assume(receiver != address(0)); + + harness.setDefaultRoyalty(receiver, feeNumerator); + + assertEq(harness.getDefaultRoyaltyReceiver(), receiver); + assertEq(harness.getDefaultRoyaltyFraction(), feeNumerator); + } + + function test_RevertWhen_SetDefaultRoyalty_InvalidFee() public { + uint96 invalidFee = 10001; // More than 100% + + vm.expectRevert( + abi.encodeWithSelector( + LibRoyalty.ERC2981InvalidDefaultRoyalty.selector, + invalidFee, + FEE_DENOMINATOR + ) + ); + harness.setDefaultRoyalty(royaltyReceiver, invalidFee); + } + + function test_RevertWhen_SetDefaultRoyalty_ZeroReceiver() public { + vm.expectRevert( + abi.encodeWithSelector(LibRoyalty.ERC2981InvalidDefaultRoyaltyReceiver.selector, address(0)) + ); + harness.setDefaultRoyalty(address(0), 500); + } + + // ============================================ + // deleteDefaultRoyalty Tests + // ============================================ + + function test_DeleteDefaultRoyalty() public { + harness.setDefaultRoyalty(royaltyReceiver, 500); + harness.deleteDefaultRoyalty(); + + assertEq(harness.getDefaultRoyaltyReceiver(), address(0)); + assertEq(harness.getDefaultRoyaltyFraction(), 0); + } + + function test_DeleteDefaultRoyalty_NoEffectOnTokenRoyalty() public { + uint256 tokenId = 1; + + harness.setDefaultRoyalty(alice, 500); + harness.setTokenRoyalty(tokenId, bob, 1000); + harness.deleteDefaultRoyalty(); + + assertEq(harness.getTokenRoyaltyReceiver(tokenId), bob); + assertEq(harness.getTokenRoyaltyFraction(tokenId), 1000); + } + + function test_DeleteDefaultRoyalty_RoyaltyInfoReturnsZero() public { + uint256 salePrice = 100 ether; + + harness.setDefaultRoyalty(royaltyReceiver, 500); + harness.deleteDefaultRoyalty(); + + (address receiver, uint256 royaltyAmount) = harness.royaltyInfo(1, salePrice); + + assertEq(receiver, address(0)); + assertEq(royaltyAmount, 0); + } + + // ============================================ + // setTokenRoyalty Tests + // ============================================ + + function test_SetTokenRoyalty() public { + uint256 tokenId = 1; + uint96 feeNumerator = 500; // 5% + + harness.setTokenRoyalty(tokenId, royaltyReceiver, feeNumerator); + + assertEq(harness.getTokenRoyaltyReceiver(tokenId), royaltyReceiver); + assertEq(harness.getTokenRoyaltyFraction(tokenId), feeNumerator); + } + + function test_SetTokenRoyalty_MultipleTokens() public { + harness.setTokenRoyalty(1, alice, 500); + harness.setTokenRoyalty(2, bob, 1000); + harness.setTokenRoyalty(3, charlie, 250); + + assertEq(harness.getTokenRoyaltyReceiver(1), alice); + assertEq(harness.getTokenRoyaltyFraction(1), 500); + + assertEq(harness.getTokenRoyaltyReceiver(2), bob); + assertEq(harness.getTokenRoyaltyFraction(2), 1000); + + assertEq(harness.getTokenRoyaltyReceiver(3), charlie); + assertEq(harness.getTokenRoyaltyFraction(3), 250); + } + + function test_SetTokenRoyalty_UpdatesExisting() public { + uint256 tokenId = 1; + + harness.setTokenRoyalty(tokenId, alice, 500); + harness.setTokenRoyalty(tokenId, bob, 1000); + + assertEq(harness.getTokenRoyaltyReceiver(tokenId), bob); + assertEq(harness.getTokenRoyaltyFraction(tokenId), 1000); + } + + function test_SetTokenRoyalty_ZeroPercentage() public { + uint256 tokenId = 1; + + harness.setTokenRoyalty(tokenId, royaltyReceiver, 0); + + assertEq(harness.getTokenRoyaltyReceiver(tokenId), royaltyReceiver); + assertEq(harness.getTokenRoyaltyFraction(tokenId), 0); + } + + function test_SetTokenRoyalty_MaxPercentage() public { + uint256 tokenId = 1; + uint96 maxFee = 10000; // 100% + + harness.setTokenRoyalty(tokenId, royaltyReceiver, maxFee); + + assertEq(harness.getTokenRoyaltyReceiver(tokenId), royaltyReceiver); + assertEq(harness.getTokenRoyaltyFraction(tokenId), maxFee); + } + + function testFuzz_SetTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) public { + vm.assume(feeNumerator <= FEE_DENOMINATOR); + vm.assume(receiver != address(0)); + + harness.setTokenRoyalty(tokenId, receiver, feeNumerator); + + assertEq(harness.getTokenRoyaltyReceiver(tokenId), receiver); + assertEq(harness.getTokenRoyaltyFraction(tokenId), feeNumerator); + } + + function test_RevertWhen_SetTokenRoyalty_InvalidFee() public { + uint256 tokenId = 1; + uint96 invalidFee = 10001; // More than 100% + + vm.expectRevert( + abi.encodeWithSelector( + LibRoyalty.ERC2981InvalidTokenRoyalty.selector, + tokenId, + invalidFee, + FEE_DENOMINATOR + ) + ); + harness.setTokenRoyalty(tokenId, royaltyReceiver, invalidFee); + } + + function test_RevertWhen_SetTokenRoyalty_ZeroReceiver() public { + uint256 tokenId = 1; + + vm.expectRevert( + abi.encodeWithSelector( + LibRoyalty.ERC2981InvalidTokenRoyaltyReceiver.selector, + tokenId, + address(0) + ) + ); + harness.setTokenRoyalty(tokenId, address(0), 500); + } + + // ============================================ + // resetTokenRoyalty Tests + // ============================================ + + function test_ResetTokenRoyalty() public { + uint256 tokenId = 1; + + harness.setTokenRoyalty(tokenId, royaltyReceiver, 500); + harness.resetTokenRoyalty(tokenId); + + assertEq(harness.getTokenRoyaltyReceiver(tokenId), address(0)); + assertEq(harness.getTokenRoyaltyFraction(tokenId), 0); + } + + function test_ResetTokenRoyalty_FallsBackToDefault() public { + uint256 tokenId = 1; + + harness.setDefaultRoyalty(alice, 500); + harness.setTokenRoyalty(tokenId, bob, 1000); + + assertEq(harness.getTokenRoyaltyReceiver(tokenId), bob); + assertEq(harness.getTokenRoyaltyFraction(tokenId), 1000); + + harness.resetTokenRoyalty(tokenId); + + assertEq(harness.getTokenRoyaltyReceiver(tokenId), address(0)); + assertEq(harness.getTokenRoyaltyFraction(tokenId), 0); + } + + function test_ResetTokenRoyalty_MultipleTokens() public { + harness.setTokenRoyalty(1, alice, 500); + harness.setTokenRoyalty(2, bob, 1000); + harness.setTokenRoyalty(3, charlie, 250); + + harness.resetTokenRoyalty(1); + harness.resetTokenRoyalty(3); + + assertEq(harness.getTokenRoyaltyReceiver(1), address(0)); + assertEq(harness.getTokenRoyaltyReceiver(2), bob); + assertEq(harness.getTokenRoyaltyReceiver(3), address(0)); + } + + function testFuzz_ResetTokenRoyalty(uint256 tokenId, uint96 feeNumerator) public { + vm.assume(feeNumerator <= FEE_DENOMINATOR); + + harness.setTokenRoyalty(tokenId, royaltyReceiver, feeNumerator); + harness.resetTokenRoyalty(tokenId); + + assertEq(harness.getTokenRoyaltyReceiver(tokenId), address(0)); + assertEq(harness.getTokenRoyaltyFraction(tokenId), 0); + } + + // ============================================ + // Integration Tests + // ============================================ + + function test_SetDefaultThenTokenThenReset() public { + uint256 tokenId = 5; + uint256 salePrice = 100 ether; + + harness.setDefaultRoyalty(alice, 500); // 5% + + (address receiver1, uint256 royalty1) = harness.royaltyInfo(tokenId, salePrice); + assertEq(receiver1, alice); + assertEq(royalty1, 5 ether); + + harness.setTokenRoyalty(tokenId, bob, 1000); // 10% + + (address receiver2, uint256 royalty2) = harness.royaltyInfo(tokenId, salePrice); + assertEq(receiver2, bob); + assertEq(royalty2, 10 ether); + + harness.resetTokenRoyalty(tokenId); + + (address receiver3, uint256 royalty3) = harness.royaltyInfo(tokenId, salePrice); + assertEq(receiver3, alice); + assertEq(royalty3, 5 ether); + } + + function test_ComplexRoyaltyFlow() public { + uint256 token1 = 1; + uint256 token2 = 2; + uint256 token3 = 3; + uint256 salePrice = 100 ether; + + harness.setDefaultRoyalty(alice, 500); // 5% default + + harness.setTokenRoyalty(token1, bob, 1000); // 10% + harness.setTokenRoyalty(token2, charlie, 250); // 2.5% + + (address receiver1, uint256 royalty1) = harness.royaltyInfo(token1, salePrice); + (address receiver2, uint256 royalty2) = harness.royaltyInfo(token2, salePrice); + (address receiver3, uint256 royalty3) = harness.royaltyInfo(token3, salePrice); + + assertEq(receiver1, bob); + assertEq(royalty1, 10 ether); + + assertEq(receiver2, charlie); + assertEq(royalty2, 2.5 ether); + + assertEq(receiver3, alice); + assertEq(royalty3, 5 ether); + + harness.deleteDefaultRoyalty(); + + (receiver3, royalty3) = harness.royaltyInfo(token3, salePrice); + assertEq(receiver3, address(0)); + assertEq(royalty3, 0); + } +} + diff --git a/test/token/Royalty/RoyaltyFacet.t.sol b/test/token/Royalty/RoyaltyFacet.t.sol new file mode 100644 index 00000000..36221d10 --- /dev/null +++ b/test/token/Royalty/RoyaltyFacet.t.sol @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {RoyaltyFacet} from "../../../src/token/Royalty/RoyaltyFacet.sol"; +import {RoyaltyFacetHarness} from "./harnesses/RoyaltyFacetHarness.sol"; + +contract RoyaltyFacetTest is Test { + RoyaltyFacetHarness public facet; + + address public alice; + address public bob; + address public charlie; + address public royaltyReceiver; + + uint256 constant FEE_DENOMINATOR = 10000; + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + charlie = makeAddr("charlie"); + royaltyReceiver = makeAddr("royaltyReceiver"); + + facet = new RoyaltyFacetHarness(); + } + + // ============================================ + // Helper Functions + // ============================================ + + /// @notice Helper to set default royalty + function _setDefaultRoyalty(address _receiver, uint96 _feeNumerator) internal { + facet.setDefaultRoyalty(_receiver, _feeNumerator); + } + + /// @notice Helper to set token royalty + function _setTokenRoyalty(uint256 _tokenId, address _receiver, uint96 _feeNumerator) internal { + facet.setTokenRoyalty(_tokenId, _receiver, _feeNumerator); + } + + // ============================================ + // royaltyInfo Tests + // ============================================ + + function test_RoyaltyInfo_NoRoyaltySet() public view { + uint256 tokenId = 1; + uint256 salePrice = 1 ether; + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, address(0)); + assertEq(royaltyAmount, 0); + } + + function test_RoyaltyInfo_DefaultRoyaltyOnly() public { + uint96 feeNumerator = 500; // 5% + uint256 salePrice = 100 ether; + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, salePrice * feeNumerator / FEE_DENOMINATOR); + } + + function test_RoyaltyInfo_5PercentRoyalty() public { + uint96 feeNumerator = 500; // 5% + uint256 salePrice = 100 ether; + uint256 expectedRoyalty = 5 ether; + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalty); + } + + function test_RoyaltyInfo_10PercentRoyalty() public { + uint96 feeNumerator = 1000; // 10% + uint256 salePrice = 50 ether; + uint256 expectedRoyalty = 5 ether; + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalty); + } + + function test_RoyaltyInfo_50PercentRoyalty() public { + uint96 feeNumerator = 5000; // 50% + uint256 salePrice = 10 ether; + uint256 expectedRoyalty = 5 ether; + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalty); + } + + function test_RoyaltyInfo_100PercentRoyalty() public { + uint96 feeNumerator = 10000; // 100% + uint256 salePrice = 1 ether; + uint256 expectedRoyalty = 1 ether; + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalty); + } + + function test_RoyaltyInfo_TokenSpecificRoyalty() public { + uint256 tokenId = 5; + uint96 tokenFeeNumerator = 750; // 7.5% + uint256 salePrice = 100 ether; + + _setTokenRoyalty(tokenId, bob, tokenFeeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, bob); + assertEq(royaltyAmount, 7.5 ether); + } + + function test_RoyaltyInfo_TokenSpecificOverridesDefault() public { + uint256 tokenId = 10; + uint96 defaultFeeNumerator = 1000; // 10% + uint96 tokenFeeNumerator = 250; // 2.5% + uint256 salePrice = 100 ether; + + _setDefaultRoyalty(alice, defaultFeeNumerator); + _setTokenRoyalty(tokenId, bob, tokenFeeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, bob); + assertEq(royaltyAmount, 2.5 ether); + } + + function test_RoyaltyInfo_ZeroSalePrice() public { + uint96 feeNumerator = 500; // 5% + uint256 salePrice = 0; + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, 0); + } + + function test_RoyaltyInfo_ZeroRoyaltyPercentage() public { + uint96 feeNumerator = 0; // 0% + uint256 salePrice = 100 ether; + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, 0); + } + + function testFuzz_RoyaltyInfo_WithValidFee(uint96 feeNumerator, uint256 salePrice) public { + vm.assume(feeNumerator <= FEE_DENOMINATOR); + vm.assume(salePrice <= 1000000 ether); // Prevent overflow with reasonable maximum + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, (salePrice * feeNumerator) / FEE_DENOMINATOR); + } + + function testFuzz_RoyaltyInfo_WithTokenRoyalty( + uint256 tokenId, + uint96 feeNumerator, + uint256 salePrice + ) public { + vm.assume(feeNumerator <= FEE_DENOMINATOR); + vm.assume(salePrice <= 1000000 ether); // Prevent overflow with reasonable maximum + + _setTokenRoyalty(tokenId, bob, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, bob); + assertEq(royaltyAmount, (salePrice * feeNumerator) / FEE_DENOMINATOR); + } + + function test_RoyaltyInfo_MultipleTokensDifferentRoyalties() public { + uint256 token1 = 1; + uint256 token2 = 2; + uint256 token3 = 3; + uint256 salePrice = 100 ether; + + _setDefaultRoyalty(alice, 500); // 5% + _setTokenRoyalty(token1, bob, 1000); // 10% + _setTokenRoyalty(token2, charlie, 250); // 2.5% + + (address receiver1, uint256 royalty1) = facet.royaltyInfo(token1, salePrice); + (address receiver2, uint256 royalty2) = facet.royaltyInfo(token2, salePrice); + (address receiver3, uint256 royalty3) = facet.royaltyInfo(token3, salePrice); + + assertEq(receiver1, bob); + assertEq(royalty1, 10 ether); + + assertEq(receiver2, charlie); + assertEq(royalty2, 2.5 ether); + + assertEq(receiver3, alice); + assertEq(royalty3, 5 ether); + } + + function test_RoyaltyInfo_FractionalRoyalty() public { + uint96 feeNumerator = 1; // 0.01% + uint256 salePrice = 100000 ether; + uint256 expectedRoyalty = 10 ether; + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalty); + } + + function test_RoyaltyInfo_LargeSalePrice() public { + uint96 feeNumerator = 500; // 5% + uint256 salePrice = 1000000000 ether; // Large but safe value + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, salePrice * feeNumerator / FEE_DENOMINATOR); + } + + function test_RoyaltyInfo_VariousFeePercentages() public { + uint256 salePrice = 1 ether; + + uint96[] memory fees = new uint96[](6); + fees[0] = 1; // 0.01% + fees[1] = 25; // 0.25% + fees[2] = 100; // 1% + fees[3] = 250; // 2.5% + fees[4] = 500; // 5% + fees[5] = 750; // 7.5% + + uint256[] memory expectedRoyalties = new uint256[](6); + expectedRoyalties[0] = 0.0001 ether; + expectedRoyalties[1] = 0.0025 ether; + expectedRoyalties[2] = 0.01 ether; + expectedRoyalties[3] = 0.025 ether; + expectedRoyalties[4] = 0.05 ether; + expectedRoyalties[5] = 0.075 ether; + + for (uint256 i = 0; i < fees.length; i++) { + _setDefaultRoyalty(royaltyReceiver, fees[i]); + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalties[i]); + } + } + + function test_RoyaltyInfo_SmallTokenId() public { + uint256 tokenId = 0; + uint96 feeNumerator = 500; // 5% + uint256 salePrice = 100 ether; + + _setTokenRoyalty(tokenId, royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, 5 ether); + } + + function test_RoyaltyInfo_VeryLargeTokenId() public { + uint256 tokenId = type(uint256).max; + uint96 feeNumerator = 1000; // 10% + uint256 salePrice = 50 ether; + + _setTokenRoyalty(tokenId, royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, 5 ether); + } + + function test_RoyaltyInfo_FallsBackWhenTokenRoyaltyNotSet() public { + uint256 tokenId = 999; + uint96 defaultFee = 750; // 7.5% + uint256 salePrice = 100 ether; + + _setDefaultRoyalty(royaltyReceiver, defaultFee); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(tokenId, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, 7.5 ether); + } + + function test_RoyaltyInfo_DefaultRoyaltyAddress() public { + uint256 salePrice = 100 ether; + uint96 feeNumerator = 500; // 5% + + _setDefaultRoyalty(alice, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, alice); + assertEq(royaltyAmount, 5 ether); + } + + function test_RoyaltyInfo_ZeroAddressReceiverReturnsZero() public { + // Set up a token royalty with non-zero fee but zero address + // This simulates the edge case + uint256 tokenId = 5; + uint256 salePrice = 100 ether; + + // No royalty set - should return zero + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(tokenId, salePrice); + assertEq(receiver, address(0)); + assertEq(royaltyAmount, 0); + } + + // ============================================ + // Storage Consistency Tests + // ============================================ + + function test_StorageSlot_Consistency() public { + uint96 feeNumerator = 500; // 5% + + facet.setDefaultRoyalty(royaltyReceiver, feeNumerator); + + // Read back through facet function to verify + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(999, 100 ether); + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, 5 ether); + } + + function test_StorageSlot_TokenRoyaltyConsistency() public { + uint256 tokenId = 42; + uint96 feeNumerator = 1000; // 10% + + facet.setTokenRoyalty(tokenId, bob, feeNumerator); + + // Read back through facet function to verify + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(tokenId, 100 ether); + assertEq(receiver, bob); + assertEq(royaltyAmount, 10 ether); + } + + // ============================================ + // Edge Cases + // ============================================ + + function test_RoyaltyInfo_WithMaximumValues() public { + uint96 maxFee = 10000; // 100% + uint256 maxSalePrice = 1000000000 ether; // Large but safe value + + _setDefaultRoyalty(royaltyReceiver, maxFee); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, maxSalePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, maxSalePrice); + } + + function test_RoyaltyInfo_VariousTokenIds() public { + uint96 feeNumerator = 500; // 5% + uint256 salePrice = 100 ether; + + _setTokenRoyalty(1, alice, feeNumerator); + _setTokenRoyalty(100, bob, feeNumerator); + _setTokenRoyalty(999999, charlie, feeNumerator); + + (address receiver1, uint256 royalty1) = facet.royaltyInfo(1, salePrice); + (address receiver2, uint256 royalty2) = facet.royaltyInfo(100, salePrice); + (address receiver3, uint256 royalty3) = facet.royaltyInfo(999999, salePrice); + + assertEq(receiver1, alice); + assertEq(royalty1, 5 ether); + + assertEq(receiver2, bob); + assertEq(royalty2, 5 ether); + + assertEq(receiver3, charlie); + assertEq(royalty3, 5 ether); + } + + function test_RoyaltyInfo_MinimalRoyaltyFee() public { + uint96 feeNumerator = 1; // 0.01% + uint256 salePrice = 1 ether; + uint256 expectedRoyalty = 0.0001 ether; // 0.0001 ETH + + _setDefaultRoyalty(royaltyReceiver, feeNumerator); + + (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, salePrice); + + assertEq(receiver, royaltyReceiver); + assertEq(royaltyAmount, expectedRoyalty); + } + + function test_ComplexScenario_MultipleTokensAndDefaults() public { + // Set up complex royalty structure + _setDefaultRoyalty(alice, 500); // 5% default + + _setTokenRoyalty(1, bob, 1000); // Token 1: 10% + _setTokenRoyalty(2, charlie, 250); // Token 2: 2.5% + + uint256 salePrice = 100 ether; + + // Token 1 should use token-specific + (address receiver1, uint256 royalty1) = facet.royaltyInfo(1, salePrice); + assertEq(receiver1, bob); + assertEq(royalty1, 10 ether); + + // Token 2 should use token-specific + (address receiver2, uint256 royalty2) = facet.royaltyInfo(2, salePrice); + assertEq(receiver2, charlie); + assertEq(royalty2, 2.5 ether); + + // Token 999 should use default + (address receiver3, uint256 royalty3) = facet.royaltyInfo(999, salePrice); + assertEq(receiver3, alice); + assertEq(royalty3, 5 ether); + } +} + diff --git a/test/token/Royalty/harnesses/LibRoyaltyHarness.sol b/test/token/Royalty/harnesses/LibRoyaltyHarness.sol new file mode 100644 index 00000000..60a72825 --- /dev/null +++ b/test/token/Royalty/harnesses/LibRoyaltyHarness.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {LibRoyalty} from "../../../../src/token/Royalty/LibRoyalty.sol"; + +/// @title LibRoyaltyHarness +/// @notice Test harness that exposes LibRoyalty's internal functions as external +/// @dev Required for testing since LibRoyalty only has internal functions +contract LibRoyaltyHarness { + /// @notice Exposes LibRoyalty.royaltyInfo as an external function + function royaltyInfo(uint256 _tokenId, uint256 _salePrice) + external + view + returns (address receiver, uint256 royaltyAmount) + { + return LibRoyalty.royaltyInfo(_tokenId, _salePrice); + } + + /// @notice Exposes LibRoyalty.setDefaultRoyalty as an external function + function setDefaultRoyalty(address _receiver, uint96 _feeNumerator) external { + LibRoyalty.setDefaultRoyalty(_receiver, _feeNumerator); + } + + /// @notice Exposes LibRoyalty.deleteDefaultRoyalty as an external function + function deleteDefaultRoyalty() external { + LibRoyalty.deleteDefaultRoyalty(); + } + + /// @notice Exposes LibRoyalty.setTokenRoyalty as an external function + function setTokenRoyalty(uint256 _tokenId, address _receiver, uint96 _feeNumerator) external { + LibRoyalty.setTokenRoyalty(_tokenId, _receiver, _feeNumerator); + } + + /// @notice Exposes LibRoyalty.resetTokenRoyalty as an external function + function resetTokenRoyalty(uint256 _tokenId) external { + LibRoyalty.resetTokenRoyalty(_tokenId); + } + + /// @notice Get default royalty receiver for testing + function getDefaultRoyaltyReceiver() external view returns (address) { + return LibRoyalty.getStorage().defaultRoyaltyInfo.receiver; + } + + /// @notice Get default royalty fraction for testing + function getDefaultRoyaltyFraction() external view returns (uint96) { + return LibRoyalty.getStorage().defaultRoyaltyInfo.royaltyFraction; + } + + /// @notice Get token-specific royalty receiver for testing + function getTokenRoyaltyReceiver(uint256 _tokenId) external view returns (address) { + return LibRoyalty.getStorage().tokenRoyaltyInfo[_tokenId].receiver; + } + + /// @notice Get token-specific royalty fraction for testing + function getTokenRoyaltyFraction(uint256 _tokenId) external view returns (uint96) { + return LibRoyalty.getStorage().tokenRoyaltyInfo[_tokenId].royaltyFraction; + } +} + diff --git a/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol b/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol new file mode 100644 index 00000000..22955ae5 --- /dev/null +++ b/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {RoyaltyFacet} from "../../../../src/token/Royalty/RoyaltyFacet.sol"; + +/// @title RoyaltyFacetHarness +/// @notice Test harness for RoyaltyFacet +/// @dev Adds helper functions to set up royalty state for testing +contract RoyaltyFacetHarness is RoyaltyFacet { + /// @notice Set default royalty (for testing) + /// @dev Directly manipulates storage to set default royalty info + function setDefaultRoyalty(address _receiver, uint96 _feeNumerator) external { + RoyaltyStorage storage s = getStorage(); + s.defaultRoyaltyInfo = RoyaltyInfo(_receiver, _feeNumerator); + } + + /// @notice Set token royalty (for testing) + /// @dev Directly manipulates storage to set token-specific royalty info + function setTokenRoyalty(uint256 _tokenId, address _receiver, uint96 _feeNumerator) external { + RoyaltyStorage storage s = getStorage(); + s.tokenRoyaltyInfo[_tokenId] = RoyaltyInfo(_receiver, _feeNumerator); + } + + /// @notice Reset token royalty (for testing) + /// @dev Removes token-specific royalty info + function resetTokenRoyalty(uint256 _tokenId) external { + RoyaltyStorage storage s = getStorage(); + delete s.tokenRoyaltyInfo[_tokenId]; + } + + /// @notice Delete default royalty (for testing) + /// @dev Removes default royalty info + function deleteDefaultRoyalty() external { + RoyaltyStorage storage s = getStorage(); + delete s.defaultRoyaltyInfo; + } +} + From 3f48b3f89870625770f2dc5bdc448274152c06c1 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Mon, 27 Oct 2025 16:54:50 -0400 Subject: [PATCH 05/29] format test --- test/token/Royalty/LibRoyalty.t.sol | 29 +++++------------------- test/token/Royalty/RoyaltyFacet.t.sol | 32 ++++++++++++--------------- 2 files changed, 19 insertions(+), 42 deletions(-) diff --git a/test/token/Royalty/LibRoyalty.t.sol b/test/token/Royalty/LibRoyalty.t.sol index 095a9ea2..31a3b5a0 100644 --- a/test/token/Royalty/LibRoyalty.t.sol +++ b/test/token/Royalty/LibRoyalty.t.sol @@ -176,11 +176,7 @@ contract LibRoyaltyTest is Test { assertEq(royaltyAmount, (salePrice * feeNumerator) / FEE_DENOMINATOR); } - function testFuzz_RoyaltyInfo_WithTokenRoyalty( - uint256 tokenId, - uint96 feeNumerator, - uint256 salePrice - ) public { + function testFuzz_RoyaltyInfo_WithTokenRoyalty(uint256 tokenId, uint96 feeNumerator, uint256 salePrice) public { vm.assume(feeNumerator <= FEE_DENOMINATOR); vm.assume(salePrice <= 1000000 ether); // Prevent overflow with reasonable maximum @@ -287,19 +283,13 @@ contract LibRoyaltyTest is Test { uint96 invalidFee = 10001; // More than 100% vm.expectRevert( - abi.encodeWithSelector( - LibRoyalty.ERC2981InvalidDefaultRoyalty.selector, - invalidFee, - FEE_DENOMINATOR - ) + abi.encodeWithSelector(LibRoyalty.ERC2981InvalidDefaultRoyalty.selector, invalidFee, FEE_DENOMINATOR) ); harness.setDefaultRoyalty(royaltyReceiver, invalidFee); } function test_RevertWhen_SetDefaultRoyalty_ZeroReceiver() public { - vm.expectRevert( - abi.encodeWithSelector(LibRoyalty.ERC2981InvalidDefaultRoyaltyReceiver.selector, address(0)) - ); + vm.expectRevert(abi.encodeWithSelector(LibRoyalty.ERC2981InvalidDefaultRoyaltyReceiver.selector, address(0))); harness.setDefaultRoyalty(address(0), 500); } @@ -411,12 +401,7 @@ contract LibRoyaltyTest is Test { uint96 invalidFee = 10001; // More than 100% vm.expectRevert( - abi.encodeWithSelector( - LibRoyalty.ERC2981InvalidTokenRoyalty.selector, - tokenId, - invalidFee, - FEE_DENOMINATOR - ) + abi.encodeWithSelector(LibRoyalty.ERC2981InvalidTokenRoyalty.selector, tokenId, invalidFee, FEE_DENOMINATOR) ); harness.setTokenRoyalty(tokenId, royaltyReceiver, invalidFee); } @@ -425,11 +410,7 @@ contract LibRoyaltyTest is Test { uint256 tokenId = 1; vm.expectRevert( - abi.encodeWithSelector( - LibRoyalty.ERC2981InvalidTokenRoyaltyReceiver.selector, - tokenId, - address(0) - ) + abi.encodeWithSelector(LibRoyalty.ERC2981InvalidTokenRoyaltyReceiver.selector, tokenId, address(0)) ); harness.setTokenRoyalty(tokenId, address(0), 500); } diff --git a/test/token/Royalty/RoyaltyFacet.t.sol b/test/token/Royalty/RoyaltyFacet.t.sol index 36221d10..712f283a 100644 --- a/test/token/Royalty/RoyaltyFacet.t.sol +++ b/test/token/Royalty/RoyaltyFacet.t.sol @@ -180,11 +180,7 @@ contract RoyaltyFacetTest is Test { assertEq(royaltyAmount, (salePrice * feeNumerator) / FEE_DENOMINATOR); } - function testFuzz_RoyaltyInfo_WithTokenRoyalty( - uint256 tokenId, - uint96 feeNumerator, - uint256 salePrice - ) public { + function testFuzz_RoyaltyInfo_WithTokenRoyalty(uint256 tokenId, uint96 feeNumerator, uint256 salePrice) public { vm.assume(feeNumerator <= FEE_DENOMINATOR); vm.assume(salePrice <= 1000000 ether); // Prevent overflow with reasonable maximum @@ -247,14 +243,14 @@ contract RoyaltyFacetTest is Test { function test_RoyaltyInfo_VariousFeePercentages() public { uint256 salePrice = 1 ether; - + uint96[] memory fees = new uint96[](6); - fees[0] = 1; // 0.01% - fees[1] = 25; // 0.25% - fees[2] = 100; // 1% - fees[3] = 250; // 2.5% - fees[4] = 500; // 5% - fees[5] = 750; // 7.5% + fees[0] = 1; // 0.01% + fees[1] = 25; // 0.25% + fees[2] = 100; // 1% + fees[3] = 250; // 2.5% + fees[4] = 500; // 5% + fees[5] = 750; // 7.5% uint256[] memory expectedRoyalties = new uint256[](6); expectedRoyalties[0] = 0.0001 ether; @@ -341,7 +337,7 @@ contract RoyaltyFacetTest is Test { function test_StorageSlot_Consistency() public { uint96 feeNumerator = 500; // 5% - + facet.setDefaultRoyalty(royaltyReceiver, feeNumerator); // Read back through facet function to verify @@ -369,7 +365,7 @@ contract RoyaltyFacetTest is Test { function test_RoyaltyInfo_WithMaximumValues() public { uint96 maxFee = 10000; // 100% uint256 maxSalePrice = 1000000000 ether; // Large but safe value - + _setDefaultRoyalty(royaltyReceiver, maxFee); (address receiver, uint256 royaltyAmount) = facet.royaltyInfo(1, maxSalePrice); @@ -392,10 +388,10 @@ contract RoyaltyFacetTest is Test { assertEq(receiver1, alice); assertEq(royalty1, 5 ether); - + assertEq(receiver2, bob); assertEq(royalty2, 5 ether); - + assertEq(receiver3, charlie); assertEq(royalty3, 5 ether); } @@ -416,10 +412,10 @@ contract RoyaltyFacetTest is Test { function test_ComplexScenario_MultipleTokensAndDefaults() public { // Set up complex royalty structure _setDefaultRoyalty(alice, 500); // 5% default - + _setTokenRoyalty(1, bob, 1000); // Token 1: 10% _setTokenRoyalty(2, charlie, 250); // Token 2: 2.5% - + uint256 salePrice = 100 ether; // Token 1 should use token-specific From f2551078cf528a1d08ce76a7a3a0e2ba3c39b9a9 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Tue, 28 Oct 2025 11:13:37 -0400 Subject: [PATCH 06/29] adjust to adams feedback --- test/token/Royalty/RoyaltyFacet.t.sol | 2 +- .../Royalty/harnesses/RoyaltyFacetHarness.sol | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/test/token/Royalty/RoyaltyFacet.t.sol b/test/token/Royalty/RoyaltyFacet.t.sol index 712f283a..e6e9fef4 100644 --- a/test/token/Royalty/RoyaltyFacet.t.sol +++ b/test/token/Royalty/RoyaltyFacet.t.sol @@ -319,7 +319,7 @@ contract RoyaltyFacetTest is Test { assertEq(royaltyAmount, 5 ether); } - function test_RoyaltyInfo_ZeroAddressReceiverReturnsZero() public { + function test_RoyaltyInfo_ZeroAddressReceiverReturnsZero() public view { // Set up a token royalty with non-zero fee but zero address // This simulates the edge case uint256 tokenId = 5; diff --git a/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol b/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol index 22955ae5..a2e3dfbd 100644 --- a/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol +++ b/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol @@ -20,19 +20,5 @@ contract RoyaltyFacetHarness is RoyaltyFacet { RoyaltyStorage storage s = getStorage(); s.tokenRoyaltyInfo[_tokenId] = RoyaltyInfo(_receiver, _feeNumerator); } - - /// @notice Reset token royalty (for testing) - /// @dev Removes token-specific royalty info - function resetTokenRoyalty(uint256 _tokenId) external { - RoyaltyStorage storage s = getStorage(); - delete s.tokenRoyaltyInfo[_tokenId]; - } - - /// @notice Delete default royalty (for testing) - /// @dev Removes default royalty info - function deleteDefaultRoyalty() external { - RoyaltyStorage storage s = getStorage(); - delete s.defaultRoyaltyInfo; - } } From 2022c170712cc43c1c78fb3f1ca9ff46d9b497b1 Mon Sep 17 00:00:00 2001 From: Jayy4rl Date: Tue, 28 Oct 2025 14:57:26 +0100 Subject: [PATCH 07/29] docs: add STYLE.md and update docs to reference coding style guide (closes #103) --- CONTRIBUTING.md | 3 +++ STYLE.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 STYLE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f699c871..ea0377f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,6 +100,9 @@ Once you have a clear idea of what you want to do, [create an issue](https://git ## Code Standards +### Coding Style Guide +All code must follow the Compose coding style guide. See [STYLE.md](STYLE.md) for required conventions, rules, and examples. + ### Solidity Feature Ban **IMPORTANT**: This library has strict rules about which Solidity features can be used. Anyone submitting a pull request that uses any of the banned features will be fined **$100 USDC**. diff --git a/STYLE.md b/STYLE.md new file mode 100644 index 00000000..f101445b --- /dev/null +++ b/STYLE.md @@ -0,0 +1,52 @@ +# Compose Coding Style Guide + +This style guide documents the coding conventions required for all Compose code. All contributors must follow these rules to ensure consistency and readability. + +## 1. Naming Conventions +- **Parameter Names:** All parameters for events, errors, and functions must be preceded with an underscore (`_`). + - Example: + ```solidity + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + error ERC20InvalidSender(address _sender); + function transfer(address _to, uint256 _amount) external {} + ``` +- **Camel Case:** Use camelCase for variable, function, contract, and library names, except for standard uppercase abbreviations (e.g., ERC). + - Example: `totalSupply`, `LibERC20`, `ERC721Facet` + +## 2. Control Structures +- **Brackets Required:** One-line `if` statements without code block brackets `{}` are not allowed. Always use a newline and brackets. + - Example: + ```solidity + // Bad + if (x > 0) return; + // Good + if (x > 0) { + return; + } + ``` + +## 3. Internal Functions +- **Facets:** Internal functions in facets must be marked `internal`. There should be few or no internal functions in facets; repeat code if it improves readability. +- **Libraries:** Internal functions in libraries are not preceded by any visibility keyword (all are internal). + +## 4. Value Resetting +- Use `delete` to set a value to zero. + - Example: + ```solidity + delete balances[_owner]; + ``` + +## 5. Formatting +- Format code using the default settings of `forge fmt`. Run `forge fmt` before submitting code. + +## 6. References and Examples +- For more examples, see: + - [`src/token/ERC721/ERC721/ERC721Facet.sol`](src/token/ERC721/ERC721/ERC721Facet.sol) + - [`src/token/ERC721/ERC721/LibERC721.sol`](src/token/ERC721/ERC721/LibERC721.sol) + +## 7. Additional Rules +- More rules may be derived from the above example files. When in doubt, follow the patterns established in those files. + +--- + +**All contributors must follow this style guide.** From b01b7ec797fc5a376804e8a9cb8946e5cb670fb0 Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 11:32:44 -0400 Subject: [PATCH 08/29] Improved STYLE.md --- STYLE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/STYLE.md b/STYLE.md index f101445b..5064c0ee 100644 --- a/STYLE.md +++ b/STYLE.md @@ -26,8 +26,8 @@ This style guide documents the coding conventions required for all Compose code. ``` ## 3. Internal Functions -- **Facets:** Internal functions in facets must be marked `internal`. There should be few or no internal functions in facets; repeat code if it improves readability. -- **Libraries:** Internal functions in libraries are not preceded by any visibility keyword (all are internal). +- **Facets:** Internal function names in facets should be prefixed with `internal` if they otherwise have the same name as an external function in the same facet. Usually should be few or no internal functions in facets; repeat code if it improves readability. +- **Libraries:** All functions in libraries use the `internal` visibility specifier. ## 4. Value Resetting - Use `delete` to set a value to zero. From abe813bc10e2fc8a626025a4b9b0604375fb6a63 Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 11:52:08 -0400 Subject: [PATCH 09/29] improved STYLE.md --- STYLE.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/STYLE.md b/STYLE.md index 5064c0ee..a3079bd2 100644 --- a/STYLE.md +++ b/STYLE.md @@ -2,6 +2,32 @@ This style guide documents the coding conventions required for all Compose code. All contributors must follow these rules to ensure consistency and readability. + +## 1. No Imports in Facets +Facets in Compose are self-contained, stand-alone smart contracts. Keep the code in the facet and make it as readable as possible, + +Importing other files into Compose facets is not allowed. +- Example: + ```solidity + // This is not allowed in Compose' facets or libraries + import {LibOwner} from "../../../src/access/Owner/LibOwner.sol"; + ``` + +## 1. Facets Are Read From The Top Down +Put your code in the facet in a way that it can be read from the top of the file and down to the bottom of the file, without having to jump to any other place in the file. + +This means that anything must be defined first before it used in a facet. This makes it easier to read a facet because the reader doesn't have to jump around the file to see what things are. In addition, it makes the code base consistent in how it is written and read. + +## 2. No Inheritance + +Facets may not inherit other contracts or interfaces. + +- Example: + ```solidity + // This is not allowed in Compose' facets or libraries + contract ERC721Facet is IERCFacet { + ``` + ## 1. Naming Conventions - **Parameter Names:** All parameters for events, errors, and functions must be preceded with an underscore (`_`). - Example: @@ -26,7 +52,7 @@ This style guide documents the coding conventions required for all Compose code. ``` ## 3. Internal Functions -- **Facets:** Internal function names in facets should be prefixed with `internal` if they otherwise have the same name as an external function in the same facet. Usually should be few or no internal functions in facets; repeat code if it improves readability. +- **Facets:** Internal function names in facets should be prefixed with `internal` if they otherwise have the same name as an external function in the same facet. Usually, there should be few or no internal functions in facets; repeat code if it improves readability. - **Libraries:** All functions in libraries use the `internal` visibility specifier. ## 4. Value Resetting From 5b5db3e634c2a806637f36b35d8b424bbbcf3574 Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 11:52:41 -0400 Subject: [PATCH 10/29] improved STYLE.md --- STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STYLE.md b/STYLE.md index a3079bd2..326579e0 100644 --- a/STYLE.md +++ b/STYLE.md @@ -4,7 +4,7 @@ This style guide documents the coding conventions required for all Compose code. ## 1. No Imports in Facets -Facets in Compose are self-contained, stand-alone smart contracts. Keep the code in the facet and make it as readable as possible, +Facets in Compose are self-contained, stand-alone smart contracts. Keep the code in the facet and make it as readable as possible. Importing other files into Compose facets is not allowed. - Example: From d4addff4d3c632178167c1de35022cdf65f95d19 Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 11:53:29 -0400 Subject: [PATCH 11/29] improved STYLE.md --- STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STYLE.md b/STYLE.md index 326579e0..60b09a12 100644 --- a/STYLE.md +++ b/STYLE.md @@ -16,7 +16,7 @@ Importing other files into Compose facets is not allowed. ## 1. Facets Are Read From The Top Down Put your code in the facet in a way that it can be read from the top of the file and down to the bottom of the file, without having to jump to any other place in the file. -This means that anything must be defined first before it used in a facet. This makes it easier to read a facet because the reader doesn't have to jump around the file to see what things are. In addition, it makes the code base consistent in how it is written and read. +This means that everything must be defined first before it used in a facet. This makes it easier to read a facet because the reader doesn't have to jump around the file to see what things are. In addition, it makes the code base consistent in how it is written and read. ## 2. No Inheritance From a2b895f60c6eccb1ab439a9b7788176ad52d2fa9 Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 11:54:28 -0400 Subject: [PATCH 12/29] improved STYLE.md --- STYLE.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/STYLE.md b/STYLE.md index 60b09a12..1a0840e0 100644 --- a/STYLE.md +++ b/STYLE.md @@ -28,7 +28,7 @@ Facets may not inherit other contracts or interfaces. contract ERC721Facet is IERCFacet { ``` -## 1. Naming Conventions +## 3. Naming Conventions - **Parameter Names:** All parameters for events, errors, and functions must be preceded with an underscore (`_`). - Example: ```solidity @@ -39,7 +39,7 @@ Facets may not inherit other contracts or interfaces. - **Camel Case:** Use camelCase for variable, function, contract, and library names, except for standard uppercase abbreviations (e.g., ERC). - Example: `totalSupply`, `LibERC20`, `ERC721Facet` -## 2. Control Structures +## 4. Control Structures - **Brackets Required:** One-line `if` statements without code block brackets `{}` are not allowed. Always use a newline and brackets. - Example: ```solidity @@ -51,26 +51,26 @@ Facets may not inherit other contracts or interfaces. } ``` -## 3. Internal Functions +## 5. Internal Functions - **Facets:** Internal function names in facets should be prefixed with `internal` if they otherwise have the same name as an external function in the same facet. Usually, there should be few or no internal functions in facets; repeat code if it improves readability. - **Libraries:** All functions in libraries use the `internal` visibility specifier. -## 4. Value Resetting +## 6. Value Resetting - Use `delete` to set a value to zero. - Example: ```solidity delete balances[_owner]; ``` -## 5. Formatting +## 7. Formatting - Format code using the default settings of `forge fmt`. Run `forge fmt` before submitting code. -## 6. References and Examples +## 8. References and Examples - For more examples, see: - [`src/token/ERC721/ERC721/ERC721Facet.sol`](src/token/ERC721/ERC721/ERC721Facet.sol) - [`src/token/ERC721/ERC721/LibERC721.sol`](src/token/ERC721/ERC721/LibERC721.sol) -## 7. Additional Rules +## 9. Additional Rules - More rules may be derived from the above example files. When in doubt, follow the patterns established in those files. --- From 16168cee2c981f3ee09a2daa5a6ff68d69f6b648 Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 11:55:17 -0400 Subject: [PATCH 13/29] improved STYLE.md --- STYLE.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/STYLE.md b/STYLE.md index 1a0840e0..77c377e4 100644 --- a/STYLE.md +++ b/STYLE.md @@ -13,12 +13,12 @@ Importing other files into Compose facets is not allowed. import {LibOwner} from "../../../src/access/Owner/LibOwner.sol"; ``` -## 1. Facets Are Read From The Top Down +## 2. Facets Are Read From The Top Down Put your code in the facet in a way that it can be read from the top of the file and down to the bottom of the file, without having to jump to any other place in the file. This means that everything must be defined first before it used in a facet. This makes it easier to read a facet because the reader doesn't have to jump around the file to see what things are. In addition, it makes the code base consistent in how it is written and read. -## 2. No Inheritance +## 3. No Inheritance Facets may not inherit other contracts or interfaces. @@ -28,7 +28,7 @@ Facets may not inherit other contracts or interfaces. contract ERC721Facet is IERCFacet { ``` -## 3. Naming Conventions +## 4. Naming Conventions - **Parameter Names:** All parameters for events, errors, and functions must be preceded with an underscore (`_`). - Example: ```solidity @@ -39,7 +39,7 @@ Facets may not inherit other contracts or interfaces. - **Camel Case:** Use camelCase for variable, function, contract, and library names, except for standard uppercase abbreviations (e.g., ERC). - Example: `totalSupply`, `LibERC20`, `ERC721Facet` -## 4. Control Structures +## 5. Control Structures - **Brackets Required:** One-line `if` statements without code block brackets `{}` are not allowed. Always use a newline and brackets. - Example: ```solidity @@ -51,26 +51,26 @@ Facets may not inherit other contracts or interfaces. } ``` -## 5. Internal Functions +## 6. Internal Functions - **Facets:** Internal function names in facets should be prefixed with `internal` if they otherwise have the same name as an external function in the same facet. Usually, there should be few or no internal functions in facets; repeat code if it improves readability. - **Libraries:** All functions in libraries use the `internal` visibility specifier. -## 6. Value Resetting +## 7. Value Resetting - Use `delete` to set a value to zero. - Example: ```solidity delete balances[_owner]; ``` -## 7. Formatting +## 8. Formatting - Format code using the default settings of `forge fmt`. Run `forge fmt` before submitting code. -## 8. References and Examples +## 9. References and Examples - For more examples, see: - [`src/token/ERC721/ERC721/ERC721Facet.sol`](src/token/ERC721/ERC721/ERC721Facet.sol) - [`src/token/ERC721/ERC721/LibERC721.sol`](src/token/ERC721/ERC721/LibERC721.sol) -## 9. Additional Rules +## 10. Additional Rules - More rules may be derived from the above example files. When in doubt, follow the patterns established in those files. --- From d26996dafba3e69502c6b245156163f728c2249c Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 11:56:13 -0400 Subject: [PATCH 14/29] improved STYLE.md --- STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STYLE.md b/STYLE.md index 77c377e4..ab9ed13b 100644 --- a/STYLE.md +++ b/STYLE.md @@ -6,7 +6,7 @@ This style guide documents the coding conventions required for all Compose code. ## 1. No Imports in Facets Facets in Compose are self-contained, stand-alone smart contracts. Keep the code in the facet and make it as readable as possible. -Importing other files into Compose facets is not allowed. +Importing other files into Compose facets **is not allowed**. - Example: ```solidity // This is not allowed in Compose' facets or libraries From 676ee8214e6aeb0fbc509c64f042c14670ae65ac Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 11:57:45 -0400 Subject: [PATCH 15/29] improved STYLE.md --- STYLE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/STYLE.md b/STYLE.md index ab9ed13b..4a6d2e2c 100644 --- a/STYLE.md +++ b/STYLE.md @@ -13,10 +13,10 @@ Importing other files into Compose facets **is not allowed**. import {LibOwner} from "../../../src/access/Owner/LibOwner.sol"; ``` -## 2. Facets Are Read From The Top Down -Put your code in the facet in a way that it can be read from the top of the file and down to the bottom of the file, without having to jump to any other place in the file. +## 2. Facets And Libraries Are Read From The Top Down +Put your code in a facet or library in a way that it can be read from the top of the file and down to the bottom of the file, without having to jump to any other place in the file. -This means that everything must be defined first before it used in a facet. This makes it easier to read a facet because the reader doesn't have to jump around the file to see what things are. In addition, it makes the code base consistent in how it is written and read. +This means that everything must be defined first before it used. This makes it easier to read a facet or library because the reader doesn't have to jump around the file to see what things are. In addition, it makes the code base consistent in how it is written and read. ## 3. No Inheritance From c36cd1af1a4d5f0d77363bc5ae3b3ab6559a3d8c Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 11:59:47 -0400 Subject: [PATCH 16/29] improved STYLE.md --- STYLE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/STYLE.md b/STYLE.md index 4a6d2e2c..6ac11bf6 100644 --- a/STYLE.md +++ b/STYLE.md @@ -7,7 +7,7 @@ This style guide documents the coding conventions required for all Compose code. Facets in Compose are self-contained, stand-alone smart contracts. Keep the code in the facet and make it as readable as possible. Importing other files into Compose facets **is not allowed**. -- Example: +- Example of what not to do: ```solidity // This is not allowed in Compose' facets or libraries import {LibOwner} from "../../../src/access/Owner/LibOwner.sol"; @@ -16,13 +16,13 @@ Importing other files into Compose facets **is not allowed**. ## 2. Facets And Libraries Are Read From The Top Down Put your code in a facet or library in a way that it can be read from the top of the file and down to the bottom of the file, without having to jump to any other place in the file. -This means that everything must be defined first before it used. This makes it easier to read a facet or library because the reader doesn't have to jump around the file to see what things are. In addition, it makes the code base consistent in how it is written and read. +This means that everything must be defined before it used. This makes it easier to read a facet or library because the reader doesn't have to jump around the file to see what things are. In addition, it makes the code base consistent in how it is written and read. ## 3. No Inheritance Facets may not inherit other contracts or interfaces. -- Example: +- Example of what not to do: ```solidity // This is not allowed in Compose' facets or libraries contract ERC721Facet is IERCFacet { From a04dc426f13b3835fe9c83b7fa777194541df0e8 Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 12:03:50 -0400 Subject: [PATCH 17/29] Removed unused import statement from ERC721Enumerable --- src/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol b/src/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol index 932abe6c..1418b3d6 100644 --- a/src/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol +++ b/src/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.30; -import {LibUtils} from "../../../libraries/LibUtils.sol"; - /// @title ERC721 Receiver Interface /// @notice Interface for contracts that want to support safe ERC721 token transfers. /// @dev Implementers must return the function selector to confirm token receipt. From d61e5b0a7595c07a894a00863324efbfda9af6b9 Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 12:10:22 -0400 Subject: [PATCH 18/29] improved STYLE.md --- STYLE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/STYLE.md b/STYLE.md index 6ac11bf6..21bb7e5a 100644 --- a/STYLE.md +++ b/STYLE.md @@ -75,4 +75,6 @@ Facets may not inherit other contracts or interfaces. --- +Note: All these rules do not apply to *tests*. + **All contributors must follow this style guide.** From ee146e96fbe5ffa0d2215730bad0334d7adeea88 Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 18:45:41 -0400 Subject: [PATCH 19/29] Improved STYLE.md --- STYLE.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/STYLE.md b/STYLE.md index 21bb7e5a..30067c99 100644 --- a/STYLE.md +++ b/STYLE.md @@ -62,15 +62,25 @@ Facets may not inherit other contracts or interfaces. delete balances[_owner]; ``` -## 8. Formatting +## 8. Avoid Assembly + - Avoid using assembly if you can. If you can't and you access memory, do it safely, and use `assembly ("memory-safe")`. + `"memory safe"` tells the Solidity compiler not to skip optimizations because memory is being safely used. + - Example: + ```solidity + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + ``` + +## 9. Formatting - Format code using the default settings of `forge fmt`. Run `forge fmt` before submitting code. -## 9. References and Examples +## 10. References and Examples - For more examples, see: - [`src/token/ERC721/ERC721/ERC721Facet.sol`](src/token/ERC721/ERC721/ERC721Facet.sol) - [`src/token/ERC721/ERC721/LibERC721.sol`](src/token/ERC721/ERC721/LibERC721.sol) -## 10. Additional Rules +## 11. Additional Rules - More rules may be derived from the above example files. When in doubt, follow the patterns established in those files. --- From c94836aa987fa2e9ef5ce47d891c52c0b06bf98d Mon Sep 17 00:00:00 2001 From: Nick Mudge Date: Tue, 28 Oct 2025 18:52:59 -0400 Subject: [PATCH 20/29] Improved STYLE.md --- STYLE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/STYLE.md b/STYLE.md index 30067c99..dd8b895c 100644 --- a/STYLE.md +++ b/STYLE.md @@ -63,8 +63,8 @@ Facets may not inherit other contracts or interfaces. ``` ## 8. Avoid Assembly - - Avoid using assembly if you can. If you can't and you access memory, do it safely, and use `assembly ("memory-safe")`. - `"memory safe"` tells the Solidity compiler not to skip optimizations because memory is being safely used. + - Avoid using assembly if you can. If you can't and you access memory, do it [safely](https://docs.soliditylang.org/en/latest/assembly.html#memory-safety), and use `assembly ("memory-safe")`. + `"memory safe"` tells the Solidity compiler that memory is being used safely so it should not disable optimizations. - Example: ```solidity assembly ("memory-safe") { From 23940ea6085e80da59119de02b19aabb42dc2c0b Mon Sep 17 00:00:00 2001 From: aapsi Date: Thu, 30 Oct 2025 15:45:51 +0530 Subject: [PATCH 21/29] Refactored IERC1155 and IERC1155Receiver interfaces to clarify parameter descriptions and added IERC1155Receiver interface implementation in a new file. --- src/interfaces/IERC1155.sol | 39 +++-------------------- src/interfaces/IERC1155Receiver.sol | 48 +++++++++++++++++++++++++++++ src/token/ERC1155/ERC1155Facet.sol | 4 +-- 3 files changed, 54 insertions(+), 37 deletions(-) create mode 100644 src/interfaces/IERC1155Receiver.sol diff --git a/src/interfaces/IERC1155.sol b/src/interfaces/IERC1155.sol index 4a6cc456..465717c2 100644 --- a/src/interfaces/IERC1155.sol +++ b/src/interfaces/IERC1155.sol @@ -76,8 +76,8 @@ interface IERC1155 { function balanceOf(address _account, uint256 _id) external view returns (uint256); /// @notice Batched version of {balanceOf}. - /// @param _accounts The addresses to query the balances of. - /// @param _ids The token types to query. + /// @param _accounts The addresses to query the balances of (order and length must match _ids array). + /// @param _ids The token types to query (order and length must match _accounts array). /// @return The balances of the token types. function balanceOfBatch(address[] calldata _accounts, uint256[] calldata _ids) external @@ -106,8 +106,8 @@ interface IERC1155 { /// @notice Batched version of {safeTransferFrom}. /// @param _from The address to transfer from. /// @param _to The address to transfer to. - /// @param _ids The token types to transfer. - /// @param _values The amounts to transfer. + /// @param _ids The token types to transfer (order and length must match _values array). + /// @param _values The amounts to transfer (order and length must match _ids array). /// @param _data Additional data with no specified format. function safeBatchTransferFrom( address _from, @@ -122,34 +122,3 @@ interface IERC1155 { /// @return The URI for the token type. function uri(uint256 _id) external view returns (string memory); } - -/// @title ERC-1155 Token Receiver Interface -/// @notice Interface for contracts that want to support safe transfers from ERC-1155 token contracts. -/// @dev Contracts implementing this must return the correct selector to confirm token receipt. -interface IERC1155Receiver { - /// @notice Handles the receipt of a single ERC-1155 token type. - /// @param _operator The address which initiated the transfer. - /// @param _from The address which previously owned the token. - /// @param _id The token type being transferred. - /// @param _value The amount of tokens being transferred. - /// @param _data Additional data with no specified format. - /// @return The selector to confirm the token transfer. - 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. - /// @param _operator The address which initiated the batch transfer. - /// @param _from The address which previously owned the tokens. - /// @param _ids The token types being transferred. - /// @param _values The amounts of tokens being transferred. - /// @param _data Additional data with no specified format. - /// @return The selector to confirm the batch token transfer. - function onERC1155BatchReceived( - address _operator, - address _from, - uint256[] calldata _ids, - uint256[] calldata _values, - bytes calldata _data - ) external returns (bytes4); -} diff --git a/src/interfaces/IERC1155Receiver.sol b/src/interfaces/IERC1155Receiver.sol new file mode 100644 index 00000000..9413b95c --- /dev/null +++ b/src/interfaces/IERC1155Receiver.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/// @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); +} diff --git a/src/token/ERC1155/ERC1155Facet.sol b/src/token/ERC1155/ERC1155Facet.sol index 9ad8b506..5bd0d543 100644 --- a/src/token/ERC1155/ERC1155Facet.sol +++ b/src/token/ERC1155/ERC1155Facet.sol @@ -33,8 +33,8 @@ interface IERC1155Receiver { * * @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 _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. */ From c1d4639de962443e464ca6f8004632fa12ec52ca Mon Sep 17 00:00:00 2001 From: SuperFranky Date: Thu, 30 Oct 2025 13:21:03 +0100 Subject: [PATCH 22/29] added the transfer functions and erc1155 receiver validation for minting --- src/token/ERC1155/LibERC1155.sol | 154 ++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 2 deletions(-) diff --git a/src/token/ERC1155/LibERC1155.sol b/src/token/ERC1155/LibERC1155.sol index b09072e4..3f43105a 100644 --- a/src/token/ERC1155/LibERC1155.sol +++ b/src/token/ERC1155/LibERC1155.sol @@ -1,6 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.30; +/// @title ERC-1155 Token Receiver Interface +/// @notice Interface for contracts that want to handle safe transfers of ERC-1155 tokens. +interface IERC1155Receiver { + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) + external + returns (bytes4); + + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external returns (bytes4); +} + /// @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. @@ -26,6 +42,11 @@ library LibERC1155 { /// @param _valuesLength Length of the values array. error ERC1155InvalidArrayLength(uint256 _idsLength, uint256 _valuesLength); + /// @notice Thrown when 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. /// @param _operator The address which initiated the transfer. /// @param _from The address which previously owned the token. @@ -74,7 +95,7 @@ library LibERC1155 { /** * @notice Mints a single token type to an address. * @dev Increases the balance and emits a TransferSingle event. - * Does NOT perform receiver validation - use with caution. + * 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. @@ -88,12 +109,30 @@ library LibERC1155 { 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, "") 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. - * Does NOT perform receiver validation - use with caution. + * 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. @@ -113,6 +152,24 @@ library LibERC1155 { } emit TransferBatch(msg.sender, address(0), _to, _ids, _values); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155BatchReceived(msg.sender, address(0), _ids, _values, "") 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)) + } + } + } + } } /** @@ -176,4 +233,97 @@ library LibERC1155 { emit TransferBatch(msg.sender, _from, address(0), _ids, _values); } + + /** + * @notice Transfers a single token type from one address to another. + * @dev Validates ownership, approval, and receiver address before updating balances. + * Does NOT perform ERC1155Receiver validation - use with caution. + * Intended for custom facets that need transfer logic with authorization. + * @param _from The address to transfer from. + * @param _to The address to transfer to. + * @param _id The token type to transfer. + * @param _value The amount of tokens to transfer. + * @param _operator The address initiating the transfer (may be owner or approved operator). + */ + function transferFrom(address _from, address _to, uint256 _id, uint256 _value, address _operator) internal { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + + ERC1155Storage storage s = getStorage(); + + // Check authorization + if (_from != _operator && !s.isApprovedForAll[_from][_operator]) { + revert ERC1155MissingApprovalForAll(_operator, _from); + } + + uint256 fromBalance = s.balanceOf[_id][_from]; + + if (fromBalance < _value) { + revert ERC1155InsufficientBalance(_from, fromBalance, _value, _id); + } + + unchecked { + s.balanceOf[_id][_from] = fromBalance - _value; + } + s.balanceOf[_id][_to] += _value; + + emit TransferSingle(_operator, _from, _to, _id, _value); + } + + /** + * @notice Transfers multiple token types from one address to another in a single transaction. + * @dev Validates ownership, approval, and receiver address before updating balances for each token type. + * Does NOT perform ERC1155Receiver validation - use with caution. + * Intended for custom facets that need batch transfer logic with authorization. + * @param _from The address to transfer from. + * @param _to The address to transfer to. + * @param _ids The token types to transfer. + * @param _values The amounts of tokens to transfer for each type. + * @param _operator The address initiating the transfer (may be owner or approved operator). + */ + function transferBatchFrom( + address _from, + address _to, + uint256[] memory _ids, + uint256[] memory _values, + address _operator + ) internal { + if (_from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + if (_to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (_ids.length != _values.length) { + revert ERC1155InvalidArrayLength(_ids.length, _values.length); + } + + ERC1155Storage storage s = getStorage(); + + // Check authorization + if (_from != _operator && !s.isApprovedForAll[_from][_operator]) { + revert ERC1155MissingApprovalForAll(_operator, _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; + } + s.balanceOf[id][_to] += value; + } + + emit TransferBatch(_operator, _from, _to, _ids, _values); + } } \ No newline at end of file From 35a758b0d730b8f4cd09ec58eeb9155ff54ed100 Mon Sep 17 00:00:00 2001 From: SuperFranky Date: Thu, 30 Oct 2025 13:21:34 +0100 Subject: [PATCH 23/29] ran forge fmt --- src/token/ERC1155/LibERC1155.sol | 2 +- test/token/Royalty/LibRoyalty.t.sol | 1 - test/token/Royalty/RoyaltyFacet.t.sol | 1 - test/token/Royalty/harnesses/LibRoyaltyHarness.sol | 1 - test/token/Royalty/harnesses/RoyaltyFacetHarness.sol | 1 - 5 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/token/ERC1155/LibERC1155.sol b/src/token/ERC1155/LibERC1155.sol index 3f43105a..9c73229e 100644 --- a/src/token/ERC1155/LibERC1155.sol +++ b/src/token/ERC1155/LibERC1155.sol @@ -326,4 +326,4 @@ library LibERC1155 { emit TransferBatch(_operator, _from, _to, _ids, _values); } -} \ No newline at end of file +} diff --git a/test/token/Royalty/LibRoyalty.t.sol b/test/token/Royalty/LibRoyalty.t.sol index 31a3b5a0..44939750 100644 --- a/test/token/Royalty/LibRoyalty.t.sol +++ b/test/token/Royalty/LibRoyalty.t.sol @@ -525,4 +525,3 @@ contract LibRoyaltyTest is Test { assertEq(royalty3, 0); } } - diff --git a/test/token/Royalty/RoyaltyFacet.t.sol b/test/token/Royalty/RoyaltyFacet.t.sol index e6e9fef4..3cf20689 100644 --- a/test/token/Royalty/RoyaltyFacet.t.sol +++ b/test/token/Royalty/RoyaltyFacet.t.sol @@ -434,4 +434,3 @@ contract RoyaltyFacetTest is Test { assertEq(royalty3, 5 ether); } } - diff --git a/test/token/Royalty/harnesses/LibRoyaltyHarness.sol b/test/token/Royalty/harnesses/LibRoyaltyHarness.sol index 60a72825..2f1d8248 100644 --- a/test/token/Royalty/harnesses/LibRoyaltyHarness.sol +++ b/test/token/Royalty/harnesses/LibRoyaltyHarness.sol @@ -56,4 +56,3 @@ contract LibRoyaltyHarness { return LibRoyalty.getStorage().tokenRoyaltyInfo[_tokenId].royaltyFraction; } } - diff --git a/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol b/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol index a2e3dfbd..60f99b8b 100644 --- a/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol +++ b/test/token/Royalty/harnesses/RoyaltyFacetHarness.sol @@ -21,4 +21,3 @@ contract RoyaltyFacetHarness is RoyaltyFacet { s.tokenRoyaltyInfo[_tokenId] = RoyaltyInfo(_receiver, _feeNumerator); } } - From 4260d6a70f7067be51447813ac436de532dc8e38 Mon Sep 17 00:00:00 2001 From: aapsi Date: Thu, 30 Oct 2025 18:13:41 +0530 Subject: [PATCH 24/29] Fix forge fmt formatting for ERC1155Facet --- src/token/ERC1155/ERC1155Facet.sol | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/token/ERC1155/ERC1155Facet.sol b/src/token/ERC1155/ERC1155Facet.sol index 5bd0d543..a82ca706 100644 --- a/src/token/ERC1155/ERC1155Facet.sol +++ b/src/token/ERC1155/ERC1155Facet.sol @@ -152,7 +152,13 @@ contract ERC1155Facet { * actual token type ID in hexadecimal form. * @return The URI for the token type. */ - function uri(uint256 /* _id */ ) external view returns (string memory) { + function uri( + uint256 /* _id */ + ) + external + view + returns (string memory) + { return getStorage().uri; } @@ -251,8 +257,9 @@ contract ERC1155Facet { emit TransferSingle(msg.sender, _from, _to, _id, _value); if (_to.code.length > 0) { - try IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _id, _value, _data) returns (bytes4 response) - { + try IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _id, _value, _data) returns ( + bytes4 response + ) { if (response != IERC1155Receiver.onERC1155Received.selector) { revert ERC1155InvalidReceiver(_to); } From 08da6c9aff7cd687310d2b92efb8653b902b2d85 Mon Sep 17 00:00:00 2001 From: aapsi Date: Fri, 31 Oct 2025 02:19:57 +0530 Subject: [PATCH 25/29] feat(ERC1155): enhance URI handling with baseURI and token-specific URIs --- src/token/ERC1155/ERC1155Facet.sol | 26 +++++++++++------------ src/token/ERC1155/LibERC1155.sol | 33 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/token/ERC1155/ERC1155Facet.sol b/src/token/ERC1155/ERC1155Facet.sol index a82ca706..91c50523 100644 --- a/src/token/ERC1155/ERC1155Facet.sol +++ b/src/token/ERC1155/ERC1155Facet.sol @@ -128,8 +128,10 @@ contract ERC1155Facet { */ struct ERC1155Storage { string uri; + string baseURI; mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; mapping(address account => mapping(address operator => bool)) isApprovedForAll; + mapping(uint256 tokenId => string) tokenURIs; } /** @@ -145,21 +147,19 @@ contract ERC1155Facet { } /** - * @notice Returns the URI for token type `id`. - * @dev This implementation returns the same URI for all token types. It relies - * on the token type ID substitution mechanism defined in the ERC-1155 standard. - * Clients calling this function must replace the `{id}` substring with the - * actual token type ID in hexadecimal form. + * @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) - { - return getStorage().uri; + 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; } /** diff --git a/src/token/ERC1155/LibERC1155.sol b/src/token/ERC1155/LibERC1155.sol index 9c73229e..399cfcdb 100644 --- a/src/token/ERC1155/LibERC1155.sol +++ b/src/token/ERC1155/LibERC1155.sol @@ -67,6 +67,11 @@ library LibERC1155 { 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. bytes32 constant STORAGE_POSITION = keccak256("compose.erc1155"); @@ -78,6 +83,8 @@ library LibERC1155 { string uri; mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; mapping(address account => mapping(address operator => bool)) isApprovedForAll; + string baseURI; + mapping(uint256 tokenId => string) tokenURIs; } /** @@ -326,4 +333,30 @@ library LibERC1155 { emit TransferBatch(_operator, _from, _to, _ids, _values); } + + /** + * @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) internal { + 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) internal { + ERC1155Storage storage s = getStorage(); + s.baseURI = _baseURI; + } } From d015b58db29ebd19b0f85b355fc10a7d2a86ff02 Mon Sep 17 00:00:00 2001 From: SuperFranky Date: Thu, 30 Oct 2025 22:02:25 +0100 Subject: [PATCH 26/29] implemented safe transfer requirements --- src/token/ERC1155/LibERC1155.sol | 46 +++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/token/ERC1155/LibERC1155.sol b/src/token/ERC1155/LibERC1155.sol index 399cfcdb..e153c6df 100644 --- a/src/token/ERC1155/LibERC1155.sol +++ b/src/token/ERC1155/LibERC1155.sol @@ -242,10 +242,10 @@ library LibERC1155 { } /** - * @notice Transfers a single token type from one address to another. + * @notice Safely transfers a single token type from one address to another. * @dev Validates ownership, approval, and receiver address before updating balances. - * Does NOT perform ERC1155Receiver validation - use with caution. - * Intended for custom facets that need transfer logic with authorization. + * Performs ERC1155Receiver validation if recipient is a contract (safe transfer). + * Complies with EIP-1155 safe transfer requirements. * @param _from The address to transfer from. * @param _to The address to transfer to. * @param _id The token type to transfer. @@ -279,13 +279,29 @@ library LibERC1155 { s.balanceOf[_id][_to] += _value; emit TransferSingle(_operator, _from, _to, _id, _value); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155Received(_operator, _from, _id, _value, "") 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 Transfers multiple token types from one address to another in a single transaction. + * @notice Safely transfers multiple token types from one address to another in a single transaction. * @dev Validates ownership, approval, and receiver address before updating balances for each token type. - * Does NOT perform ERC1155Receiver validation - use with caution. - * Intended for custom facets that need batch transfer logic with authorization. + * Performs ERC1155Receiver validation if recipient is a contract (safe transfer). + * Complies with EIP-1155 safe transfer requirements. * @param _from The address to transfer from. * @param _to The address to transfer to. * @param _ids The token types to transfer. @@ -332,6 +348,24 @@ library LibERC1155 { } emit TransferBatch(_operator, _from, _to, _ids, _values); + + if (_to.code.length > 0) { + try IERC1155Receiver(_to).onERC1155BatchReceived(_operator, _from, _ids, _values, "") 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)) + } + } + } + } } /** From d779165ababefeb402b7ec026a915a9623360876 Mon Sep 17 00:00:00 2001 From: aapsi Date: Fri, 31 Oct 2025 09:33:50 +0530 Subject: [PATCH 27/29] refactor(ERC1155): rename transfer functions to safeTransferFrom and safeBatchTransferFrom, and adjust baseURI handling in ERC1155Storage --- src/token/ERC1155/LibERC1155.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/token/ERC1155/LibERC1155.sol b/src/token/ERC1155/LibERC1155.sol index e153c6df..f4b3c626 100644 --- a/src/token/ERC1155/LibERC1155.sol +++ b/src/token/ERC1155/LibERC1155.sol @@ -81,9 +81,9 @@ library LibERC1155 { */ struct ERC1155Storage { string uri; + string baseURI; mapping(uint256 id => mapping(address account => uint256 balance)) balanceOf; mapping(address account => mapping(address operator => bool)) isApprovedForAll; - string baseURI; mapping(uint256 tokenId => string) tokenURIs; } @@ -252,7 +252,7 @@ library LibERC1155 { * @param _value The amount of tokens to transfer. * @param _operator The address initiating the transfer (may be owner or approved operator). */ - function transferFrom(address _from, address _to, uint256 _id, uint256 _value, address _operator) internal { + function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, address _operator) internal { if (_from == address(0)) { revert ERC1155InvalidSender(address(0)); } @@ -308,7 +308,7 @@ library LibERC1155 { * @param _values The amounts of tokens to transfer for each type. * @param _operator The address initiating the transfer (may be owner or approved operator). */ - function transferBatchFrom( + function safeBatchTransferFrom( address _from, address _to, uint256[] memory _ids, From 2ee42777a00a6e1346aebd1f1944197fac2f6a4a Mon Sep 17 00:00:00 2001 From: SuperFranky Date: Fri, 31 Oct 2025 14:36:07 +0100 Subject: [PATCH 28/29] added natspec and ran forge fmt --- src/token/ERC1155/ERC1155Facet.sol | 5 ++--- src/token/ERC1155/LibERC1155.sol | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/token/ERC1155/ERC1155Facet.sol b/src/token/ERC1155/ERC1155Facet.sol index 91c50523..71591955 100644 --- a/src/token/ERC1155/ERC1155Facet.sol +++ b/src/token/ERC1155/ERC1155Facet.sol @@ -257,9 +257,8 @@ contract ERC1155Facet { emit TransferSingle(msg.sender, _from, _to, _id, _value); if (_to.code.length > 0) { - try IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _id, _value, _data) returns ( - bytes4 response - ) { + try IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _id, _value, _data) returns (bytes4 response) + { if (response != IERC1155Receiver.onERC1155Received.selector) { revert ERC1155InvalidReceiver(_to); } diff --git a/src/token/ERC1155/LibERC1155.sol b/src/token/ERC1155/LibERC1155.sol index f4b3c626..f1a7041a 100644 --- a/src/token/ERC1155/LibERC1155.sol +++ b/src/token/ERC1155/LibERC1155.sol @@ -4,10 +4,40 @@ pragma solidity >=0.8.30; /// @title ERC-1155 Token Receiver Interface /// @notice Interface for contracts that want to handle safe transfers of ERC-1155 tokens. 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, From a885f2a7657637c876ad0e1afa1f9c1e7a4504e7 Mon Sep 17 00:00:00 2001 From: aapsi Date: Fri, 31 Oct 2025 19:14:27 +0530 Subject: [PATCH 29/29] forge fmt fix --- src/token/ERC1155/ERC1155Facet.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/token/ERC1155/ERC1155Facet.sol b/src/token/ERC1155/ERC1155Facet.sol index 71591955..91c50523 100644 --- a/src/token/ERC1155/ERC1155Facet.sol +++ b/src/token/ERC1155/ERC1155Facet.sol @@ -257,8 +257,9 @@ contract ERC1155Facet { emit TransferSingle(msg.sender, _from, _to, _id, _value); if (_to.code.length > 0) { - try IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _id, _value, _data) returns (bytes4 response) - { + try IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _id, _value, _data) returns ( + bytes4 response + ) { if (response != IERC1155Receiver.onERC1155Received.selector) { revert ERC1155InvalidReceiver(_to); }