From 474ba357164aee311fdcddacadd363f0128a7be6 Mon Sep 17 00:00:00 2001 From: Haroldwonder Date: Sat, 18 Oct 2025 07:04:11 +0100 Subject: [PATCH] Add and refine NatSpec documentation for ERC721Facet and ERC721EnumerableFacet --- README.md | 89 +++++- src/ERC20/ERC20/{ERC20.sol => ERC20Facet.sol} | 12 +- src/ERC20/ERC20/libraries/LibERC20.sol | 34 ++- .../ERC721/{ERC721.sol => ERC721Facet.sol} | 196 ++++++------- src/ERC721/ERC721/libraries/LibERC721.sol | 94 +++++++ .../ERC721EnumerableFacet.sol | 266 ++++++++++++++++++ .../libraries/LibERC721Enumerable.sol | 132 +++++++++ 7 files changed, 694 insertions(+), 129 deletions(-) rename src/ERC20/ERC20/{ERC20.sol => ERC20Facet.sol} (96%) rename src/ERC721/ERC721/{ERC721.sol => ERC721Facet.sol} (55%) create mode 100644 src/ERC721/ERC721/libraries/LibERC721.sol create mode 100644 src/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol create mode 100644 src/ERC721/ERC721Enumerable/libraries/LibERC721Enumerable.sol diff --git a/README.md b/README.md index 0870a227..85d7eb47 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ + +**NOTE:** *Compose is at a very early stage and is currently only available to contributors for building the library. It is NOT production ready.* + +*The Solidity feature ban, and the design of the library described below only apply to the library itself. It does not apply to the *users* of the library -- the people who will use this library to make their diamonds. It is our job to help users do what they want to do.* + + # Compose Forget everything you know about designing and organizing smart contracts -- because Compose is different. @@ -10,7 +16,7 @@ We are breaking existing software development rules in order to write good softw None of the following features in the Solidity programming language are allowed to be used in this smart contract library. Anyone submitting a pull request that uses any of these features will be fined **$100 USDC**. -Endless discussion about what and why Solidity features should or shouldn't be allowed in this library is *encouraged*. +[Endless discussion](https://discord.gg/DCBD2UKbxc) about what and why Solidity features should or shouldn't be allowed in this library is *encouraged*. It isn't that any of these features are bad, that isn't the point. It is that we are writing the best software we can, and part of that is using a limited feature set. This is the "less is more" idea or keep it simple stupid (KISS). @@ -30,15 +36,15 @@ If this feature ban breaks your mind, just realize that this smart contract libr 4. ### No public or private or internal variables - No contract or library may have variables declared private or public or internal. For example: `uint256 public counter;`. These visibility labels are not needed because the library uses ERC-8042 Diamond Storage throughout. + No contract or library may have storage variables declared private or public or internal. For example: `uint256 public counter;`. These visibility labels are not needed because the library uses ERC-8042 Diamond Storage throughout. This restriction does not apply to constants or immutable variables, which may be declared `internal`. -5. ### No private functions +5. ### No private or public functions - No contract or library may have a function declared private. For example: `function approve(address _spender, uint256 _value) private { ...`. This means all functions in contracts must be delcared `internal` or `external`. + No contract or library may have a function declared private or public. For example: `function approve(address _spender, uint256 _value) private { ...`. This means all functions in contracts must be declared `internal` or `external`. 6. ### No external functions in Solidity libraries - No Solidity library may have any external functions. For example: `function name() external view returns (string memory)`. All functions in Solidity libraries must be delcared `internal`. + No Solidity library may have any external functions. For example: `function name() external view returns (string memory)`. All functions in Solidity libraries must be declared `internal`. 7. ### No `using for` in Solidity libraries @@ -46,11 +52,80 @@ If this feature ban breaks your mind, just realize that this smart contract libr 8. ### No `selfdestruct`. - No contract or libary may use `selfdestruct`. + No contract or library may use `selfdestruct`. Other Solidity features will likely be added to this ban list. -Note that the feature ban applies to the smart contracts and libraries within Compose. It does not apply to the users that use Compose. Users can do what they want to do and it is our job to help them. +**Note** that the feature ban applies to the smart contracts and libraries within Compose. It does not apply to the users that use Compose. Users can do what they want to do and it is our job to help them. + +## Purpose of Compose + +The purpose of Compose is to help people create smart contract systems. We want to help them do that quickly, securely, confidently, with understanding, and with the functionality they want. Nothing is more important than this purpose. + +## Vision + +Compose is an effort to apply software engineering principles specifically to a smart contract library. Smart contracts are not like other software, so let's not treat them like other software. We need to re-evaluate knowledge of programming and software engineering specifically as it applies to smart contracts. Let's really look at what smart contracts are and design and write our library for specifically what we are dealing with. + +What we are dealing with: + +1. **Smart contracts are immutable.** Once deployed, the source code for a smart contract doesn't change. +2. **Smart contracts are forever.** Once deployed, smart contracts can run or exist forever. +3. **Smart contracts are shared.** Once deployed, smart contracts can be seen and accessed by anyone. +4. **Smart contracts run on a distributed network.** Once deployed, smart contracts are running within the capabilities and constraints of the Ethereum Virtual Machine (EVM) and the blockchain network it is deployed on. +5. **Smart contracts must be secure.** Once deployed, there can be very serious consequences if their is a bug or security vulnerability in a smart contract. +6. **Smart contracts are written in a specific language** In our case our Compose is written in the Solidity programming language. + +If we gather all knowledge about programming and software engineering that has ever existed and will exist, including what you know and what you will soon learn or know, and we evaluate that knowledge as it can best apply specifically to a smart contract library, to create the best smart contract library possible, what do we end up with? Hopefully we end up with what Compose becomes. + +## Design + +The design and implementation of Compose is based on the following design principles. + +1. ### Understanding + This is the top design and guiding principle of this project. We help our users *understand* the things they want to know so they can *confidently* achieve what they are trying to do. This is why we must have very good documentation, and why we write easy to read and understand code. Understanding leads to solutions, creates confidence, kills bugs and gets things done. Understanding is everything. So we nurture it and create it. + +1. ### The code is written to be read + The code in this library is written to be read and understood by others easily. We want our users to understand our library and be confident with it. We help them do that with code that is easy to read and understand. + + We hope thousands of smart contract systems use our smart contracts. We say in advance to thousands of people in the future, over tens or hundreds of years, who are reading the verified source code of deployed smart contract systems that use our library, **YOU'RE WELCOME**, for making it easy to read and understand. + +1. ### Repeat yourself + The DRY principle — *Don’t Repeat Yourself* — is a well-known rule in software development. We **intentionally** break that rule. + + In traditional software, DRY reduces duplication and makes it easier to update multiple parts of a program by changing one section of code. But deployed smart contracts *don’t change*. DRY can actually reduce clarity. Every internal function adds another indirection that developers must trace through, and those functions sometimes introduce extra logic for different cases. Repetition can make smart contracts easier to read and reason about. + + That said, DRY still has its place. When a large block of code performs a complete, self-contained action and is used identically in multiple locations, moving it into an internal function can improve readability. For example, Compose's ERC-721 implementation uses an `internalTransferFrom` function to eliminate duplication while keeping the code easy to read and understand. + + **Guideline:** Repeat yourself when it makes your code easier to read and understand. Use DRY sparingly and only to make code more readable by removing a lot of unnecessary duplication. + +1. ### Compose diamonds + + A diamond contract is a smart contract that gets its functionality from other contracts called facets. You can add, replace, or remove functionality from these facets, which lets the diamond contract change or grow without deploying a completely new contract. This design makes it easier to build smart contracts that are modular (made of separate parts) and composable (able to work together in flexible ways). A diamond contract can be deployed and then incrementally developed by adding/replacing/removing functionality over time. Diamond contracts can be upgradeable or immutable. [ERC-2535 Diamonds](https://eips.ethereum.org/EIPS/eip-2535) is the standard that defines how diamond contracts work. + + Compose is specifically designed to help users develop and deploy [diamond contracts](https://eips.ethereum.org/EIPS/eip-2535). A major part of this project is creating an onchain diamond factory that makes it easy to deploy diamonds that use facets provided by this library and elsewhere. + + Much of Compose consists of facets and Solidity libraries that are used by users to create diamond contracts. + +1. ### Onchain Composability + + We design facets for maximum onchain reusability and composability. + + We plan to deploy the facets written in this library to many blockchains. There's no reason to take our Solidity source code, as is, and deploy it yourself to a blockchain if it is already deployed there. Just use the facets that are already deployed. We will maintain lists of blockchain addresses for facets that are deployed. + + For example if you want a diamond contract with standard ERC721 NFT functionality, then deploy a diamond contract using this library and add the ERC721 functionality from the existing, already deployed ERC721 facet. You do not need to deploy an ERC721 facet from this library if it has already been deployed to the blockchain you are using. + + Users also have the option of taking our facet source code and modifying it for their needs and deploying what they wish. + + + + + + + + + + + diff --git a/src/ERC20/ERC20/ERC20.sol b/src/ERC20/ERC20/ERC20Facet.sol similarity index 96% rename from src/ERC20/ERC20/ERC20.sol rename to src/ERC20/ERC20/ERC20Facet.sol index 5db33777..c6fcdf99 100644 --- a/src/ERC20/ERC20/ERC20.sol +++ b/src/ERC20/ERC20/ERC20Facet.sol @@ -1,13 +1,8 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity >=0.8.30; -/** - * @title ERC20 - * @notice A minimal ERC-20 token implementation using ERC-8042 diamond storage. - * @dev This contract avoids Solidity inheritance, constructors, and modifiers as per Compose conventions. - * It includes ERC-6093 custom errors for efficient error handling. - */ -contract ERC20 { + +contract ERC20Facet { /// @notice Thrown when an account has insufficient balance for a transfer or burn. @@ -80,7 +75,6 @@ contract ERC20 { } } - /** * @notice Returns the name of the token. * @return The token name. diff --git a/src/ERC20/ERC20/libraries/LibERC20.sol b/src/ERC20/ERC20/libraries/LibERC20.sol index 0a55bb92..85ff4004 100644 --- a/src/ERC20/ERC20/libraries/LibERC20.sol +++ b/src/ERC20/ERC20/libraries/LibERC20.sol @@ -31,22 +31,34 @@ library LibERC20 { assembly { s.slot := position } - } + } - function transfer(address _to, uint256 _value) internal { + function mint(address _account, uint256 _value) internal { ERC20Storage storage s = getStorage(); - if (_to == address(0)) { + if (_account == address(0)) { revert ERC20InvalidReceiver(address(0)); } - uint256 fromBalance = s.balanceOf[msg.sender]; - if (fromBalance < _value) { - revert ERC20InsufficientBalance(msg.sender, fromBalance, _value); + unchecked { + s.totalSupply += _value; + s.balanceOf[_account] += _value; + } + emit Transfer(address(0), _account, _value); + } + + function burn(address _account, uint256 _value) internal { + ERC20Storage storage s = getStorage(); + if (_account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + uint256 accountBalance = s.balanceOf[_account]; + if (accountBalance < _value) { + revert ERC20InsufficientBalance(_account, accountBalance, _value); } unchecked { - s.balanceOf[msg.sender] = fromBalance - _value; - s.balanceOf[_to] += _value; + s.balanceOf[_account] = accountBalance - _value; + s.totalSupply -= _value; } - emit Transfer(msg.sender, _to, _value); + emit Transfer(_account, address(0), _value); } function transferFrom(address _from, address _to, uint256 _value) internal { @@ -71,5 +83,7 @@ library LibERC20 { s.balanceOf[_to] += _value; } emit Transfer(_from, _to, _value); - } + } + + } \ No newline at end of file diff --git a/src/ERC721/ERC721/ERC721.sol b/src/ERC721/ERC721/ERC721Facet.sol similarity index 55% rename from src/ERC721/ERC721/ERC721.sol rename to src/ERC721/ERC721/ERC721Facet.sol index f16ebf62..f2be040a 100644 --- a/src/ERC721/ERC721/ERC721.sol +++ b/src/ERC721/ERC721/ERC721Facet.sol @@ -1,48 +1,73 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity >=0.8.30; - -/// @notice Interface for contracts that want to support safeTransfers. +/// @title ERC-721 Token Receiver Interface +/// @notice Interface for contracts that want to handle safe transfers of ERC-721 tokens. +/// @dev Contracts implementing this must return the selector to confirm token receipt. interface IERC721Receiver { + /// @notice Handles the receipt of an NFT. + /// @param _operator The address which called `safeTransferFrom`. + /// @param _from The previous owner of the token. + /// @param _tokenId The NFT identifier being transferred. + /// @param _data Additional data with no specified format. + /// @return The selector to confirm the token transfer. function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external returns (bytes4); } +/// @title ERC-721 Token (Zero-Dependency Implementation) +/// @notice A complete, dependency-free ERC-721 implementation using the diamond storage pattern. +/// @dev This facet provides metadata, ownership, approvals, safe transfers, minting, burning, and helpers. +contract ERC721Facet { -/// @title ERC-721 token (zero-dependency implementation) -/// @notice A complete, dependency-free ERC-721 implementation using the project's storage pattern. -/// @dev This contract provides metadata, ownership, approvals, safe transfers (with local IERC721Receiver check), -/// minting, burning, and helpers. It intentionally avoids external imports. -contract ERC721 { - - // ERC-6093: Custom errors for ERC-721 + /// @notice Error indicating the queried owner address is invalid (zero address). error ERC721InvalidOwner(address _owner); + + /// @notice Error indicating that the queried token does not exist. error ERC721NonexistentToken(uint256 _tokenId); + + /// @notice Error indicating the sender does not match the token owner. error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + + /// @notice Error indicating the sender address is invalid. error ERC721InvalidSender(address _sender); + + /// @notice Error indicating the receiver address is invalid. error ERC721InvalidReceiver(address _receiver); + + /// @notice Error indicating the operator lacks approval to transfer the given token. error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + + /// @notice Error indicating the approver address is invalid. error ERC721InvalidApprover(address _approver); + + /// @notice Error indicating the operator address is invalid. error ERC721InvalidOperator(address _operator); + /// @notice Emitted when ownership of an NFT changes by any mechanism. event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + /// @notice Emitted when the approved address for an NFT is changed or reaffirmed. event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); + + /// @notice Emitted when an operator is enabled or disabled for an owner. event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); - // Struct storage position defined by keccak256 hash - // of diamond storage identifier bytes32 constant STORAGE_POSITION = keccak256("compose.erc721"); - // Storage defined using the ERC-8042 standard - // @custom:storage-location erc8042:compose.erc721 + /// @custom:storage-location erc8042:compose.erc721 struct ERC721Storage { string name; - string symbol; + string symbol; + mapping(uint256 tokenId => string tokenURI) tokenURIOf; mapping(uint256 tokenId => address owner) ownerOf; mapping(address owner => uint256 balance) balanceOf; mapping(uint256 tokenId => address approved) approved; mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; } + /// @notice Returns a pointer to the ERC-721 storage struct. + /// @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION. + /// @return s The ERC721Storage struct in storage. function getStorage() internal pure returns (ERC721Storage storage s) { bytes32 position = STORAGE_POSITION; assembly { @@ -50,14 +75,21 @@ contract ERC721 { } } + /// @notice Returns the token collection name. + /// @return The name of the token collection. function name() external view returns (string memory) { return getStorage().name; } + /// @notice Returns the token collection symbol. + /// @return The symbol of the token collection. function symbol() external view returns (string memory) { return getStorage().symbol; - } + } + /// @notice Returns the number of tokens owned by a given address. + /// @param _owner The address to query the balance of. + /// @return The balance (number of tokens) owned by `_owner`. function balanceOf(address _owner) external view returns (uint256) { if (_owner == address(0)) { revert ERC721InvalidOwner(_owner); @@ -65,6 +97,9 @@ contract ERC721 { return getStorage().balanceOf[_owner]; } + /// @notice Returns the owner of a given token ID. + /// @param _tokenId The token ID to query. + /// @return The address of the token owner. function ownerOf(uint256 _tokenId) public view returns (address) { address owner = getStorage().ownerOf[_tokenId]; if (owner == address(0)) { @@ -73,6 +108,9 @@ contract ERC721 { return owner; } + /// @notice Returns the approved address for a given token ID. + /// @param _tokenId The token ID to query the approval of. + /// @return The approved address for the token. function getApproved(uint256 _tokenId) external view returns (address) { address owner = getStorage().ownerOf[_tokenId]; if (owner == address(0)) { @@ -81,10 +119,17 @@ contract ERC721 { return getStorage().approved[_tokenId]; } + /// @notice Returns true if an operator is approved to manage all of an owner's assets. + /// @param _owner The token owner. + /// @param _operator The operator address. + /// @return True if the operator is approved for all tokens of the owner. function isApprovedForAll(address _owner, address _operator) external view returns (bool) { return getStorage().isApprovedForAll[_owner][_operator]; } + /// @notice Approves another address to transfer the given token ID. + /// @param _approved The address to be approved. + /// @param _tokenId The token ID to approve. function approve(address _approved, uint256 _tokenId) external { ERC721Storage storage s = getStorage(); address owner = s.ownerOf[_tokenId]; @@ -98,15 +143,22 @@ contract ERC721 { emit Approval(owner, _approved, _tokenId); } + /// @notice Approves or revokes permission for an operator to manage all caller's assets. + /// @param _operator The operator address to set approval for. + /// @param _approved True to approve, false to revoke. function setApprovalForAll(address _operator, bool _approved) external { if (_operator == address(0)) { revert ERC721InvalidOperator(_operator); } getStorage().isApprovedForAll[msg.sender][_operator] = _approved; emit ApprovalForAll(msg.sender, _operator, _approved); - } - - function transferFrom(address _from, address _to, uint256 _tokenId) external { + } + + /// @dev Internal function to transfer a token, checking for ownership and approval. + /// @param _from The current owner of the token. + /// @param _to The address to receive the token. + /// @param _tokenId The token ID to transfer. + function internalTransferFrom(address _from, address _to, uint256 _tokenId) internal { ERC721Storage storage s = getStorage(); if (_to == address(0)) { revert ERC721InvalidReceiver(address(0)); @@ -131,33 +183,22 @@ contract ERC721 { s.ownerOf[_tokenId] = _to; emit Transfer(_from, _to, _tokenId); } + + /// @notice Transfers a token from one address to another. + /// @param _from The current owner of the token. + /// @param _to The address to receive the token. + /// @param _tokenId The token ID to transfer. + function transferFrom(address _from, address _to, uint256 _tokenId) external { + internalTransferFrom(_from, _to, _tokenId); + } + /// @notice Safely transfers a token, checking if the receiver can handle ERC-721 tokens. + /// @param _from The current owner of the token. + /// @param _to The address to receive the token. + /// @param _tokenId The token ID to transfer. function safeTransferFrom(address _from, address _to, uint256 _tokenId) external { - ERC721Storage storage s = getStorage(); - if (_to == address(0)) { - revert ERC721InvalidReceiver(address(0)); - } - address owner = s.ownerOf[_tokenId]; - if (owner == address(0)) { - revert ERC721NonexistentToken(_tokenId); - } - if (owner != _from) { - revert ERC721IncorrectOwner(_from, _tokenId, owner); - } - if (msg.sender != _from) { - if(!s.isApprovedForAll[_from][msg.sender] && msg.sender != s.approved[_tokenId]) { - revert ERC721InsufficientApproval(msg.sender, _tokenId); - } - } - delete s.approved[_tokenId]; - unchecked { - s.balanceOf[_from]--; - s.balanceOf[_to]++; - } - s.ownerOf[_tokenId] = _to; - emit Transfer(_from, _to, _tokenId); + internalTransferFrom(_from, _to, _tokenId); - // If _to is a contract, check for IERC721Receiver implementation if (_to.code.length > 0) { try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, "") returns (bytes4 retval) { if (retval != IERC721Receiver.onERC721Received.selector) { @@ -165,7 +206,6 @@ contract ERC721 { } } catch (bytes memory reason) { if (reason.length == 0) { - // non-IERC721Receiver implementer revert ERC721InvalidReceiver(_to); } else { assembly ("memory-safe") { @@ -176,32 +216,13 @@ contract ERC721 { } } + /// @notice Safely transfers a token with additional data. + /// @param _from The current owner of the token. + /// @param _to The address to receive the token. + /// @param _tokenId The token ID to transfer. + /// @param _data Additional data with no specified format. function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external { - ERC721Storage storage s = getStorage(); - if (_to == address(0)) { - revert ERC721InvalidReceiver(address(0)); - } - address owner = s.ownerOf[_tokenId]; - if (owner == address(0)) { - revert ERC721NonexistentToken(_tokenId); - } - if (owner != _from) { - revert ERC721IncorrectOwner(_from, _tokenId, owner); - } - if (msg.sender != _from) { - if(!s.isApprovedForAll[_from][msg.sender] && msg.sender != s.approved[_tokenId]) { - revert ERC721InsufficientApproval(msg.sender, _tokenId); - } - } - delete s.approved[_tokenId]; - unchecked { - s.balanceOf[_from]--; - s.balanceOf[_to]++; - } - s.ownerOf[_tokenId] = _to; - emit Transfer(_from, _to, _tokenId); - - // If _to is a contract, check for IERC721Receiver implementation + internalTransferFrom(_from, _to, _tokenId); if (_to.code.length > 0) { try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4 retval) { if (retval != IERC721Receiver.onERC721Received.selector) { @@ -209,44 +230,13 @@ contract ERC721 { } } catch (bytes memory reason) { if (reason.length == 0) { - // non-IERC721Receiver implementer revert ERC721InvalidReceiver(_to); - } else { + } else { assembly ("memory-safe") { revert(add(reason, 0x20), mload(reason)) } } } } - } - - function _mint(address _to, uint256 _tokenId) internal { - ERC721Storage storage s = getStorage(); - if (_to == address(0)) { - revert ERC721InvalidReceiver(address(0)); - } - if (s.ownerOf[_tokenId] != address(0)) { - revert ERC721NonexistentToken(_tokenId); - } - s.ownerOf[_tokenId] = _to; - unchecked { - s.balanceOf[_to]++; - } - emit Transfer(address(0), _to, _tokenId); - } - - function _burn(uint256 _tokenId) internal { - ERC721Storage storage s = getStorage(); - address owner = s.ownerOf[_tokenId]; - if (owner == address(0)) { - revert ERC721NonexistentToken(_tokenId); - } - delete s.ownerOf[_tokenId]; - delete s.approved[_tokenId]; - unchecked { - s.balanceOf[owner]--; - } - emit Transfer(owner, address(0), _tokenId); - } - -} \ No newline at end of file + } +} diff --git a/src/ERC721/ERC721/libraries/LibERC721.sol b/src/ERC721/ERC721/libraries/LibERC721.sol new file mode 100644 index 00000000..97ac8079 --- /dev/null +++ b/src/ERC721/ERC721/libraries/LibERC721.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +library LibERC721 { + + // ERC-6093: Custom errors for ERC-721 + error ERC721NonexistentToken(uint256 _tokenId); + error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + error ERC721InvalidSender(address _sender); + error ERC721InvalidReceiver(address _receiver); + error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + // Struct storage position defined by keccak256 hash + // of diamond storage identifier + bytes32 constant STORAGE_POSITION = keccak256("compose.erc721"); + + // Storage defined using the ERC-8042 standard + // @custom:storage-location erc8042:compose.erc721 + struct ERC721Storage { + string name; + string symbol; + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256 balance) balanceOf; + mapping(uint256 tokenId => address approved) approved; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + } + + function getStorage() internal pure returns (ERC721Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + function transferFrom(address _from, address _to, uint256 _tokenId) internal { + ERC721Storage storage s = getStorage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (owner != _from) { + revert ERC721IncorrectOwner(_from, _tokenId, owner); + } + if (msg.sender != _from) { + if(!s.isApprovedForAll[_from][msg.sender] && msg.sender != s.approved[_tokenId]) { + revert ERC721InsufficientApproval(msg.sender, _tokenId); + } + } + delete s.approved[_tokenId]; + unchecked { + s.balanceOf[_from]--; + s.balanceOf[_to]++; + } + s.ownerOf[_tokenId] = _to; + emit Transfer(_from, _to, _tokenId); + } + + function mint(address _to, uint256 _tokenId) internal { + ERC721Storage storage s = getStorage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + if (s.ownerOf[_tokenId] != address(0)) { + revert ERC721InvalidSender(address(0)); + } + s.ownerOf[_tokenId] = _to; + unchecked { + s.balanceOf[_to]++; + } + emit Transfer(address(0), _to, _tokenId); + } + + function burn(uint256 _tokenId) internal { + ERC721Storage storage s = getStorage(); + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + delete s.ownerOf[_tokenId]; + delete s.approved[_tokenId]; + unchecked { + s.balanceOf[owner]--; + } + emit Transfer(owner, address(0), _tokenId); + } + + + +} \ No newline at end of file diff --git a/src/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol b/src/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol new file mode 100644 index 00000000..70e614d5 --- /dev/null +++ b/src/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/// @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. +interface IERC721Receiver { + /// @notice Handles the receipt of an NFT. + /// @param _operator The address which initiated the transfer. + /// @param _from The previous owner of the token. + /// @param _tokenId The NFT identifier being transferred. + /// @param _data Additional data with no specified format. + /// @return A bytes4 value indicating acceptance of the transfer. + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external returns (bytes4); +} + +/// @title ERC-721 Enumerable Token (Zero-dependency Implementation) +/// @notice A complete, dependency-free ERC-721 implementation with enumeration support using a custom storage layout. +/// @dev Provides metadata, ownership, approvals, enumeration, safe transfers, minting, and burning features. +contract ERC721EnumerableFacet { + + + /// @notice Thrown when querying or transferring from an invalid owner address. + error ERC721InvalidOwner(address _owner); + /// @notice Thrown when operating on a non-existent token. + error ERC721NonexistentToken(uint256 _tokenId); + /// @notice Thrown when the provided owner does not match the actual owner of the token. + error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + /// @notice Thrown when the sender address is invalid. + error ERC721InvalidSender(address _sender); + /// @notice Thrown when the receiver address is invalid. + error ERC721InvalidReceiver(address _receiver); + /// @notice Thrown when the operator lacks sufficient approval for a transfer. + error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + /// @notice Thrown when an invalid approver is provided. + error ERC721InvalidApprover(address _approver); + /// @notice Thrown when an invalid operator is provided. + error ERC721InvalidOperator(address _operator); + /// @notice Thrown when an index is out of bounds during enumeration. + error ERC721OutOfBoundsIndex(address _owner, uint256 _index); + + + /// @notice Emitted when a token is transferred between addresses. + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + /// @notice Emitted when a token is approved for transfer by another address. + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); + /// @notice Emitted when an operator is approved or revoked for all tokens of an owner. + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + + + bytes32 constant STORAGE_POSITION = keccak256("compose.erc721.enumerable"); + + /// @custom:storage-location erc8042:compose.erc721.enumerable + struct ERC721EnumerableStorage { + string name; + string symbol; + mapping(uint256 tokenId => string tokenURI) tokenURIOf; + + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256[] ownedTokens) ownedTokensOf; + mapping(uint256 tokenId => uint256 ownedTokensIndex) ownedTokensIndexOf; + uint256[] allTokens; + mapping(uint256 tokenId => uint256 allTokensIndex) allTokensIndexOf; + + mapping(uint256 tokenId => address approved) approved; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + } + + /// @notice Returns the storage struct used by this facet. + /// @return s The ERC721Enumerable storage struct. + function getStorage() internal pure returns (ERC721EnumerableStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + + /// @notice Returns the name of the token collection. + /// @return The token collection name. + function name() external view returns (string memory) { + return getStorage().name; + } + + /// @notice Returns the symbol of the token collection. + /// @return The token symbol. + function symbol() external view returns (string memory) { + return getStorage().symbol; + } + + /// @notice Returns the total number of tokens in existence. + /// @return The total supply of tokens. + function totalSupply() external view returns (uint256) { + return getStorage().allTokens.length; + } + + + /// @notice Returns the number of tokens owned by an address. + /// @param _owner The address to query. + /// @return The balance (number of tokens owned). + function balanceOf(address _owner) external view returns (uint256) { + if (_owner == address(0)) { + revert ERC721InvalidOwner(_owner); + } + return getStorage().ownedTokensOf[_owner].length; + } + + /// @notice Returns the owner of a given token ID. + /// @param _tokenId The token ID to query. + /// @return The address of the token owner. + function ownerOf(uint256 _tokenId) public view returns (address) { + address owner = getStorage().ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + return owner; + } + + /// @notice Returns a token ID owned by a given address at a specific index. + /// @param _owner The address to query. + /// @param _index The index of the token. + /// @return The token ID owned by `_owner` at `_index`. + function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256) { + ERC721EnumerableStorage storage s = getStorage(); + if (_index >= s.ownedTokensOf[_owner].length) { + revert ERC721OutOfBoundsIndex(_owner, _index); + } + return s.ownedTokensOf[_owner][_index]; + } + + + /// @notice Returns the approved address for a given token ID. + /// @param _tokenId The token ID to query. + /// @return The approved address for the token. + function getApproved(uint256 _tokenId) external view returns (address) { + address owner = getStorage().ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + return getStorage().approved[_tokenId]; + } + + /// @notice Returns whether an operator is approved for all tokens of an owner. + /// @param _owner The token owner. + /// @param _operator The operator address. + /// @return True if approved for all, false otherwise. + function isApprovedForAll(address _owner, address _operator) external view returns (bool) { + return getStorage().isApprovedForAll[_owner][_operator]; + } + + /// @notice Approves another address to transfer a specific token ID. + /// @param _approved The address being approved. + /// @param _tokenId The token ID to approve. + function approve(address _approved, uint256 _tokenId) external { + ERC721EnumerableStorage storage s = getStorage(); + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (msg.sender != owner && !s.isApprovedForAll[owner][msg.sender]) { + revert ERC721InvalidApprover(_approved); + } + s.approved[_tokenId] = _approved; + emit Approval(owner, _approved, _tokenId); + } + + /// @notice Approves or revokes an operator to manage all tokens of the caller. + /// @param _operator The operator address. + /// @param _approved True to approve, false to revoke. + function setApprovalForAll(address _operator, bool _approved) external { + if (_operator == address(0)) { + revert ERC721InvalidOperator(_operator); + } + getStorage().isApprovedForAll[msg.sender][_operator] = _approved; + emit ApprovalForAll(msg.sender, _operator, _approved); + } + + + /// @notice Internal function to transfer ownership of a token ID. + /// @param _from The address sending the token. + /// @param _to The address receiving the token. + /// @param _tokenId The token ID being transferred. + function internalTransferFrom(address _from, address _to, uint256 _tokenId) internal { + ERC721EnumerableStorage storage s = getStorage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (owner != _from) { + revert ERC721IncorrectOwner(_from, _tokenId, owner); + } + if (msg.sender != _from) { + if (!s.isApprovedForAll[_from][msg.sender] && msg.sender != s.approved[_tokenId]) { + revert ERC721InsufficientApproval(msg.sender, _tokenId); + } + } + delete s.approved[_tokenId]; + + uint256 tokenIndex = s.ownedTokensIndexOf[_tokenId]; + uint256 lastTokenIndex = s.ownedTokensOf[_from].length - 1; + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.ownedTokensOf[_from][lastTokenIndex]; + s.ownedTokensOf[_from][tokenIndex] = lastTokenId; + s.ownedTokensIndexOf[lastTokenId] = tokenIndex; + } + s.ownedTokensOf[_from].pop(); + + s.ownedTokensIndexOf[_tokenId] = s.ownedTokensOf[_to].length; + s.ownedTokensOf[_to].push(_tokenId); + s.ownerOf[_tokenId] = _to; + + emit Transfer(_from, _to, _tokenId); + } + + /// @notice Transfers a token from one address to another. + /// @param _from The current owner of the token. + /// @param _to The recipient address. + /// @param _tokenId The token ID to transfer. + function transferFrom(address _from, address _to, uint256 _tokenId) external { + internalTransferFrom(_from, _to, _tokenId); + } + + /// @notice Safely transfers a token, checking for receiver contract compatibility. + /// @param _from The current owner of the token. + /// @param _to The recipient address. + /// @param _tokenId The token ID to transfer. + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external { + internalTransferFrom(_from, _to, _tokenId); + if (_to.code.length > 0) { + try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, "") returns (bytes4 retval) { + if (retval != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) revert ERC721InvalidReceiver(_to); + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } + + /// @notice Safely transfers a token with additional data. + /// @param _from The current owner of the token. + /// @param _to The recipient address. + /// @param _tokenId The token ID to transfer. + /// @param _data Additional data to send to the receiver contract. + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external { + internalTransferFrom(_from, _to, _tokenId); + if (_to.code.length > 0) { + try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4 retval) { + if (retval != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(_to); + } + } catch (bytes memory reason) { + if (reason.length == 0) revert ERC721InvalidReceiver(_to); + assembly ("memory-safe") { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } +} diff --git a/src/ERC721/ERC721Enumerable/libraries/LibERC721Enumerable.sol b/src/ERC721/ERC721Enumerable/libraries/LibERC721Enumerable.sol new file mode 100644 index 00000000..ac0b5e64 --- /dev/null +++ b/src/ERC721/ERC721Enumerable/libraries/LibERC721Enumerable.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +library LibERC721 { + + // ERC-6093: Custom errors for ERC-721 + error ERC721NonexistentToken(uint256 _tokenId); + error ERC721IncorrectOwner(address _sender, uint256 _tokenId, address _owner); + error ERC721InvalidSender(address _sender); + error ERC721InvalidReceiver(address _receiver); + error ERC721InsufficientApproval(address _operator, uint256 _tokenId); + + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + // Struct storage position defined by keccak256 hash + // of diamond storage identifier + bytes32 constant STORAGE_POSITION = keccak256("compose.erc721.enumerable"); + + // Storage defined using the ERC-8042 standard + // @custom:storage-location erc8042:compose.erc721.enumerable + struct ERC721EnumerableStorage { + string name; + string symbol; + + mapping(uint256 tokenId => address owner) ownerOf; + mapping(address owner => uint256[] ownedTokens) ownedTokensOf; + mapping(uint256 tokenId => uint256 ownedTokensIndex) ownedTokensIndexOf; + uint256[] allTokens; + mapping(uint256 tokenId => uint256 allTokensIndex) allTokensIndexOf; + + mapping(uint256 tokenId => address approved) approved; + mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll; + } + + function getStorage() internal pure returns (ERC721EnumerableStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + function transferFrom(address _from, address _to, uint256 _tokenId, address _sender) internal { + ERC721EnumerableStorage storage s = getStorage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (owner != _from) { + revert ERC721IncorrectOwner(_from, _tokenId, owner); + } + if (_sender != _from) { + if(!s.isApprovedForAll[_from][_sender] && _sender != s.approved[_tokenId]) { + revert ERC721InsufficientApproval(_sender, _tokenId); + } + } + delete s.approved[_tokenId]; + // removing token from _from's ownedTokens + uint256 tokenIndex = s.ownedTokensIndexOf[_tokenId]; + uint256 lastTokenIndex = s.ownedTokensOf[_from].length - 1; + if(tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.ownedTokensOf[_from][lastTokenIndex]; + s.ownedTokensOf[_from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token + s.ownedTokensIndexOf[lastTokenId] = tokenIndex; // Update the moved token's index + } + s.ownedTokensOf[_from].pop(); + // adding token to _to's ownedTokens + s.ownedTokensIndexOf[_tokenId] = s.ownedTokensOf[_to].length; + s.ownedTokensOf[_to].push(_tokenId); + s.ownerOf[_tokenId] = _to; + emit Transfer(_from, _to, _tokenId); + } + + function mint(address _to, uint256 _tokenId) internal { + ERC721EnumerableStorage storage s = getStorage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + if (s.ownerOf[_tokenId] != address(0)) { + revert ERC721InvalidSender(address(0)); + } + s.ownedTokensIndexOf[_tokenId] = s.ownedTokensOf[_to].length; + s.ownedTokensOf[_to].push(_tokenId); + + s.allTokensIndexOf[_tokenId] = s.allTokens.length; + s.allTokens.push(_tokenId); + + emit Transfer(address(0), _to, _tokenId); + } + + function burn(uint256 _tokenId, address _sender) internal { + ERC721EnumerableStorage storage s = getStorage(); + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (_sender != owner) { + if(!s.isApprovedForAll[owner][_sender] && _sender != s.approved[_tokenId]) { + revert ERC721InsufficientApproval(_sender, _tokenId); + } + } + delete s.ownerOf[_tokenId]; + delete s.approved[_tokenId]; + + // removing token from _from's ownedTokens + uint256 tokenIndex = s.ownedTokensIndexOf[_tokenId]; + uint256 lastTokenIndex = s.ownedTokensOf[owner].length - 1; + if(tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.ownedTokensOf[owner][lastTokenIndex]; + s.ownedTokensOf[owner][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token + s.ownedTokensIndexOf[lastTokenId] = tokenIndex; // Update the moved token's index + } + s.ownedTokensOf[owner].pop(); + + // removing token from allTokens + tokenIndex = s.allTokensIndexOf[_tokenId]; + lastTokenIndex = s.allTokens.length - 1; + if(tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.allTokens[lastTokenIndex]; + s.allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token + s.allTokensIndexOf[lastTokenId] = tokenIndex; // Update the moved token's index + } + s.allTokens.pop(); + + emit Transfer(owner, address(0), _tokenId); + } + + + +} \ No newline at end of file