From 52c9f4ab0b7f6baf2b4525a79eea69fae97a9d08 Mon Sep 17 00:00:00 2001 From: aapsi Date: Mon, 27 Oct 2025 01:53:09 +0530 Subject: [PATCH 1/2] Add ERC-2981 NFT Royalty Standard implementation Implements the ERC-2981 standard for NFT royalty information using Compose's diamond storage pattern. This implementation provides: - ERC2981Facet with royaltyInfo() query function - LibERC2981 library with internal setter functions for royalty configuration - Support for both default and per-token royalty settings - IERC2981 interface with custom errors Note: External setter functions are intentionally omitted from the facet to allow maintainers to decide on the appropriate access control pattern (e.g., owner-only, role-based, or public with token ownership checks). Tests will be added once the final contract design and access control approach are confirmed. --- src/ERC2981/ERC2981/ERC2981Facet.sol | 84 ++++++++++++ src/ERC2981/ERC2981/libraries/LibERC2981.sol | 131 +++++++++++++++++++ src/interfaces/IERC2981.sol | 31 +++++ 3 files changed, 246 insertions(+) create mode 100644 src/ERC2981/ERC2981/ERC2981Facet.sol create mode 100644 src/ERC2981/ERC2981/libraries/LibERC2981.sol create mode 100644 src/interfaces/IERC2981.sol diff --git a/src/ERC2981/ERC2981/ERC2981Facet.sol b/src/ERC2981/ERC2981/ERC2981Facet.sol new file mode 100644 index 00000000..ed5f9bfe --- /dev/null +++ b/src/ERC2981/ERC2981/ERC2981Facet.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/// @title ERC-2981 NFT Royalty Standard +/// @notice Implements royalty queries for NFT secondary sales. +/// @dev Provides standardized royalty information to NFT marketplaces and platforms. +/// Supports both default and per-token royalty configurations using diamond storage. +contract ERC2981Facet { + /// @notice Thrown when default royalty fee exceeds 100% (10000 basis points). + /// @param _numerator The fee numerator that exceeds the denominator. + /// @param _denominator The fee denominator (10000 basis points). + error ERC2981InvalidDefaultRoyalty(uint256 _numerator, uint256 _denominator); + + /// @notice Thrown when default royalty receiver is the zero address. + /// @param _receiver The invalid receiver address. + error ERC2981InvalidDefaultRoyaltyReceiver(address _receiver); + + /// @notice Thrown when token-specific royalty fee exceeds 100% (10000 basis points). + /// @param _tokenId The token ID with invalid royalty configuration. + /// @param _numerator The fee numerator that exceeds the denominator. + /// @param _denominator The fee denominator (10000 basis points). + error ERC2981InvalidTokenRoyalty(uint256 _tokenId, uint256 _numerator, uint256 _denominator); + + /// @notice Thrown when token-specific royalty receiver is the zero address. + /// @param _tokenId The token ID with invalid royalty configuration. + /// @param _receiver The invalid receiver address. + error ERC2981InvalidTokenRoyaltyReceiver(uint256 _tokenId, address _receiver); + + bytes32 constant STORAGE_POSITION = keccak256("compose.erc2981"); + + /// @dev The denominator with which to interpret royalty fees as a percentage of sale price. + /// Expressed in basis points where 10000 = 100%. This value aligns with the ERC-2981 + /// specification and marketplace expectations. Implemented as a constant for gas efficiency + /// rather than the virtual function pattern, as Compose does not support inheritance-based + /// customization. To modify this value, deploy a custom facet implementation. + uint96 constant FEE_DENOMINATOR = 10000; + + /// @notice Structure containing royalty information. + /// @param receiver The address that will receive royalty payments. + /// @param royaltyFraction The royalty fee expressed in basis points. + struct RoyaltyInfo { + address receiver; + uint96 royaltyFraction; + } + + /// @custom:storage-location erc8042:compose.erc2981 + struct ERC2981Storage { + RoyaltyInfo defaultRoyaltyInfo; + mapping(uint256 tokenId => RoyaltyInfo) tokenRoyaltyInfo; + } + + /// @notice Returns a pointer to the ERC-2981 storage struct. + /// @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + /// @return s The ERC2981Storage struct in storage. + function getStorage() internal pure returns (ERC2981Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /// @notice Returns royalty information for a given token and sale price. + /// @dev Returns token-specific royalty if set, otherwise falls back to default royalty. + /// Royalty amount is calculated as a percentage of the sale price using basis points. + /// @param _tokenId The NFT asset queried for royalty information. + /// @param _salePrice The sale price of the NFT asset specified by _tokenId. + /// @return receiver The address designated to receive the royalty payment. + /// @return royaltyAmount The royalty payment amount for _salePrice. + function royaltyInfo(uint256 _tokenId, uint256 _salePrice) + external + view + returns (address receiver, uint256 royaltyAmount) + { + ERC2981Storage storage s = getStorage(); + RoyaltyInfo memory royalty = s.tokenRoyaltyInfo[_tokenId]; + + if (royalty.receiver == address(0)) { + royalty = s.defaultRoyaltyInfo; + } + + receiver = royalty.receiver; + royaltyAmount = (_salePrice * royalty.royaltyFraction) / FEE_DENOMINATOR; + } +} diff --git a/src/ERC2981/ERC2981/libraries/LibERC2981.sol b/src/ERC2981/ERC2981/libraries/LibERC2981.sol new file mode 100644 index 00000000..6c40a790 --- /dev/null +++ b/src/ERC2981/ERC2981/libraries/LibERC2981.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/// @title LibERC2981 — ERC-2981 Royalty Standard Library +/// @notice Provides internal functions and storage layout for ERC-2981 royalty logic. +/// @dev Uses ERC-8042 for storage location standardization. Compatible with OpenZeppelin's ERC2981 behavior. +library LibERC2981 { + /// @notice Thrown when default royalty fee exceeds 100% (10000 basis points). + /// @param _numerator The fee numerator that exceeds the denominator. + /// @param _denominator The fee denominator (10000 basis points). + error ERC2981InvalidDefaultRoyalty(uint256 _numerator, uint256 _denominator); + + /// @notice Thrown when default royalty receiver is the zero address. + /// @param _receiver The invalid receiver address. + error ERC2981InvalidDefaultRoyaltyReceiver(address _receiver); + + /// @notice Thrown when token-specific royalty fee exceeds 100% (10000 basis points). + /// @param _tokenId The token ID with invalid royalty configuration. + /// @param _numerator The fee numerator that exceeds the denominator. + /// @param _denominator The fee denominator (10000 basis points). + error ERC2981InvalidTokenRoyalty(uint256 _tokenId, uint256 _numerator, uint256 _denominator); + + /// @notice Thrown when token-specific royalty receiver is the zero address. + /// @param _tokenId The token ID with invalid royalty configuration. + /// @param _receiver The invalid receiver address. + error ERC2981InvalidTokenRoyaltyReceiver(uint256 _tokenId, address _receiver); + + bytes32 constant STORAGE_POSITION = keccak256("compose.erc2981"); + + /// @dev The denominator with which to interpret royalty fees as a percentage of sale price. + /// Expressed in basis points where 10000 = 100%. This value aligns with the ERC-2981 + /// specification and marketplace expectations. Implemented as a constant for gas efficiency + /// rather than the virtual function pattern, as Compose does not support inheritance-based + /// customization. To modify this value, deploy a custom facet implementation. + uint96 constant FEE_DENOMINATOR = 10000; + + /// @notice Structure containing royalty information. + /// @param receiver The address that will receive royalty payments. + /// @param royaltyFraction The royalty fee expressed in basis points. + struct RoyaltyInfo { + address receiver; + uint96 royaltyFraction; + } + + /// @custom:storage-location erc8042:compose.erc2981 + struct ERC2981Storage { + RoyaltyInfo defaultRoyaltyInfo; + mapping(uint256 tokenId => RoyaltyInfo) tokenRoyaltyInfo; + } + + /// @notice Returns the ERC-2981 storage struct from its predefined slot. + /// @dev Uses inline assembly to access diamond storage location. + /// @return s The storage reference for ERC-2981 state variables. + function getStorage() internal pure returns (ERC2981Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /// @notice Queries royalty information for a given token and sale price. + /// @dev Returns token-specific royalty or falls back to default royalty. + /// Royalty amount is calculated as a percentage of the sale price using basis points. + /// @param _tokenId The NFT asset queried for royalty information. + /// @param _salePrice The sale price of the NFT asset. + /// @return receiver The address designated to receive the royalty payment. + /// @return royaltyAmount The royalty payment amount for _salePrice. + function royaltyInfo(uint256 _tokenId, uint256 _salePrice) + internal + view + returns (address receiver, uint256 royaltyAmount) + { + ERC2981Storage storage s = getStorage(); + RoyaltyInfo memory royalty = s.tokenRoyaltyInfo[_tokenId]; + + if (royalty.receiver == address(0)) { + royalty = s.defaultRoyaltyInfo; + } + + receiver = royalty.receiver; + royaltyAmount = (_salePrice * royalty.royaltyFraction) / FEE_DENOMINATOR; + } + + /// @notice Sets the default royalty information that applies to all tokens. + /// @dev Validates receiver and fee, then updates default royalty storage. + /// @param _receiver The royalty recipient address. + /// @param _feeNumerator The royalty fee in basis points. + function setDefaultRoyalty(address _receiver, uint96 _feeNumerator) internal { + if (_feeNumerator > FEE_DENOMINATOR) { + revert ERC2981InvalidDefaultRoyalty(_feeNumerator, FEE_DENOMINATOR); + } + if (_receiver == address(0)) { + revert ERC2981InvalidDefaultRoyaltyReceiver(address(0)); + } + + ERC2981Storage storage s = getStorage(); + s.defaultRoyaltyInfo = RoyaltyInfo(_receiver, _feeNumerator); + } + + /// @notice Removes default royalty information. + /// @dev After calling this function, royaltyInfo will return (address(0), 0) for tokens without specific royalty. + function deleteDefaultRoyalty() internal { + ERC2981Storage storage s = getStorage(); + delete s.defaultRoyaltyInfo; + } + + /// @notice Sets royalty information for a specific token, overriding the default. + /// @dev Validates receiver and fee, then updates token-specific royalty storage. + /// @param _tokenId The token ID to configure royalty for. + /// @param _receiver The royalty recipient address. + /// @param _feeNumerator The royalty fee in basis points. + function setTokenRoyalty(uint256 _tokenId, address _receiver, uint96 _feeNumerator) internal { + if (_feeNumerator > FEE_DENOMINATOR) { + revert ERC2981InvalidTokenRoyalty(_tokenId, _feeNumerator, FEE_DENOMINATOR); + } + if (_receiver == address(0)) { + revert ERC2981InvalidTokenRoyaltyReceiver(_tokenId, address(0)); + } + + ERC2981Storage storage s = getStorage(); + s.tokenRoyaltyInfo[_tokenId] = RoyaltyInfo(_receiver, _feeNumerator); + } + + /// @notice Resets royalty information for a specific token to use the default setting. + /// @dev Clears token-specific royalty storage, causing fallback to default royalty. + /// @param _tokenId The token ID to reset royalty configuration for. + function resetTokenRoyalty(uint256 _tokenId) internal { + ERC2981Storage storage s = getStorage(); + delete s.tokenRoyaltyInfo[_tokenId]; + } +} diff --git a/src/interfaces/IERC2981.sol b/src/interfaces/IERC2981.sol new file mode 100644 index 00000000..464241c3 --- /dev/null +++ b/src/interfaces/IERC2981.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/// @title ERC-2981 NFT Royalty Standard Interface +/// @notice Interface for ERC-2981 royalty information with custom errors +/// @dev This interface includes all custom errors used by ERC-2981 implementations +interface IERC2981 { + /// @notice Error indicating the default royalty fee exceeds 100% (10000 basis points). + error ERC2981InvalidDefaultRoyalty(uint256 _numerator, uint256 _denominator); + + /// @notice Error indicating the default royalty receiver is the zero address. + error ERC2981InvalidDefaultRoyaltyReceiver(address _receiver); + + /// @notice Error indicating a token-specific royalty fee exceeds 100% (10000 basis points). + error ERC2981InvalidTokenRoyalty(uint256 _tokenId, uint256 _numerator, uint256 _denominator); + + /// @notice Error indicating a token-specific royalty receiver is the zero address. + error ERC2981InvalidTokenRoyaltyReceiver(uint256 _tokenId, address _receiver); + + /// @notice Returns royalty information for a given token and sale price. + /// @dev Called with the sale price to determine how much royalty is owed and to whom. + /// Implementations MUST calculate royalty as a percentage of the sale price. + /// @param _tokenId The NFT asset queried for royalty information. + /// @param _salePrice The sale price of the NFT asset specified by _tokenId. + /// @return receiver The address designated to receive the royalty payment. + /// @return royaltyAmount The royalty payment amount for _salePrice. + function royaltyInfo(uint256 _tokenId, uint256 _salePrice) + external + view + returns (address receiver, uint256 royaltyAmount); +} From c95c69a35c006e1a42daf3f20b4376b3036c9986 Mon Sep 17 00:00:00 2001 From: aapsi Date: Mon, 27 Oct 2025 11:23:10 +0530 Subject: [PATCH 2/2] Add ERC-2981 Royalty Standard library and facet implementation. Introduce LibRoyalty library and RoyaltyFacet contract to implement the ERC-2981 NFT royalty standard. --- .../Royalty/LibRoyalty.sol} | 24 ++++++++++--------- .../Royalty/RoyaltyFacet.sol} | 18 +++++++------- 2 files changed, 23 insertions(+), 19 deletions(-) rename src/{ERC2981/ERC2981/libraries/LibERC2981.sol => token/Royalty/LibRoyalty.sol} (89%) rename src/{ERC2981/ERC2981/ERC2981Facet.sol => token/Royalty/RoyaltyFacet.sol} (86%) diff --git a/src/ERC2981/ERC2981/libraries/LibERC2981.sol b/src/token/Royalty/LibRoyalty.sol similarity index 89% rename from src/ERC2981/ERC2981/libraries/LibERC2981.sol rename to src/token/Royalty/LibRoyalty.sol index 6c40a790..00582255 100644 --- a/src/ERC2981/ERC2981/libraries/LibERC2981.sol +++ b/src/token/Royalty/LibRoyalty.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.30; -/// @title LibERC2981 — ERC-2981 Royalty Standard Library +/// @title LibRoyalty - ERC-2981 Royalty Standard Library /// @notice Provides internal functions and storage layout for ERC-2981 royalty logic. /// @dev Uses ERC-8042 for storage location standardization. Compatible with OpenZeppelin's ERC2981 behavior. -library LibERC2981 { +/// This is an implementation of the ERC-2981 NFT Royalty Standard. +library LibRoyalty { /// @notice Thrown when default royalty fee exceeds 100% (10000 basis points). /// @param _numerator The fee numerator that exceeds the denominator. /// @param _denominator The fee denominator (10000 basis points). @@ -43,15 +44,15 @@ library LibERC2981 { } /// @custom:storage-location erc8042:compose.erc2981 - struct ERC2981Storage { + struct RoyaltyStorage { RoyaltyInfo defaultRoyaltyInfo; mapping(uint256 tokenId => RoyaltyInfo) tokenRoyaltyInfo; } - /// @notice Returns the ERC-2981 storage struct from its predefined slot. + /// @notice Returns the royalty storage struct from its predefined slot. /// @dev Uses inline assembly to access diamond storage location. - /// @return s The storage reference for ERC-2981 state variables. - function getStorage() internal pure returns (ERC2981Storage storage s) { + /// @return s The storage reference for royalty state variables. + function getStorage() internal pure returns (RoyaltyStorage storage s) { bytes32 position = STORAGE_POSITION; assembly { s.slot := position @@ -61,6 +62,7 @@ library LibERC2981 { /// @notice Queries royalty information for a given token and sale price. /// @dev Returns token-specific royalty or falls back to default royalty. /// Royalty amount is calculated as a percentage of the sale price using basis points. + /// Implements the ERC-2981 royaltyInfo function logic. /// @param _tokenId The NFT asset queried for royalty information. /// @param _salePrice The sale price of the NFT asset. /// @return receiver The address designated to receive the royalty payment. @@ -70,7 +72,7 @@ library LibERC2981 { view returns (address receiver, uint256 royaltyAmount) { - ERC2981Storage storage s = getStorage(); + RoyaltyStorage storage s = getStorage(); RoyaltyInfo memory royalty = s.tokenRoyaltyInfo[_tokenId]; if (royalty.receiver == address(0)) { @@ -93,14 +95,14 @@ library LibERC2981 { revert ERC2981InvalidDefaultRoyaltyReceiver(address(0)); } - ERC2981Storage storage s = getStorage(); + RoyaltyStorage storage s = getStorage(); s.defaultRoyaltyInfo = RoyaltyInfo(_receiver, _feeNumerator); } /// @notice Removes default royalty information. /// @dev After calling this function, royaltyInfo will return (address(0), 0) for tokens without specific royalty. function deleteDefaultRoyalty() internal { - ERC2981Storage storage s = getStorage(); + RoyaltyStorage storage s = getStorage(); delete s.defaultRoyaltyInfo; } @@ -117,7 +119,7 @@ library LibERC2981 { revert ERC2981InvalidTokenRoyaltyReceiver(_tokenId, address(0)); } - ERC2981Storage storage s = getStorage(); + RoyaltyStorage storage s = getStorage(); s.tokenRoyaltyInfo[_tokenId] = RoyaltyInfo(_receiver, _feeNumerator); } @@ -125,7 +127,7 @@ library LibERC2981 { /// @dev Clears token-specific royalty storage, causing fallback to default royalty. /// @param _tokenId The token ID to reset royalty configuration for. function resetTokenRoyalty(uint256 _tokenId) internal { - ERC2981Storage storage s = getStorage(); + RoyaltyStorage storage s = getStorage(); delete s.tokenRoyaltyInfo[_tokenId]; } } diff --git a/src/ERC2981/ERC2981/ERC2981Facet.sol b/src/token/Royalty/RoyaltyFacet.sol similarity index 86% rename from src/ERC2981/ERC2981/ERC2981Facet.sol rename to src/token/Royalty/RoyaltyFacet.sol index ed5f9bfe..a0da9a3c 100644 --- a/src/ERC2981/ERC2981/ERC2981Facet.sol +++ b/src/token/Royalty/RoyaltyFacet.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.30; -/// @title ERC-2981 NFT Royalty Standard -/// @notice Implements royalty queries for NFT secondary sales. +/// @title Royalty Facet - ERC-2981 NFT Royalty Standard Implementation +/// @notice Implements royalty queries for NFT secondary sales per ERC-2981 standard. /// @dev Provides standardized royalty information to NFT marketplaces and platforms. /// Supports both default and per-token royalty configurations using diamond storage. -contract ERC2981Facet { +/// This is an implementation of the ERC-2981 NFT Royalty Standard. +contract RoyaltyFacet { /// @notice Thrown when default royalty fee exceeds 100% (10000 basis points). /// @param _numerator The fee numerator that exceeds the denominator. /// @param _denominator The fee denominator (10000 basis points). @@ -44,15 +45,15 @@ contract ERC2981Facet { } /// @custom:storage-location erc8042:compose.erc2981 - struct ERC2981Storage { + struct RoyaltyStorage { RoyaltyInfo defaultRoyaltyInfo; mapping(uint256 tokenId => RoyaltyInfo) tokenRoyaltyInfo; } - /// @notice Returns a pointer to the ERC-2981 storage struct. + /// @notice Returns a pointer to the royalty storage struct. /// @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. - /// @return s The ERC2981Storage struct in storage. - function getStorage() internal pure returns (ERC2981Storage storage s) { + /// @return s The RoyaltyStorage struct in storage. + function getStorage() internal pure returns (RoyaltyStorage storage s) { bytes32 position = STORAGE_POSITION; assembly { s.slot := position @@ -62,6 +63,7 @@ contract ERC2981Facet { /// @notice Returns royalty information for a given token and sale price. /// @dev Returns token-specific royalty if set, otherwise falls back to default royalty. /// Royalty amount is calculated as a percentage of the sale price using basis points. + /// Implements the ERC-2981 royaltyInfo function. /// @param _tokenId The NFT asset queried for royalty information. /// @param _salePrice The sale price of the NFT asset specified by _tokenId. /// @return receiver The address designated to receive the royalty payment. @@ -71,7 +73,7 @@ contract ERC2981Facet { view returns (address receiver, uint256 royaltyAmount) { - ERC2981Storage storage s = getStorage(); + RoyaltyStorage storage s = getStorage(); RoyaltyInfo memory royalty = s.tokenRoyaltyInfo[_tokenId]; if (royalty.receiver == address(0)) {