# docs.efp.app llms-full.txt > Ethereum Follow Protocol (EFP) is an onchain social graph protocol for Ethereum accounts. > Updated at: 22:35 05/28/25 **Is EFP a social network?** > No, EFP is just a social graph. It has no names or profiles (use [ENS](https://ens.domains/) for that), no authentication protocol (use [SIWE](https://login.xyz/)), nor posting or tweeting. It's a primitive of the Ethereum identity stack meant to be combined with others elements in that stack in third party apps. [This article](https://mirror.xyz/brantly.eth/7nJZCqyvhbdTIfq4oSnNEjlUUyxS9sf3pTHcBNi8Te8) explains the vision. **Can I post or tweet on EFP?** > No, EFP is just a social graph (e.g. who follows who). However, web3 social networks could use EFP as their social graph (just as they could use ENS for their usernames and profiles). **So I followed some people on EFP, now what?** > In the EFP app, there's an Onchain Feed (powered by [Interface](https://interface.social/)) showing the onchain activity of the people you follow, plus the Leaderboard that shows how you stack up against other EFP users. But the most important place to use EFP is in other apps that integrate it. We keep a non-exhaustive list of apps that have integrated EFP on our homepage. **How can my app integrate EFP?** > Any way you want; EFP is open protocol. Ideas include: using EFP to provide additional context for who an Ethereum account is (e.g. showing a user's EFP follower and following counts on their profile, showing friends of friends that follow them, etc), using followings for contacts or message filtering, recommendations, etc. [Our API](https://docs.efp.app/api/) is a convenient way to get EFP data. Message us on Discord or elsewhere to let us know you integrated EFP and we'll put your logo and link on our website. **Is EFP centralized?** > No. The core components of EFP are all onchain and decentralized. Our team maintains and runs an Indexer that mirrors all onchain EFP data to an offchain database for easy access and analysis for serving EFP data through our API. Our indexer is opensource, and anyone can spin up their own indexer or build their own. **Is there an organization behind the creation and maintenance of EFP?** > Yes, EFP is developed by the non-profit corporation Follow Protocol Foundation. **How is EFP funded?** > So far, it has been funded mostly by grants from the ENS DAO. We list the sources of our major grants on our homepage. **What is the relationship of EFP to ENS?** > EFP has a close relationship to ENS: EFP is designed to completement ENS and other elements of the Ethereum identity stack, e.g. EFP has no names or profiles, since it assumes composability with ENS; the ENS DAO has provided large grants to the development of EFP; and the creator of EFP, brantly.eth, used to be on the ENS core team and is still involved in the ENS DAO. **Can I have more than one EFP List?** > Yes, but it's usually not needed since you can use tags to sort different groups of people you follow in your one list. If you do have more than one List, only one can be designated as your Primary List (the list that represents your Ethereum account) at a time. **Can I follow other identifiers besides Ethereum addresses?** > Right now, EFP only support Ethereum addresses, but we plan to support other identifiers (ENS names, NFT smart contracts, etc) in the future. **Can I see the EFP code?** > Yep, it's all [open source on Github](https://github.com/ethereumfollowprotocol). **Can I reset a list? As in, reset the roles and clear all the list records.** > If you are the Owner role of the list, then yes. This is useful if you bought an EFP List number on a secondary market, or just want to start over who you're following but keep your same list number. Here's how: 1) In the Connect Wallet menu, ensure you have selected the list you want to reset. 2) Go to My Profile in the nav bar, click the 3 dot menu next to your name, and select List Settings. 3) Click Edit Settings, then Reset List. You'll then be prompted to do two transactions. **I'm bored.!** > After you've set up your list on EFP, [check this out](https://hackertyper.net/). --- Ethereum Follow Protocol (EFP) is an onchain social graph protocol for Ethereum accounts. ## EFP List NFT Users mint an EFP List NFT to create an **EFP List**. Minting an EFP List NFT is free (plus gas). ### Roles Every EFP List has three roles, each of which are held by an Ethereum address. 1. **Owner:** - Is the owner of the EFP List NFT - Can transfer ownership of the EFP List NFT - Can edit the List Storage Location, which stores the records for that list, as well as who the Manager and User are 2. **Manager:** - This is set in the List Records contract, not the NFT - Is the manager of the EFP list records and metadata - Can transfer the Manager role to another address - Can set or update the user - Can add/remove list records and add/remove tags - Can add metadata key/value to the list 3. **User:** - This is set in the List Records contract, not the NFT - The Ethereum address for whom the list is intended; the Ethereum address that is following the Ethereum addresses in the EFP List. Typically, all three roles (Owner, Manager, User) are the same Ethereum address, but they can be different. --- ## List Storage Location Your EFP List NFT specifies a **List Storage Location** where the **List Records** are stored, which can be one of the following: - Ethereum L1 smart contract - Ethereum L2 smart contract - CCIP-read pointers for off-chain storage (future) The List Storage Location itself (the smart contract or off-chain system) must specify a Manager role, an Ethereum account that is able to edit the List Records. Typically, the Manager will be the same Ethereum account as the Owner and User roles of the EFP List NFT, but they can be different. To prevent frontrunning, a user should first claim a slot number in their chosen List Storage Location, then mint their EFP NFT and set their List Storage Location (with the chain, smart contract address, and secured slot number). --- ## List Records An EFP List is formed from a set of **List Records**. Each record has a record type, but only one record type is supported at launch: - **Address Record** - Contains an Ethereum address, with zero or more tags. - These records are typically interpreted as a "follow" of the specified Ethereum address, with some exceptions explained in the Followers section below. --- ## Order of operations While a user may interact with the EFP smart contracts in any order (no order is enforced in the smart contracts), it's recommended that to prevent frontrunning a user should first claim a slot number in their chosen List Storage Location, then next mint their EFP NFT and set their List Storage Location (with the chain, smart contract address, and secured slot number). --- ## Tags A **Tag** is a string associated with a List Record in an EFP list. Tags only count for an account if that account is also followed by the user, otherwise they're not counted. List Records can have zero or more tags. A few tags are standardized with specified semantics. Users may also set custom tags. ## Standard Tags - **no tag** - If a List Record has no tags, it is interpreted as a simple follow without further categorization. - **"block"** - This tag means neither the user nor the blocked account should see each other’s activity in a relevant app. - List Records with this tag are not included in Followers count, even if the List Record has other tags. - If both “block” and “mute” tags are present, “block” takes precedence. - **"mute"** - This tag means the user shouldn't see the muted account’s activity in a relevant app, but the muted account might still be able to see the user’s activity. - List Records with this tag are not included in Followers count, even if the List Record has other tags. - If both “block” and “mute” tags are present, “block” takes precedence. - **"top8"** - This tag means the account should appear in the user's "Top 8" in UIs that support it. - If a user has more than eight followed accounts with the "top8" tag, then only show the eight most recent should be included in a "Top 8" displayed in a UI. ### Custom Tags Users can use additional arbitrary custom tags. A custom tag can be any UTF-8 string with the following constraints: - maximum length of 255 bytes - no leading or trailing whitespace - more constraints to be added as needed --- ## Account Metadata EFP provides an Account Metadata contract that allows users to set EFP-related metadata specific to their Ethereum account, namely to specify a Primary List. ### Primary List Determining if a list is a Primary List is a two step process: the Ethereum account that set it as the Primary List in Account Metadata must match the User role for the list. Apps should first check the Primary List value for an Ethereum account, and if set, default to using that EFP List for that Ethereum account. Only Primary Lists are counted as Followers. --- ## Social Graph The social graph is formed from the union of all Primary Lists. The User role of each Primary List determines which Ethereum account is following the Ethereum addresses in that List. ### Followers **Followers** is the total number of EFP Primary Lists that follow a particular account, excluding those whoe block or mute the account. ### Following **Following** is the total number of unique Ethereum accounts followed by a list, excluding accounts tagged with “block” or “mute”. --- ## Base Colors | Color | Hex | | ------------- | ------- | | Dark Grey | #333333 | | Yellow | #FFF500 | | Pink | #FF79C9 | | Follow Button | #FFE066 | | Addition | #A1F783 | | Deletion | #FF7C7C | | Text Neutral | #999999 | ## Theme Colors | Theme | Neutral | Text | Grey | NavItem | | --------- | ------- | ------- | ------- | --------- | | Light | #ffffff | #000000 | #E4E4E7 | #b4b4b4 | | Dark | #333333 | #ffffff | #71717A | #94a3b822 | | Halloween | #000000 | #ffffff | #61616A | #94a3b822 | --- List of **EFP Logos** in various file formats. | Format | SVG | PNG | | --- | --- | --- | | Logo With Text (light) | Full Logo SVG **Download** | Full Logo PNG **Download** | | Logo With Text (dark) | Full Logo SVG **Download** | Full Logo PNG **Download** | | Logo Only | Logo SVG **Download** | Logo PNG **Download** | --- EFP is for everyone, and that includes translating the app to make it more easily accessible to a wider range of users. Please consider using your expertise to help us do that! *Before submitting a translation, please read this entire page carefully.* ## Rewards Those who either submit a full translation or make a significant contribution to one may earn: - Up to **$200** (paid in Ethereum L1 USDC) - A special **EFP Translator POAP** Whether a submitter earns one, both, or part the above rewards is determined at our discretion, based on the quality of the submission and extent of their contribution. If you earn a reward, we'll send it to you soon after your translation has been merged to our app. ## Qualifying Languages We now have a large number of languages, thanks to everyone who contributed! Going forward, we will focus on languages with a significant number of speakers and/or significant number of crypto users. Check which languages we already have and only submit if you think it meets the above criteria. Feel free to email us asking us if the language would be accepted before you make the translation. ## How to Submit or Contribute **Before submitting a full translation for a new language, check to see if we already have a translation for it.** You can check the language selector menu in the app or the [translations folder on Github](https://github.com/ethereumfollowprotocol/app/tree/main/public/locales), and you may want to check [pull requests](https://github.com/ethereumfollowprotocol/app/pulls) to see if one is pending approval. **Only submit or contribute to translations for languages of which you are a native speaker** (or, in the case of dead or fictional languages, a fluent speaker). Don't use ChatGPT, Google Translate, or similar services. EFP uses niche, technical language, so **we need crypto-natives who can give us the correct translations**. You can use one of two methods for submitting a translation: ### 1) Github Make a copy of [this folder](https://github.com/ethereumfollowprotocol/app/tree/main/public/locales/en) which contains the English translation, translate all the words and phrases on the right side of the ":" in each line from English to the new language, and submit your translation as a pull request. Please kee all quotes, line breaks, and commas in place. Be sure to include your ENS name (for Ethereum USDC) and an email address (for POAP claim link) in your pull request so that, if accepted, we can send you your rewards. If you'd like to keep that information private, feel free to email them to [translations@ethfollow.xyz](mailto:translations@ethfollow.xyz). ### 2) Email If you don't know how to use Github, go to [this page](https://github.com/ethereumfollowprotocol/app/blob/main/public/locales/en/translations.json), copy and paste the text into an email, translate all the words and phrases on the right side of the ":" in each line from English to the new language, and email the translation to us at [translations@ethfollow.xyz](mailto:translations@ethfollow.xyz). Please kee all quotes, line breaks, and commas in place. Be sure to include your ENS name (for Ethereum USDC) and an email address (for POAP claim link) in your pull request so that, if accepted, we can send you your rewards. ## Corrections If you are a native speaker of a language and see an improper translation, you may submit a correction by the above mentioned methods. Your potential reward will be proportionate to your contribution. ## Ongoing expansions to translations As we add new features to the app, we will add new text in English. If a translation doesn't exist for a particular word or phrase, the app will fallback to the English translation. So **if you see English when using another language in the app, it's likely the language's translation file needs a translation added for that word or phrase**. If you are a native speaker of a language and see a missing translation, you may submit an update by the above mentioned methods. Your potential reward will be proportionate to your contribution. --- A **List Op** is a structure used to encapsulate an operation performed on a List. It includes the following fields: - `version`: A `uint8` representing the version of the List Op. This byte defines the schema of the subsequent bytes for encoding/decoding. This is used to ensure compatibility and facilitate future upgrades. - `opcode`: A `uint8` indicating the operation code. This defines the action to be taken using the List Op. - `data`: A `bytes` array which holds the operation-specific data. For instance, if the operation involves adding a List Record, this field would contain the encoded List Record. The version is always `1`. ## Operation Codes There are four operations defined at this time: | Code | Operation | Data | | ------- | ------------- | ------------------------------------ | | 0 | Reserved | N/A | | 1 | Add record | Encoded `ListRecord` | | 2 | Remove record | Encoded `ListRecord` | | 3 | Tag record | Encoded `ListRecord` followed by tag | | 4 | Untag record | Encoded `ListRecord` followed by tag | | 5 - 255 | Reserved | N/A | ## Encoding `ListOps` are encoded as byte arrays, starting with a one-byte `version` and a one-byte `opcode`, followed by the `data` of variable length. ``` +------------------+-----------------+-------------------------------+ | version (1 byte) | opcode (1 byte) | data (variable length) | +------------------+-----------------+-------------------------------+ ``` The encoding of a `ListOp` is designed to be flexible, accommodating various types of operations and their corresponding data structures. | Byte(s) | Description | | ------- | ------------------------------- | | 0 | `ListOp` version (1 byte) | | 1 | Operation code (1 byte) | | 2 - N | Encoded operation-specific data | The `2 - N` byte range is variable and depends on the operation being performed. ### Example - Add Record The following is an example of an encoded `ListOp` for adding a `ListRecord` of type 1 (address record) to a list: | Byte(s) | Description | Value | | ------- | --------------------------------- | -------------------------------------------- | | 0 | `ListOp` version (1 byte) | `0x01` | | 1 | Operation code (1 byte) | `0x01` | | 2 | `ListRecord` version (1 byte) | `0x01` | | 3 | `ListRecord` record type (1 byte) | `0x01` | | 4 - 23 | `ListRecord` data (20 bytes) | `0x00000000000000000000000000000000DeaDBeef` | ### Example - Remove Record The following is an example of an encoded `ListOp` for removing a `ListRecord` of type 1 (address record) from a list: | Byte(s) | Description | Value | | ------- | --------------------------------- | -------------------------------------------- | | 0 | `ListOp` version (1 byte) | `0x01` | | 1 | Operation code (1 byte) | `0x02` | | 2 | `ListRecord` version (1 byte) | `0x01` | | 3 | `ListRecord` record type (1 byte) | `0x01` | | 4 - 23 | `ListRecord` data (20 bytes) | `0x00000000000000000000000000000000DeaDBeef` | ### Example - Tag Record The following is an example of an encoded `ListOp` for tagging a `ListRecord` of type 1 (address record) in a list: | Byte(s) | Description | Value | | ------- | --------------------------------- | -------------------------------------------- | | 0 | `ListOp` version (1 byte) | `0x01` | | 1 | Operation code (1 byte) | `0x03` | | 2 | `ListRecord` version (1 byte) | `0x01` | | 3 | `ListRecord` record type (1 byte) | `0x01` | | 4 - 23 | `ListRecord` data (20 bytes) | `0x00000000000000000000000000000000DeaDBeef` | | 24 - N | Tag (variable) (UTF-8) | `0x746167` ("tag") | The tag should be encoded as UTF-8. ### Example - Untag Record The following is an example of an encoded `ListOp` for untagging a `ListRecord` of type 1 (address record) in a list: | Byte(s) | Description | Value | | ------- | --------------------------------- | -------------------------------------------- | | 0 | `ListOp` version (1 byte) | `0x01` | | 1 | Operation code (1 byte) | `0x04` | | 2 | `ListRecord` version (1 byte) | `0x01` | | 3 | `ListRecord` record type (1 byte) | `0x01` | | 4 - 23 | `ListRecord` data (20 bytes) | `0x00000000000000000000000000000000DeaDBeef` | | 24 - N | Tag (variable) (UTF-8) | `0x746167` ("tag") | The tag should be encoded as UTF-8. ## Code List Op can be represented as a type in any programming language. Here are some examples: #### Go ```go type ListOp struct { Version uint8 Opcode uint8 Data []byte } ``` #### Python ```python class ListOp: version: int # 0-255 opcode: int # 0-255 data: bytes ``` #### Rust ```rust struct ListOp { version: u8, opcode: u8, data: Vec, } ``` #### Solidity ```solidity /** * @dev The EFP contracts don't use this struct; they only store list ops * as `bytes` with the version, opcode, and data fields tightly packed * into a single `bytes` value. However, this struct can be useful for * offchain processing with foundry or other Solidity tooling */ struct ListOp { uint8 version; uint8 opcode; bytes data; } ``` #### TypeScript ```typescript type ListOp = { version: number // 0-255 opcode: number // 0-255 data: Uint8Array } ``` --- Every Ethereum account (address) automatically is supported by EFP. Users may follow any other address and any address can mint an EFP List NFT. It useful to store EFP-related metadata that is specific to an account, such as a user's primary EFP list. This is key-value metadata for an account is called **Account Metadata**. Any Ethereum account may store key-value data in the `EFPAccountMetadata` contract specific to their account. Data is stored by `string` key and `bytes` value, for each Ethereum account. This allows for the storage of account-specific EFP configuration or preference data. ## Global Keys Global Keys must be made up of lowercase letters, numbers and the hyphen (-) character. There is only one global key for account metadata currently defined. | Key | Description | | -------------- | --------------------------------------------------------------- | | `primary-list` | The 32-byte token ID of the primary EFP List NFT for an account | ## primary-list The `primary-list` key is used to specify the Primary List for an account. (Note: For a list to be considered a Primary List, the User role for the list must match the account that specified the list as its Primary List in Account Metadata. Only Primary Lists are counted in Followers.) The Primary List is represented as a 32-byte token id. | Byte(s) | Description | Value | | --- | --- | --- | | 0-31 | Token ID (32 bytes) | 0x0000000000000000000000000000000000000000000000000000000000000001 | ### Code with example code shown below: ```solidity // set the primary EFP List for the caller's address efpAccountMetadata.setValue("primary-list", abi.encodePacked(tokenId)); ``` By reading the `primary-list` key for a given address, a client can determine the primary EFP List for that address. ```solidity address addr =
uint primaryEfpListTokenId = abi.decode(efpAccountMetadata.getValue(addr, "primary-list"), (uint)); // validate: primary EFP List must exist require(primaryEfpListTokenId < efpListRegistry.totalSupply()); // validate the user for this EFP List is the caller address user = abi.decode(efpListMetadata.getValue(primaryEfpListTokenId, "user"), (address)); require(user == addr); ``` ## Custom Keys The format for custom keys are undefined at this time but will be defined in the future. --- Every EFP List has three roles, each of which are held by an Ethereum address. 1. **Owner** - Is the ERC-721 owner of the EFP List NFT - Can transfer ownership of the EFP List NFT - Can set or update the List Storage Location 2. **Manager** - Set in the List Metadata, stored in the List Storage Location - Is the manager of the List Records and List Metadata - Can transfer the Manager role to another address - Can set or update the User role - Can add or remove list records to the list - Can add or remove tags - Can set or update List Metadata 3. **User** - Set in the List Metadata, stored in the List Storage Location - Is the Ethereum address for whom the list is intended; the Ethereum account that is following the Ethereum accounts (list records) in the list. Typically, all three roles (Owner, Manager, User) are the same Ethereum account, but they can be different. The Owner role is the most important since it controls the List Storage Location, so use a secure wallet (e.g. hardware wallet) to hold the EFP List NFT and always exercise caution when transferring ownership or updating the List Storage Location. Only you are responsible for securing your EFP List NFT. --- Every EFP List has associated key-value metadata called **List Metadata**. List Metadata is stored onchain alongside List Records in the same contract, but in a different format (key-value pairs). Data is stored with a `string` key and `bytes` value, for each EFP List. Only the Manager of the EFP List can set or update the metadata for a list. This allows Managers to store list-specific configuration or preference data. ## Global keys Global Keys must be made up of lowercase letters, numbers and the hyphen (-) character. There are currently two global keys defined for List Metadata: | Key | Description | | --------- | ---------------------------------------------------------------------- | | `manager` | The Ethereum address of the Manager role associated with the EFP List, which is able to edit List Records.| | `user` | The Ethereum address of the User role associated with the EFP List, which is the Ethereum account that is following the Ethereum accounts in the list.| ### "manager" The `manager` key is used to store the Manager role associated with an EFP List. The manager is represented as a 20-byte address. | Byte(s) | Description | Value | | ------- | ------------------ | ------------------------------------------ | | 0-19 | Address (20 bytes) | 0x00000000000000000000000000000000DeaDBeef | #### Code ```solidity // set the manager for the EFP list efpListMetadata.setValue(tokenId, "manager", abi.encodePacked(manager)); ``` By reading the `manager` key for a given List, a client can determine the Manager associated with that list. ```solidity address manager = abi.decode(efpListMetadata.getValue(tokenId, "manager"), (address)); ``` ### "user" The `user` key is used to store the User role associated with an EFP List. The user is represented as a 20-byte address. | Byte(s) | Description | Value | | ------- | ------------------ | ------------------------------------------ | | 0-19 | Address (20 bytes) | 0x00000000000000000000000000000000DeaDBeef | #### Code with example code shown below: ```solidity // set the user for the EFP List efpListMetadata.setValue(tokenId, "user", abi.encodePacked(user)); ``` By reading the `user` key for a given EFP List, a client can determine the User associated with that list. ```solidity address user = abi.decode(efpListMetadata.getValue(tokenId, "user"), (address)); ``` ## Future This pattern can be extended to support other list-specific metadata such as a name or description. ## Custom Keys The format for custom keys are undefined at this time but will be defined in the future. --- The **EFP List Registry** List Registry is an Ethereum ERC-721 contract `EFPListRegistry` where the NFT represents ownership of an EFP List. Users mint an EFP List NFT to create an **EFP List**. Minting an EFP List NFT is free (plus gas). --- A **List Record** is a fundamental EFP data structure representing a record in an EFP List. Each List Record consists of the following three components: - `version`: A `uint8` representing the version of the List Record. This is used to ensure compatibility and facilitate future upgrades. - `record_type`: A `uint8` indicating the type of record. This serves as an identifier for the kind of data the record holds. - `data`: A `bytes` array containing the actual data of the record. The structure of this data depends on the record type. The version is always `1`. ## Record Types There is only one record type defined at this time: | Type | Description | Data | Data Length | | ------- | ----------- | --------------- | ----------- | | 0 | Reserved | N/A | N/A | | 1 | Address | 20-byte address | 20 | | 2 - 255 | Reserved | N/A | N/A | Record types 0 and 2-255 are reserved for future use. To illustrate the design, however, consider hypothetical list record types: - a subscription to another EFP List, where the `data` field would contain the 32-byte token ID of the corresponding EFP NFT. - an encrypted list record, where the `data` field would contain a list record encrypted with the public key of the list owner/manager/user (for privacy). - an ERC-721 NFT token, where the `data` field would contain the 20-byte address of the ERC-721 contract, and the 32-byte token ID. - an ERC-1155 token, where the `data` field would contain the 20-byte address of the ERC-1155 contract, the 32-byte token ID (exclude token amount). - an ENS name, where the `data` field would contain the normalized string of the ENS name. - a DNS name, where the `data` field would contain the normalized string of the DNS name. - an IP address, where the `data` field would contain the IP address string. - an email address, where the `data` field would contain the email address string. - a torrent magnet link, where the `data` field would contain the magnet link string. - a git repository URL, where the `data` field would contain the git remote URL string. - an RSS feed, where the `data` field would contain the string URL of the RSS feed. - an Atom feed, where the `data` field would contain the string URL of the Atom feed. - a DID record, where the `data` field would contain the DID string. - a custom record, where the `data` field would contain arbitrary or custom data. Clients may support some or all of these record types depending on use case (once more than one record type is defined). ### Address Record (Type 1) The following is an example of an encoded `ListRecord` of type 1 (address record): | Byte(s) | Description | Value | | ------- | --------------------------------- | -------------------------------------------- | | 0 | `ListRecord` version (1 byte) | `0x01` | | 1 | `ListRecord` record_type (1 byte) | `0x01` | | 2 - 21 | `ListRecord` data (20 bytes) | `0x00000000000000000000000000000000DeaDBeef` | ## Validation `version` should always be `1`. `record_type` should always be `1` until other record types are defined. `data` should always be exactly 20 bytes for record type `1`. If any of the above conditions are not met, the `ListRecord` is considered invalid and should not be processed. ## Encoding Onchain, list records are packed into byte arrays with the **version** and **record type** prepended to **data** to form an array with `2 + data.length` bytes. ``` +------------------+---------------------+------------------------+ | version (1 byte) | recordType (1 byte) | data (variable length) | +------------------+---------------------+------------------------+ ``` This byte array will itself be a subarray of the list op data. ## Decoding Managers have permissions to upload arbitrary list record data, so clients should be prepared to handle unexpected data. When decoding a `ListRecord`, the `version` and `recordType` fields should be checked to ensure compatibility. The length of the `data` field should be checked to ensure it is the expected length for the given `recordType`. If the length of the `data` field is unexpected, the `ListRecord` should generally be ignored and not processed. ## Tags A `Tag` is a string associated with a `ListRecord` in a list. A `ListRecord` can have multiple tags associated with it. A `Tag` is represented as a string. ### Normalization Tags are normalized by converting them to lowercase and removing leading and trailing whitespace. Tags should be normalized before they are encoded into a `ListOp`. ## Code List Record can be represented as a type in any programming language. Here are some examples: ### Go ```go type ListRecord struct { Version uint8 RecordType uint8 Data []byte } ``` ### Python ```python class ListRecord: version: int # 0-255 record_type: int # 0-255 data: bytes ``` ### Rust ```rust struct ListRecord { version: u8, record_type: u8, data: Vec, } ``` ### Solidity ```solidity /** * The EFP contracts don't use this struct; they only store list ops * as `bytes` and do not store list records directly. However, this * struct can be useful for offchain processing with foundry or other * Solidity tooling */ struct ListRecord { uint8 version; uint8 recordType; bytes data; } ``` ### TypeScript ```typescript type ListRecord = { version: number // 0-255 recordType: number // 0-255 data: Uint8Array } ``` --- A **List Storage Location** defines where the list records and metadata are stored for an EFP List. The List Storage Location value is stored in the main List Registry contract and can be changed at any time by the List NFT owner. ## Onchain Storage All EFP data is stored on either Ethereum L1 or a supported L2. An EFP List can be uniquely specified via three pieces of data: - `chain_id`: The 32-byte EVM chain ID of the chain where the list is stored. - `contract_address`: The 20-byte EVM address of the contract where the list is stored. - `slot`: A 32-byte value that specifies the storage slot of the list within the contract. This disambiguates multiple lists stored within the same contract and de-couples it from the EFP List NFT token id which is stored on Ethereum and inaccessible on L2s. The following chains are supported: | Chain ID | Name | Layer | Type | 32-Byte Chain ID | | --- | --- | --- | --- | --- | | 1 | Ethereum | L1 | Ethereum | `0x0000000000000000000000000000000000000000000000000000000000000001` | | 8453 | Base | L2 | Ethereum | `0x0000000000000000000000000000000000000000000000000000000000002105` | | 10 | OP Mainnet | L2 | Ethereum | `0x000000000000000000000000000000000000000000000000000000000000000a` | ## Serialization List Storage Locations are encoded in a versioned, flexible data structure. Each List Storage Location is encoded as a `bytes` array with the following structure: - `version`: A `uint8` representing the version of the List Storage Location. This is used to ensure compatibility and facilitate future upgrades. - `location_type`: A `uint8` indicating the type of list storage location. This serves as an identifier for the kind of data the data field contains. - `data`: A `bytes` array containing the actual data of the list storage location. The structure of this data depends on the location type. The version is always `1`. The location type is always `1`. Since only one location type is currently supported, the `data` field is always a `bytes` array of length `32 + 20 + 32 = 84`. ## Location Types There is only one location type which covers both Ethereum and L2s. | Location Type | Description | Data | | ------------- | ------------ | ---------------------------------------------------------- | | 0 | Reserved | N/A | | 1 | EVM contract | 32-byte chain ID + 20-byte contract address + 32-byte slot | | 2 - 255 | Reserved | N/A | ## Code List Storage Location can be represented as a type in any programming language. Here are some examples: #### Go ```go type ListStorageLocation struct { Version uint8 LocationType uint8 Data []byte } ``` #### Python ```python class ListStorageLocation: version: int # 0-255 location_type: int # 0-255 data: bytes ``` #### Rust ```rust struct ListStorageLocation { version: u8, location_type: u8, data: Vec, } ``` #### Solidity ```solidity /** * The EFP contracts don't use this struct; they only store list * storage locations as `bytes` with the version, location type, * and data fields tightly packed into a single `bytes` value * however, this struct can be useful for offchain processing with * foundry or other Solidity tooling */ struct ListStorageLocation { uint8 version; uint8 locationType; bytes data; } ``` #### TypeScript ```typescript type ListStorageLocation = { version: number // 0-255 locationType: number // 0-255 data: Uint8Array } ``` --- **Account** (or **Ethereum account**) > An EVM account. There are no "EFP accounts". **Account Metadata** > EFP provides an Account Metadata contract that allows users to set EFP-related metadata specific to their Ethereum account, such as identifying the Primary List for that Ethereum account. (see [Account Metadata](https://docs.efp.app/design/account-metadata/)) **Address** (or **Ethereum address**) > An EVM address (e.g. 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045). There are no "EFP addresses". **Block** > A standard tag that means neither the user nor the blocked account should see each other’s activity in a relevant app; lists with this tag are not included in Followers count, even if the list tags the account with other tags; if both “block” and “mute” tags are present, “block” takes precedence. (see [Tags](https://docs.efp.app/design/tags/)) **EFP app** > Web app that enables someone to create and manage a List on EFP. (see [EFP website](https://efp.app/)) **Ethereum Follow Protocol** (or **EFP**) > An onchain social graph protocol for Ethereum accounts. The protocol whose docs you are currently reading. (see [EFP website](https://efp.app/)) **Ethereum Name Service** (or **ENS**) > A distributed, open, and extensible naming system based on the Ethereum blockchain. EFP is designed to complement ENS, and the EFP web app makes extensive use of ENS for names, avatar, and other profile data. (see [ENS website](https://ens.domains/)) **Followers** > The total number of Primary Lists that follow a particular Ethereum account, excluding lists that tag them with "block" or "mute". **Following** > The total number of Ethereum accounts followed by a list, excluding accounts tagged with "block" or "mute". **List** > An NFT in the EFP List Registry and its accompanying List Storage Location data. **List Metadata** > Metadata for a list, stored alongside List Records in a list's List Storage Location, that specifies the Ethereum accounts set for the roles Manager and User. **List Number** > The token ID for the NFT that represents a list. **List Ops** (or **List Operation**) > The same as a **List Record**; An operation performed on an EFP List, such as appending a list record (for following a particular Ethereum account, unfollowing, tagging, untagging). **List Records** > The set of list operations stored at your List Storage Location. (see [List Records](https://docs.efp.app/design/list-records/)) **List Registry** > An Ethereum ERC-721 contract where the NFT represents ownership of a particular list. **List Settings** > A term used in the EFP app that refers to a variety of settings for a list, including: the list's List Storage Location; whether the list is set as a Primary List; the Ethereum accounts set for the roles Owner, Manager, and User. **List Storage Location** > Where the records for a list are stored. (see [List Storage Location](https://docs.efp.app/design/list-storage-location/)) **Manager** > The Ethereum account that can edit a list's records. By default when a list is created, all three roles (Owner, Manager, and User) are set as the Ethereum account that created the list. Typically, all three roles remain the same account, but they can be changed to be different. (see [Roles](https://docs.efp.app/design/roles/)) **Mute** > A standard tag that means the user shouldn’t see the muted account’s activity in a relevant app, but the muted account might still be able to see the user’s activity; lists with this tag are not included in Followers count, even if the list tags the account with other tags; if both “block” and “mute” tags are present, “block” takes precedence. (see [Tags](https://docs.efp.app/design/tags/)) **Mutuals** > Ethereum accounts that an Ethereum account follows who follow them back. **Owner** > The Ethereum account that ultimately controls a List. By default when a list is created, all three roles (Owner, Manager, and User) are set as the Ethereum account that created the list. Typically, all three roles remain the same account, but they can be changed to be different. (see [Roles](https://docs.efp.app/design/roles/)) **Primary List** > The one list an Ethereum account has confirmed to represent who they follow. While an Ethereum account may have roles in more than one list, an Ethereum account can only have one Primary List. Only Primary Lists count as Followers. (see [Account Metadata](https://docs.efp.app/design/account-metadata/)) **Social Graph** > The sum total of relationships between Ethereum accounts held in EFP, counting only Primary Lists. Any list that is not set as a Primary List is not included. **Tag** > A Tag is a string associated with a List Record in an EFP list. Tags only count for an account if that account is also followed by the user, otherwise they're not counted. List Records can have zero or more tags. A few tags are standardized with specified semantics. Users may also set custom tags. (see [Tags](https://docs.efp.app/design/tags/)) **Top8** (or, **Top 8**) > The tag "top8" means the account should appear in the user's "Top 8" in UIs that support it. If a user has more than eight followed accounts with the "top8" tag, then only show the eight most recent should be included in a "Top 8" displayed in a UI. (see [Tags](https://docs.efp.app/design/tags/)) **User** > The Ethereum account for whom the list is intended; the Ethereum account that is following the Ethereum accounts in the list. By default when a list is created, all three roles (Owner, Manager, and User) are set as the Ethereum account that created the list. Typically, all three roles remain the same account, but they can be changed to be different. (see [Roles](https://docs.efp.app/design/roles/)) --- A **Tag** is a string associated with a List Record in an EFP list. Tags only count for an account if that account is also followed by the user, otherwise they're not counted. List Records can have zero or more tags. A few tags are standardized with specified semantics. Users may also set custom tags. ## Standard Tags - **no tag** - If a List Record has no tags, it is interpreted as a simple follow without further categorization. - **"block"** - This tag means neither the user nor the blocked account should see each other’s activity in a relevant app. - List Records with this tag are not included in Followers count, even if the List Record has other tags. - If both “block” and “mute” tags are present, “block” takes precedence. - **"mute"** - This tag means the user shouldn't see the muted account’s activity in a relevant app, but the muted account might still be able to see the user’s activity. - List Records with this tag are not included in Followers count, even if the List Record has other tags. - If both “block” and “mute” tags are present, “block” takes precedence. - **"top8"** - This tag means the account should appear in the user's "Top 8" in UIs that support it. - If a user has more than eight followed accounts with the "top8" tag, then only show the eight most recent should be included in a "Top 8" displayed in a UI. ## Custom Tags Users can create additional arbitrary custom tags. Any tag which violates these constraints is ignored. ### Constraints A custom tag can be any UTF-8 string with the following constraints: - maximum length of 255 bytes - no leading or trailing whitespace - more constraints to be added as needed ## Normalization Tags are normalized by converting them to lowercase and removing leading and trailing whitespace. Tags should be normalized before they are encoded into a List Op. ## Encoding Tags are encoded as UTF-8 strings. | Tag | Encoding | | ------- | ---------------- | | `block` | `62 6c 6f 63 6b` | | `mute` | `6d 75 74 65` | ## Restrictions Tags are restricted to alpha-numerics (upper and lowercase A - Z, 0 - 9) and _most_ emojis. Neither the UI nor the API will accept anything other than these characters. --- --- ## Rewards Up to **$10,000 USD** (paid in Ethereum L1 based USDC) for each verified bug in the smart contracts listed below, depending on the severity and impact of the vulnerability, determined at our discretion. ## Qualifying Smart Contracts - [EFPListRegistry.sol](https://github.com/ethereumfollowprotocol/contracts/blob/master/src/EFPListRegistry.sol) - [EFPAccountMetadata.sol](https://github.com/ethereumfollowprotocol/contracts/blob/master/src/EFPAccountMetadata.sol) - [EFPListMinter.sol](https://github.com/ethereumfollowprotocol/contracts/blob/master/src/EFPListMinter.sol) - [EFPListRecords.sol](https://github.com/ethereumfollowprotocol/contracts/blob/master/src/EFPListRecords.sol) Bugs, fixes, or suggested improvements to the EFP app, Indexer, API, and other EFP-related code can be reported as issues on their respective repos and are not covered by this bug bounty program. Suggestions for improvements to the smart contracts above are welcome to be posted as issues on their repos but do not qualify for the bug bounty. ## How to Report a Bug Email us at [bugbounty@ethfollow.xyz](mailto:bugbounty@ethfollow.xyz). Your report should include: - "Bug Report" or similar in the subject line - A detailed description of the vulnerability - Steps to reproduce the issue - Potential impact and severity assessment - Any suggested fixes or mitigations - An ENS name or Ethereum address that can receive Ethereum L1 based USDC (should your bug report be accepted) Provide as much detail as possible to help us understand and resolve the issue efficiently. ## Eligibility and Responsible Disclosure To be eligible for a reward, you must adhere to the following rules: - Only report vulnerabilities related to Ethereum Follow Protocol's smart contracts listed above. - Do not publicly disclose the vulnerability until we have had adequate time to investigate and deploy a fix. - Avoid any actions that would disrupt our services or compromise user data. --- A valid EFP list operation has four primary components that must exist in order for it to be considered `active`. - a valid [list storage location](/design/list-storage-location) (denoting chainId, listRecords contract address, and slot) - an [account metadata record](/design/account-metadata) specifying a tokenId as a user's primary list - a [list metadata record](/design/list-metadata) in the listRecords contract for 'user' role for the slot - a [list record](/design/list-records) with a specified slot that matches the list metadata record and the list storage location. This list record must have been created on the list records contract and chain specified in the list storage location The user and manager roles of a list can be different, this means that the sender of a transaction that applies list operations to a list is not necessarily the same address that is displayed as the user. ![Active List Operation](/active-list-op.png) ### Connecting the dots In order to validate a list operation, the data in each one of these components must match the others where specified. #### If User Address is known: If the only known information is a user address, then the following steps can be take to determine the user's list, list storage location and slot 1. Retrieve the user's primary list by querying the account metadata contract using the user address. 2. From the primary list, obtain the list storage location from the registry contract which includes the chainId, listRecords contract address, and slot using function getListStorageLocation(tokenId). 3. Query the list metadata record in the listRecords contract using the slot to verify the 'user' role. 4. Retrieve the list record from the listRecords contract using the slot and ensure it matches the list metadata record and list storage location in the registry. #### If List Id is known: If the only known information is a list number, then the following steps can be taken to determine the list user 1. Obtain the list storage location from the registry contract which includes the chainId, listRecords contract address, and slot using function getListStorageLocation(tokenId). 2. Query the list metadata record in the listRecords contract using the slot to verify the 'user' role. While at this step we will know the user address, we do not yet know if the user role is valid without checking for the existance of a 'primary-list' record that matches this list id and the user's address. 3. Retrieve the user's primary list by querying the account metadata contract using the user address. 4. Retrieve the list record from the listRecords contract using the slot and ensure it matches the list metadata record and list storage location in the registry. #### If Slot is known: If the only known information is the slot (i.e. you're looking at a raw list operation and want to determine the list to which it applies) 1. Query the list metadata record in the listRecords contract using the slot to find the 'user' role. While at this step we will know the user address, we do not yet know if the user role is valid without checking for the existance of a 'primary-list' record that matches this list id and the user's address. 2. Retrieve the user's primary list by querying the account metadata contract using the user address. ### Interpreting an example List Operation A List Record specifies a slot (which is specific to a list's list storage location) and an operation which describes the action being taken and the address it relates to. For instance the record below is a follow record for address `0x983110309620d911731ac0932219af06091b6744` ```solidity { slot: 38587947120907837207653958898632315929230182373855930657826753963097023554830, op: 0x01010101983110309620d911731ac0932219af06091b6744 } ``` The `op` data of this list record can be further broken down and abstracted into its constituant parts ```solidity { slot: 38587947120907837207653958898632315929230182373855930657826753963097023554830, op: { OpVersion: 0x01, OpCode: 0x01, // 0x01: Follow, 0x02: Unfollow, 0x03: Tag, 0x04: Untag RecordVersion: 0x01, RecordType: 0x01, // 0x01: Address Record RecordData: 0x983110309620d911731ac0932219af06091b6744 } } ``` This record only tells us the `slot` of the list that's doing the following but it tells us nothing about which account or list is doing the following. To determine the list doing the following we need to join the slot, chain id and the address of the list records contract and construct a list storage location. ### Interpreting an example List Storage Location ```solidity 0x010100000000000000000000000000000000000000000000000000000000000000015289fe5dabc021d02fddf23d4a4df96f4e0f17ef5550010c08608cc567bf432829280f99b40f7717290d6313134992e4971fa50e ``` This list storage location can be interpreted as follows ```solidity { Version: 0x01, Type: 0x01, Chain: 0x0000000000000000000000000000000000000000000000000000000000000001, ListRecordsContract: 0x5289fe5dabc021d02fddf23d4a4df96f4e0f17ef, Slot: 0x5550010c08608cc567bf432829280f99b40f7717290d6313134992e4971fa50e // 38587947120907837207653958898632315929230182373855930657826753963097023554830 } ``` ## Constructing State History A repository demonstrating practical application of this page that can be found here: [https://github.com/ethereumfollowprotocol/onchain](https://github.com/ethereumfollowprotocol/onchain) ### Interacting with Contracts directly Calling the Account Metadata contract to fetch a user's primary list: ```ts import { evmClients } from '#/clients/viem/index' import { efpAccountMetadataAbi } from '#/abi/generated/index' import { env } from '#/env.ts' const tokenId = await evmClients['8453']().readContract({ address: env.ACCOUNT_METADATA, abi: efpAccountMetadataAbi, functionName: 'getValue', args: [ env.USER_ADDRESS as `0x${string}`, 'primary-list' ] }) ``` Calling the List Registry contract to fetch a list's list storage location: ```ts import { evmClients } from '#/clients/viem/index' import { efpListRegistryAbi } from '#/abi/generated/index' import { env } from '#/env.ts' const listStorageLocation = await evmClients['8453']().readContract({ address: env.REGISTRY, abi: efpListRegistryAbi, functionName: 'getListStorageLocation', args: [ BigInt(tokenId) ] }) ``` Calling the List Records contract to fetch a list slot's 'user' role: ```ts import { evmClients } from '#/clients/viem/index' import { efpListRecordsAbi } from '#/abi/generated/index' import { env } from '#/env.ts' const listUser = await evmClients[parsedLsl.chainId.toString() as keyof typeof evmClients]().readContract({ address: parsedLsl.listRecordsContract as `0x${string}`, abi: efpListRecordsAbi, functionName: 'getListUser', args: [ parsedLsl.slot ] }) ``` ### Relevant Contract Events Basic EFP state for any user's followers and followings can also be constructed by 'listening' for specific events emitted by the EFP contracts and then interpreting them according to the protocol rules as stated above EFPListRecords (Base, Optimism and Ethereum Mainnet) `ListOp(uint256 indexed slot, bytes op)` `UpdateListMetadata(uint256 indexed slot, string key, bytes value)` EFPAccountMetadata (Base) `UpdateAccountMetadata(address indexed addr, string key, bytes value)` EFPListRegistry (Base) `UpdateListStorageLocation(uint256 indexed tokenId, bytes listStorageLocation)` --- --- ## Railway Template Deploying the EFP-Silo template on Railway will set up all of the aforementioned components with default parameters set. [EFP-Silo](https://railway.app/template/pDGEZm?referralCode=AavWEU) Click the link or the button below. [![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/pDGEZm?referralCode=AavWEU) #### Configuration The template will run with default values for most of the components but you will need to set primary and secondary rpc urls for all three chains that EFP is using (Base, Optimism and Ethereum Mainnet). Find the section for 'Indexer-8453', click the 'configure' button and set the rpc endpoint for all six fields accordingly. These can be Alchemy or Infura urls or local ethereum nodes if you're testing at home. ![Indexer Params](/set-rpc.png) Once all sections display 'Ready to be deployed' the 'Deploy' button at the bottom will be unlocked and you can click it to proceed. ![Deployed Environment](/deployed-environment.png) ### Syncing It should take under an hour to sync all EFP data, you can still call the api but the data will not be up to date. ### Setup API URL Enable public networking for the API by clicking on the API section, then the 'Settings' tab and scrolling down to 'Networking'. Click 'Generate Domain' to have Railway create a random public link for you. Or click 'Custom Domain' to use an already existing domain name. ![Setup API URL](/setup-api-url.png) Once your link is generated, you can use it to call your API the same way you would call the official EFP API. Just swap in your new link in the place of 'api.ethfollow.xyz' `api.ethfollow.xyz/api/v1/stats` -> `my-generated-api-link.railway.app/api/v1/stats` ![Generate Link](/api-link-generated.png) ![Call Endpoint](/api-link-call.png) ### Setup ENS Worker The API uses V3X Labs' [enstate](https://github.com/v3xlabs/enstate) to provide ENS data. This service is available at https://ens.ethfollow.xyz. EFP is happy to provide use of this endpoint to the community but please do not abuse it. Enable public networking for the ENS Worker by clicking on the 'enstate' section, then the 'Settings' tab and scrolling down to 'Networking'. Click 'Generate Domain' to have Railway create a random public link for you. Or click 'Custom Domain' to use an already existing domain name. Once public networking is enabled for the API and ENS Worker, they should be redeployed as well as the service manager. Do this by clicking into each of the services, and clicking the three dot menu on the right hand side of your currently deployed instance. Click 'Redeploy' on the menu. ![Three dot menu](/redeploy.png) ### Service Manager The services module handles several jobs that run on intervals: - Building the list of all addresses in the EFP database - Building the Leaderboard Ranking - Tracking Mutual follows - Shuffling the recommended accounts list - Refreshing ENS metadata - Building a list of accounts with recent activity The intervals for running these jobs can be adjusted to suit your needs. Additionally the accounts that appear in the recommended follows can be customized by forking the services repo and editing '/src/services/recommended/lists.ts'. Once you've edited the file, go to the 'Settings' tab on the services module and update the 'Source Repo' to point to your forked github repository. ![Services Settings](/services-settings-repo.png) ### ENV variables Most of the Environment variables are set appropriately and should not be changed unless you know exactly what you're doing. However there are a few settings than can be customized to fit your needs. As a general rule, if they're not listed below then you probably should leave them as is. #### Indexers (Indexer-base, Indexer-op, Indexer-eth) | Variable Name | Description | |---------------------------------|-------------------------------------------------------------------------| | `PRIMARY_RPC_BASE` | Primary RPC URL for Base chain | | `PRIMARY_RPC_OP` | Primary RPC URL for Optimism chain | | `PRIMARY_RPC_ETH` | Primary RPC URL for Ethereum mainnet | | `SECONDARY_RPC_BASE` | Fallback RPC URL for Base chain | | `SECONDARY_RPC_OP` | Fallback RPC URL for Optimism chain | | `SECONDARY_RPC_ETH` | Fallback RPC URL for Ethereum mainnet | | `RECOVER_HISTORY` | Boolean Flag to start the indexer in recovery mode | | `START_BLOCK` | Block number to start recovering history from, if in recovery mode | | `BATCH_SIZE` | Number of events to batch before uploading | | `CHAIN_ID` | Chain id (8453, 10 or 1) | | `RECORDS_ONLY` | Boolean flag that specifies whether the indexer should listen for just ListOps | #### Service Manager | Variable Name | Description | |---------------------------------|----------------------------------------------------------| | `ENS_API_URL` | URL for the ENS worker to use for lookups | | `EFP_CACHE_INTERVAL` | Interval (in milliseconds) to update address cache | | `EFP_MUTUALS_INTERVAL` | Interval (in milliseconds) to update mutual follows | | `LEADERBOARD_RANKING_INTERVAL` | Interval (in milliseconds) to update leaderboard | | `RECENT_FOLLOWS_INTERVAL` | Interval (in milliseconds) to update recent activity | | `RECOMMENDED_INTERVAL` | Interval (in milliseconds) to update recommended accts | | `ENSMETADATA_INTERVAL` | Interval (in milliseconds) to update ENS metadata cache | | `HEARTBEAT_INTERVAL` | Interval (in milliseconds) to call heartbeat URL | #### API | Variable Name | Description | |---------------------------------|---------------------------------------------------------| | `CACHE_TTL` | Amount of seconds to wait before expiring cache record | | `POAP_API_TOKEN` | Auth token for POAP.xyz api | | `ENS_API_URL` | URL for the ENS worker to use for lookups | #### PgBouncer (see [PgBouncer Docs](https://www.pgbouncer.org/config.html) for more information) | Variable Name | Description | |-----------------------------------|-----------------------------------------------------| | `PGBOUNCER_DEFAULT_POOL_SIZE` | Amount of connections per pool | | `PGBOUNCER_MAX_CLIENT_CONN` | Maximum number of client connections | | `PGBOUNCER_POOL_MODE` | 'Session' or 'Transaction' | | `PGBOUNCER_QUERY_TIMEOUT` | Amount of seconds to wait before dropping query | --- # EFP Emergency Response So, you've found a critical bug and something is seriously wrong with the data coming from your latest greatest contract. In most cases, the `pause` function can be called on an affected contract from the [EFP multisig](https://docs.efp.app/production/multisig/) and the contract can simply be redeployed with a fix. Then the new contract and data can be used in the indexer in place of the bad one. However in some cases, there may be correct data from a bad contract that needs to be preserved. Its important to understand how the entire EFP system works to be able to judge whether data from a questionable contract needs to be saved. ## Affected Contracts Determine which contracts are affected. Having to pause one of the contracts is not the end of the world, and properly identifying the extent of the bug will help prevent introducing second order issues in contract interactions. For instance, if a bug is found in a second or third iteration of the list records contract this would only require pausing that specific contract and not all previous deployed versions or unaffected contracts like the list registry or list minter. This would limit the affected users to only those who have specified the affected list records contract as their list storage location. #### List Minter Contract BASE: [0xDb17Bfc64aBf7B7F080a49f0Bbbf799dDbb48Ce5](https://basescan.org/address/0xDb17Bfc64aBf7B7F080a49f0Bbbf799dDbb48Ce5) If the bug is in a List Minter contract: Call the `pause` function in the List Minter contract and record the tx hash and block height at the time of the tx. - If there is bad list data in any of the lists minted from the affected list minter contract, users will still be able to reset their lists. - If lists were minted to an incorrect recipient, the users can mint new lists from the new contract. - The old list minter contract should be marked as 'deprecated' and removed from any relevant list records indexers in all regions. If a new list minter is deployed, the account metadata contract proxy records must also be updated. In the AccountMetadata contract, `removeProxy()` should be called with the old list minter's contract address, and `addProxy()` should be called with the new list minter's contract address. It should also be noted that if the list minter contract is paused, users will still be able to mint new lists by interacting directly with the registry, list records and account metadata contracts. If minting needs to be completely disabled, the registry contract must also be paused. #### List Records Contract(s) BASE: [0x41Aa48Ef3c0446b46a5b1cc6337FF3d3716E2A33](https://basescan.org/address/0x41Aa48Ef3c0446b46a5b1cc6337FF3d3716E2A33) OP: [0x4Ca00413d850DcFa3516E14d21DAE2772F2aCb85](https://optimistic.etherscan.io/address/0x4Ca00413d850DcFa3516E14d21DAE2772F2aCb85) MAINNET: [0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF](https://etherscan.io/address/0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF) If the bug is in a List Records contract: Determine how many of the list records contracts are affected. Call the `pause` function in the List Records contract and record the tx hash and block height at the time of the tx. - If there is bad list operation data in any of the list records from the affected list record contract, users may have to re-execute any list operations that were called against the affected list operations contract - If the list records contract is deprecated, users may have to migrate their existing list data from the old list records contract. - The old list records contract should be marked as 'deprecated' and removed from any relevant list records indexers in all regions. #### Account Metadata Contract BASE: [0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF](https://basescan.org/address/0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF) If the bug is in the Account Metadata contract: Call the `pause` function in the Account Metadata contract and record the tx hash and block height at the time of the tx. - If there is bad account metadata in any of the records from the affected accouint metadata contract, users may have to re-execute any transactions that were called against the affected contract - If the list records contract is deprecated, users may have to migrate their existing list data from the old list records contract. #### List Registry Contract BASE: [0x0E688f5DCa4a0a4729946ACbC44C792341714e08](https://basescan.org/address/0x0E688f5DCa4a0a4729946ACbC44C792341714e08) If the bug is in the List Registry contract: Call the `pause` function in the Account Metadata contract and record the tx hash and block height at the time of the tx. - If there is a bug in the list registry contract, it is likely that the entire set of lists and user data will have to be migrated to a new contract - Once the list registry contract is paused, users will no longer be able to mint new lists. However they will still be able to interact with existing lists (update roles, add/remove list operations) ### Worst Case Scenario If the nature of a bug is undetermined and all other avenues are exhausted, there remains a final option of pausing ALL contracts and preventing interaction with any of the EFP contracts. This is a last ditch effort that should only be pursued if there are no other options, as pausing all contracts will prevent users from interacting with any aspect of the EFP system Pausing and unpausing all contracts will not compromise data, as long as all contracts are unpaused. It is recommended that the contracts be unpaused in the following order, with minimal time elapsed between the execution of each respective `unpause` contract call: 1. Any List Records contracts 2. Account Metadata 3. List Registry 4. List Minter ## Response All `pause` and `unpause` function calls must be executed with EFP multisig via [Safe Wallet](https://app.safe.global). The function selectors for both `pause` and `unpause` functions are the same across all contracts, respectively. ### Pause Function Function selector `0x8456cb59` The `pause` transaction can be found in the EFP Safe Wallet under 'Saved Transactions' in the transaction builder ### Unpause Function Function selector `0x3f4ba83a` The `unpause` transaction can be found in the EFP Safe Wallet under 'Saved Transactions' in the transaction builder ## Further Considerations - EFP Indexers recognize `Pause` and `Unpause` events and consume records appropriately based on the emission of these events. However, if a list records contract is found to have a bug, there may be intact data that needs to be recorded and preserved prior to the emission of a `Pause` event, or after an `Unpause` event. Indexers may need to be updated to disregard events after a certain block height in order to exclude bad data that occurred prior to a `Pause` event from being consumed and processed. - Ethereum Identity Kit should have a lockdown mode where all contract addresses are zeroed out to prevent transaction prompting, or users receive a 'Maintenance' message. - efp.app should similarly have a 'Maintenance' mode where a banner is displayed notifying users that the api is available but minting new lists and creating list operations are temporarily disabled. - As the usage of EFP evolves, it is expected that users will interact with efp.app less and interact with the EFP system through third party applications and services. This means that over time more users will interact with EFP via the use of Ethereum Identity Kit components. Care should be taken to ensure that EIK supports various failure modes, and all component functionality can be enabled or disabled as necessary. --- ![EFP Backend Design](/backend-design.png) The backend architecture for EFP is comprised of the following components - [Database](https://hub.docker.com/_/postgres) (Postgres) - [PGBouncer](https://hub.docker.com/r/pgbouncer/pgbouncer) (a connection pooler for Postgres) - [EFP Indexers](https://github.com/ethereumfollowprotocol/indexer) for Base, Optimism and Ethereum Mainnet - [EFP Services](https://github.com/ethereumfollowprotocol/services) (updates ens data, leaderboard, cache, mutuals counts) - [EFP API](https://github.com/ethereumfollowprotocol/api) (Can be deployed as a cloudflare worker) - [Ens Worker](https://github.com/v3xlabs/enstate) (Can be deployed as a cloudflare worker) - [Redis](https://hub.docker.com/_/redis) Cache for the API and ENS Worker All of these components can be set up separately, whether locally or using external hosts. The quickest way to stand up all of the backend infrastructure is to use the [EFP-Silo Railway template](https://railway.app/template/pDGEZm?referralCode=AavWEU) ### Indexers EFP has Registry and AccountMetadata contracts deployed on Base and ListRecords contracts deployed on three chains: Base, Optimism And Ethereum Mainnet. To capture and store all EFP data we run three separate instances of our indexer service, one for each chain. ### Database EFP Data is stored in a Postgres Database, queries and schema can be found in the [EFP Indexer Repo](https://github.com/ethereumfollowprotocol/indexer/tree/develop/db) ### Connection Pooling PgBouncer is used to handle connection pooling for the database. This improves availability for the database over the api by sharing client connections, saving overhead on connection/reconnection. ### Data Services The services module handles several jobs that run on intervals: - Building the list of all addresses in the EFP database - Building the Leaderboard Ranking - Tracking Mutual follows - Shuffling the recommended accounts list - Refreshing ENS metadata - Building a list of accounts with recent activity The intervals for running these jobs can be adjusted to suit your needs. Additionally the accounts that appear in the recommended follows can be customized by forking the services repo and editing '/src/services/recommended/lists.ts'. Once you've edited the file, go to the 'Settings' tab on the services module and update the 'Source Repo' to point to your forked github repository. ![Services Settings](/services-settings-repo.png) ### API EFP core team provides a public API endpoint at [`https://api.ethfollow.xyz/api/v1`](https://api.ethfollow.xyz/api/v1). If you are a developer, you are free to use this endpoint to retrieve EFP data. #### Commonly used endpoints - [User Stats](https://docs.ethfollow.xyz/api/users/stats/): the follower and following counts of a particular user - [User Following](https://docs.ethfollow.xyz/api/users/following/): list of the accounts a particular user follows - [User Followers](https://docs.ethfollow.xyz/api/users/followers/): list of the accounts that follow a particular user - [User ENS data](https://docs.ethfollow.xyz/api/users/ens/): the ENS data for a particular user ### ENS Worker The API uses V3X Labs' [enstate](https://github.com/v3xlabs/enstate) to provide ENS data. This service is available at https://ens.ethfollow.xyz. EFP is happy to provide use of this endpoint to the community but please do not abuse it. Anyone can deploy this ENS worker repository to cloudflare or host their own instance locally. Read more about deploying [here](https://github.com/v3xlabs/enstate?tab=readme-ov-file#-cloudflare-workers). ### Redis Cache The Redis cache is used by both the API and the ENS Worker to cache responses for faster future retrieval. --- The EFP multisig is a set of Gnosis Safes on Base, OP Mainnet, and Ethereum Mainnet, and each requires 3 out of 4 signatories to execute a transaction. ### Multisig Wallet Addresses | Chain | Address | Gnosis Safe | | --- | --- | --- | | Base | 0x860bFe7019d6264A991277937ea6002714C3c508 | [Link](https://app.safe.global/home?safe=base:0x860bFe7019d6264A991277937ea6002714C3c508) | | OP Mainnet | 0x860bFe7019d6264A991277937ea6002714C3c508 | [Link](https://app.safe.global/home?safe=oeth:0x860bFe7019d6264A991277937ea6002714C3c508) | | Ethereum Mainnet | 0xeaa7B3B7f9A6c9782Fc17A49C4dfC170193d69Dd | [Link](https://app.safe.global/home?safe=eth:0xeaa7B3B7f9A6c9782Fc17A49C4dfC170193d69Dd) | ### Signers The signers are the same on all three chains and are controlled by members of the [EFP core team](https://efp.app/team). | Address | Owner | | ------------------------------------------ | ------------------ | | 0x983110309620D911731Ac0932219af06091b6744 | brantly.eth | | 0x96184444629F3489c4dE199871E6F99568229d8f | brantly.eth | | 0xC9C3A4337a1bba75D0860A1A81f7B990dc607334 | 0xthrpw.eth | | 0x5B0f3DBdD49614476e4f5fF5Db6fe13d41fCB516 | encrypteddegen.eth | ### EFP Contracts owned by Multisig ##### Base (Chain ID: 8453) | Name | Address | | ------------------ | ------------------------------------------ | | EFPAccountMetadata | 0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF | | EFPListRecords | 0x41Aa48Ef3c0446b46a5b1cc6337FF3d3716E2A33 | | EFPListRegistry | 0x0E688f5DCa4a0a4729946ACbC44C792341714e08 | | EFPListMinter | 0xDb17Bfc64aBf7B7F080a49f0Bbbf799dDbb48Ce5 | | TokenURIProvider | 0xC8BA343aeaF2b3b3EC79C25f05F4Ef459D9c7eFB | ##### OP Mainnet (Chain ID: 10) | Name | Address | | -------------- | ------------------------------------------ | | EFPListRecords | 0x4Ca00413d850DcFa3516E14d21DAE2772F2aCb85 | ##### Ethereum Mainnet (Chain ID: 1) | Name | Address | | -------------- | ------------------------------------------ | | EFPListRecords | 0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF | ### Privileges For each of the chains, the multisig has privileged control over the following functions in each of the contracts listed below. ### EFPAccountMetadata --- ##### Pause Allows the owner to cease all contract interaction ``` /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } ``` ##### Unpause Allows the owner to resume all contract interaction ``` /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ``` ##### Add Proxy Allows the owner to add an account to a list of privileged addresses which are allowed to update account metadata. This allows the List Minter to set default lists and metadata during the minting process. ``` /** * @dev Add proxy address. * @param proxy The proxy address. */ function addProxy(address proxy) external whenNotPaused onlyOwner { proxies[proxy] = true; emit ProxyAdded(proxy); } ``` ##### Remove Proxy Allows the owner to remove an account from the list of privileged proxy addresses ``` /** * @dev Remove proxy address. * @param proxy The proxy address. */ function removeProxy(address proxy) external whenNotPaused onlyOwner { proxies[proxy] = false; emit ProxyRemoved(proxy); } ``` ### EFPListRecords --- ##### Pause Allows the owner to cease all contract interaction ``` /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } ``` ##### Unpause Allows the owner to resume all contract interaction ``` /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ``` ### EFPListRegistry --- ##### Pause Allows the owner to cease all contract interaction ``` /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } ``` ##### Unpause Allows the owner to resume all contract interaction ``` /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ``` ##### Set TokenURIProvider Allows the owner to set the TokenURIProvider contract address ``` /** * @notice Sets the token URI provider. * @param tokenURIProvider_ The new token URI provider. */ function setTokenURIProvider(address tokenURIProvider_) external onlyOwner { tokenURIProvider = ITokenURIProvider(tokenURIProvider_); emit TokenURIProviderChange(tokenURIProvider_); } ``` ##### Set PriceOracle Allows the owner to set the `priceOracle` contract address. This contract is currentlet unset. ``` /** * @notice Sets the price oracle. * @param priceOracle_ The new price oracle. */ function setPriceOracle(address priceOracle_) external whenNotPaused onlyOwner { priceOracle = IEFPListNFTPriceOracle(priceOracle_); emit PriceOracleChange(priceOracle_); } ``` ##### Withdraw Allows the owner to withdraw Ether from the contract ``` /** * @notice Withdraws Ether from the contract. * * @param recipient The address to send the Ether to. * @param amount The amount of Ether to send. * @return Whether the transfer succeeded. */ function withdraw(address payable recipient, uint256 amount) public onlyOwner returns (bool) { require(amount <= address(this).balance, 'Insufficient balance'); (bool sent,) = recipient.call{value: amount}(''); require(sent, 'Failed to send Ether'); return sent; } ``` ##### Set MintState Allows the owner to modify limitations when minting lists ``` /// @notice Sets the mint state. /// @param _mintState The new mint state. function setMintState(MintState _mintState) external whenNotPaused onlyOwner { mintState = _mintState; emit MintStateChange(_mintState); } ``` ##### Set MaxMintBatchSize Allows the owner to modify limitations on the maximum number of lists that can be minted in one transanction ``` /// @notice Sets the max mint batch size. /// @param _maxMintBatchSize The new max mint batch size. function setMaxMintBatchSize(uint256 _maxMintBatchSize) external whenNotPaused onlyOwner { maxMintBatchSize = _maxMintBatchSize; emit MaxMintBatchSizeChange(_maxMintBatchSize); } ``` ### EFPListMinter --- ##### Pause Allows the owner to cease all contract interaction ``` /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } ``` ##### Unpause Allows the owner to resume all contract interaction ``` /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ``` ### TokenURIProvider --- ##### Set BaseURI Allows the owner to set the BaseURI use for contructing token URIs ``` /** * @dev Sets the base URI for token URIs * @param baseURI The new base URI */ function setBaseURI(string memory baseURI) external onlyOwner { _baseURI = baseURI; } ``` --- ![Follow Bot Start](/follow-bot-start.png) The **EFP Follow Bot** is a telegram bot that enables users to subscribe to Ethereum addresses or ENS names, allowing them to stay updated with the latest follows, tags, and other activities within the Ethereum Follow Protocol (EFP) ecosystem. This bot simplifies tracking and engagement, making it easier for users to stay informed about their favorite EFP accounts. Get started by interacting with the bot directly at [https://t.me/efp_follow_bot](https://t.me/efp_follow_bot) ## Commands Here are the available commands to interact with the **EFP Follow Bot**: ### subscribe ![Follow Bot Subscribe](/follow-bot-subscribe.png) `/subscribe ` Subscribe to updates for a specific Ethereum address or ENS name. Use this command to start receiving notifications about activities related to the specified account. Multiple accounts can be subscribed to at one time `/sub ` Alias for /subscribe. Functions the same way as /subscribe. ### unsubscribe ![Follow Bot Unsubscribe](/follow-bot-unsubscribe.png) `/unsubscribe ` Unsubscribe from updates for a specific Ethereum address or ENS name. Use this command to stop receiving notifications for a specific account. `/unsub ` Alias for /unsubscribe. Functions the same way as /unsubscribe. `/unsub all` Unsubscribe from all accounts. This command will stop all notifications for the current chat. ### details `/details ` Get details for a specific Ethereum address or ENS name ### list ![Follow Bot List](/follow-bot-list.png) `/list` List all subscriptions for this chat. Use this command to view all the Ethereum addresses or ENS names you are currently subscribed to. ### help `/help` Show this help message. Use this command to display a list of all available commands and their descriptions. --- ### EFP Contracts EFP currently has smart contracts on three chains: Base, OP Mainnet, and Ethereum. --- #### Base (Chain ID: 8453) | Name | Address | | -------------------- | ------------------------------------------ | | EFPAccountMetadata | 0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF | | EFPListRecords | 0x41Aa48Ef3c0446b46a5b1cc6337FF3d3716E2A33 | | EFPListRegistry | 0x0E688f5DCa4a0a4729946ACbC44C792341714e08 | | EFPListMinter | 0xDb17Bfc64aBf7B7F080a49f0Bbbf799dDbb48Ce5 | | TokenURIProvider | 0xC8BA343aeaF2b3b3EC79C25f05F4Ef459D9c7eFB | #### OP Mainnet (Chain ID: 10) | Name | Address | | -------------------- | ------------------------------------------ | | EFPListRecords | 0x4Ca00413d850DcFa3516E14d21DAE2772F2aCb85 | #### Ethereum Mainnet (Chain ID: 1) | Name | Address | | -------------------- | ------------------------------------------ | | EFPListRecords | 0x5289fE5daBC021D02FDDf23d4a4DF96F4E0F17EF | ### Testnet Deployments #### Base Sepolia (Chain ID: 84532) | Name | Address | | -------------------- | ------------------------------------------ | | EFPAccountMetadata | 0xDAf8088C4DCC8113F49192336cd594300464af8D | | EFPListRecords | 0x63B4e2Bb1E9b9D02AEF3Dc473c5B4b590219FA5e | | EFPListRegistry | 0xDdD39d838909bdFF7b067a5A42DC92Ad4823a26d | | EFPListMinter | 0x0c3301561B8e132fe18d97E69d95F5f1F2849f9b | | TokenURIProvider | 0x3c5A8bB78Ad55C8c0a8Acd0D5afeDFB95470a591 | #### OP Sepolia (Chain ID: 11155420) | Name | Address | | -------------------- | ------------------------------------------ | | EFPListRecords | 0x2f644bfec9C8E9ad822744E17d9Bf27A42e039fE | #### Ethereum Sepolia (Chain ID: 11155111) | Name | Address | | -------------------- | ------------------------------------------ | | EFPListRecords | 0xf8c6aa2a83799d0f984CA501F85f9e634F97FEf2 | --- EFP provides an open source indexer and API for indexing and retrieving EFP data. ### Public API EFP core team provides a public API endpoint at [`https://api.efp.app/api/v1`](https://api.efp.app/api/v1). If you are a developer, you are free to use this endpoint to retrieve EFP data. ### Commonly used endpoints - [User Stats](https://docs.efp.app/api/users/stats/): the follower and following counts of a particular user - [User Following](https://docs.efp.app/api/users/following/): list of the accounts a particular user follows - [User Followers](https://docs.efp.app/api/users/followers/): list of the accounts that follow a particular user - [User ENS data](https://docs.efp.app/api/users/ens/): the ENS data for a particular user ### Self-hosting You may also fork EFP's Indexer/API source code and deploy yourself. - [`ethereumfollowprotocol/api`](https://github.com/ethereumfollowprotocol/api) - [`ethereumfollowprotocol/indexer`](https://github.com/ethereumfollowprotocol/indexer) ### KV cache The API uses Cloudflare KV storage to cache some endpoint's responses. When one of these endpoints is called the cache is checked and if there is no record the data is fetched from the database. If a cached record is found it is returned immediately. All cache records are set to expire 5 minutes after they are created. The cached record for each of these endpoints can be refreshed by adding `cache=fresh` to the query params. For example: Get the cached record ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/1/details ``` Get latest data and reset the cached record ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/1/details?cache=fresh ``` ### Questions If you come across any issues, please reach out to us in our [Discord](https://discord.com/invite/ZUyG3mSXFD). --- #### /stats Get global EFP statistics. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/stats ``` ```jsonc // sample response { "stats": { "address_count": "8999", "list_count": "733", "list_op_count": "50284", }, } ``` --- #### /token/metadata/\:token_id Get NFT metadata for a specified token id #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/token/metadata/4 ``` ```jsonc // sample response { "name": "EFP List #4", "description": "Ethereum Follow Protocol (EFP) is an onchain social graph protocol for Ethereum accounts.", "image": "https://api.ethfollow.xyz/api/v1/token/image/3", "external_url": "https://efp.app/3", "attributes": [ { "trait_type": "User", "value": "brantly.eth", }, { "trait_type": "Primary List", "value": true, }, { "trait_type": "Followers", "value": 368, }, { "trait_type": "Following", "value": 1079, }, { "trait_type": "Mutuals Rank", "value": "1", }, { "trait_type": "Followers Rank", "value": "1", }, { "trait_type": "Following Rank", "value": "6", }, { "trait_type": "Blocked Rank", "value": "7", }, ], } ``` --- #### /token/image/\:token_id Get NFT image for a specified token id #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/token/image/3 ``` #### Response This endpoint responds with a formatted svg of content-type `image/svg+xml;charset=utf-8` --- #### /leaderboard/blocked Get leaderboard of users ranked according to count of users that blocked them. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/blocked ``` ```jsonc // sample response [ { "rank": 1, "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "blocked_by_count": "8" }, { "rank": 2, "address": "0xa7860e99e3ce0752d1ac53b974e309fff80277c6", "blocked_by_count": "4" }, { "rank": 3, "address": "0x3276e82ebb1b4b9f01ab9286ed6bcc6603e368e2", "blocked_by_count": "2" }, { "rank": 4, "address": "0x7265a60acaeaf3a5e18e10bc1128e72f27b2e176", "blocked_by_count": "2" }, ... ] ``` --- #### /leaderboard/all Get addresses and ens names of all leaderboard records. #### Sample Query ```sh curl https://api.ethfollow.xyz/api/v1/leaderboard/all ``` ```jsonc // sample response { "results": [ { "address": "0x0ad4bb5ceabfdb5020b01e6dc5e32526eb10e5d1", "name": "0xsailormoon.eth" }, { "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "name": "designer.eth" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "name": "garypalmerjr.eth" }, { "address": "0x14546125429faac7f3aa78da1807069692ec7464", "name": "grado.eth" }, ... ] } ``` --- #### /leaderboard/ranked Get leaderboard of users ranked according to count of mutual follows. Includes rankings for mutuals, followers, following, blocked and tagged as 'top8'. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 50. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `sort` (string, optional): Specifies ranking to sort on, possible values are 'mutuals', 'followers', 'following', 'blocked' and 'top8', default value is 'mutuals'. - `direction` (string, optional): Specifies direction to sort results, possible values are 'ASC' or 'DESC' default value is 'DESC'. - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/ranked?sort=mutuals&direction=desc ``` ```jsonc // sample response { "last_updated": "2024-09-18T19:11:43.210Z", "results": [ { "address": "0x983110309620d911731ac0932219af06091b6744", "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", "header": "https://i.imgur.com/Quo06x2.png", "mutuals_rank": "1", "followers_rank": "1", "following_rank": "6", "blocks_rank": "7", "top8_rank": "1", "mutuals": "293", "following": "1079", "followers": "366", "blocks": "1", "top8": "45", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "name": "designer.eth", "avatar": "https://euc.li/designer.eth", "header": null, "mutuals_rank": "2", "followers_rank": "6", "following_rank": "4", "blocks_rank": "7", "top8_rank": "7", "mutuals": "147", "following": "1556", "followers": "182", "blocks": "1", "top8": "7", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0x2a59071ff48936c6838dcac425fa0df6ea5979bf", "name": "mely.eth", "avatar": "https://euc.li/mely.eth", "header": "https://i.imgur.com/T2H8N2H.jpeg", "mutuals_rank": "3", "followers_rank": "12", "following_rank": "28", "blocks_rank": "7", "top8_rank": "5", "mutuals": "124", "following": "354", "followers": "138", "blocks": "1", "top8": "11", "updated_at": "2024-09-18T19:11:43.210Z" }, ... ] } ``` --- #### /leaderboard/search Search for leaderboard addresses and ENS names by a specified search term. #### Query Parameters - `term` (string, optional): Specifies the string to search for in a leaderboard address or ENS name. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/search?term=eth ``` ```jsonc // sample response { "last_updated": "2024-09-18T19:11:43.210Z", "results": [ { "address": "0x983110309620d911731ac0932219af06091b6744", "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", "header": "https://i.imgur.com/Quo06x2.png", "mutuals_rank": "1", "followers_rank": "1", "following_rank": "6", "blocks_rank": "7", "mutuals": "293", "following": "1079", "followers": "366", "blocks": "1", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "name": "designer.eth", "avatar": "https://euc.li/designer.eth", "header": null, "mutuals_rank": "2", "followers_rank": "6", "following_rank": "4", "blocks_rank": "7", "mutuals": "147", "following": "1556", "followers": "182", "blocks": "1", "updated_at": "2024-09-18T19:11:43.210Z" }, { "address": "0x2a59071ff48936c6838dcac425fa0df6ea5979bf", "name": "mely.eth", "avatar": "https://euc.li/mely.eth", "header": "https://i.imgur.com/T2H8N2H.jpeg", "mutuals_rank": "3", "followers_rank": "12", "following_rank": "28", "blocks_rank": "7", "mutuals": "124", "following": "354", "followers": "138", "blocks": "1", "updated_at": "2024-09-18T19:11:43.210Z" }, ... ] } ``` --- #### /leaderboard/muted Get leaderboard of users ranked according to count of users that muted them. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/muted ``` ```jsonc // sample response [ { "rank": 1, "address": "0x949e2988b857af2a3c9429e763d13202b7b25c88", "muted_by_count": "1", }, ] ``` --- #### /leaderboard/count Get count of all accounts in the leaderboard. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/count ``` ```jsonc // sample response { "leaderboardCount": "8788", } ``` --- #### /leaderboard/mutes Get leaderboard of users ranked according to count of users that they muted. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/mutes ``` ```jsonc // sample response [ { "rank": 2, "address": "0xfa35730094b7e0fc1b97f55663aad15e2d2e3e29", "mutes_count": "3", }, { "rank": 3, "address": "0x983110309620d911731ac0932219af06091b6744", "mutes_count": "2", }, { "rank": 4, "address": "0x2933387ec4c9bbc4a8200cfd77db53d7bc8ebc11", "mutes_count": "1", }, ] ``` --- #### /leaderboard/following Get leaderboard of users ranked according to following counts. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/following ``` ```jsonc // sample response [ { "rank": 1, "address": "0x44a3a18df15ae79bbc6c660db59428fe9a181864", "following_count": "5639" }, { "rank": 2, "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "following_count": "3403" }, { "rank": 3, "address": "0xd4713cca4068700cf722f8c2b6c05f948b75321b", "following_count": "2782" }, { "rank": 4, "address": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "following_count": "1556" }, ... ] ``` --- #### /leaderboard/blocks Get leaderboard of users ranked according to count of users that they blocked. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/blocks ``` ```jsonc // sample response [ { "rank": 1, "address": "0xc808ffa16d6773d6a9109b1ab92e839157eb0954", "blocks_count": "119" }, { "rank": 2, "address": "0xfa35730094b7e0fc1b97f55663aad15e2d2e3e29", "blocks_count": "3" }, { "rank": 3, "address": "0x983110309620d911731ac0932219af06091b6744", "blocks_count": "2" }, { "rank": 4, "address": "0x2933387ec4c9bbc4a8200cfd77db53d7bc8ebc11", "blocks_count": "1" }, ... ] ``` --- #### /leaderboard/followers Get leaderboard of users ranked according to follower counts. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/leaderboard/followers ``` ```jsonc // sample response [ { "rank": 1, "address": "0x983110309620d911731ac0932219af06091b6744", "followers_count": "365" }, { "rank": 2, "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", "followers_count": "285" }, { "rank": 3, "address": "0x849151d7d0bf1f34b70d5cad5149d28cc2308bf1", "followers_count": "270" }, { "rank": 4, "address": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", "followers_count": "228" }, ... ] ``` --- WIP --- #### /lists/\:token_id/recommended Get recommended users for a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/1/recommended ``` ```jsonc // sample response { "recommended": [ { "name": "swindler.eth", "address": "0xf972bf8592c3171b378e97bb869a980c3f476583", "avatar": "https://rainbow.mypinata.cloud/ipfs/QmcSAHrGGdXJRPmxYUk1R86Wqpfgg4TPMAXC6MfQHPugvF", "header": "https://rainbow.mypinata.cloud/ipfs/QmVDbkDutSk4phVohMs76jV4RgT3bpSdzHnesBHFxW6jRL", "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "gratefulape.eth", "address": "0x52a4c418576dc46e4116ececc6f68d1c9b9636ed", "avatar": "https://euc.li/gratefulape.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "treeskulltown.eth", "address": "0x2dacc0b072146b40e60b8596b99756112d45c924", "avatar": "https://euc.li/treeskulltown.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, ... ] } ``` --- #### /lists/\:token_id/stats Get stats of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `live` (bool, optional): Specifies whether to calculate stats or return cached values - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/stats?live=true ``` ```jsonc // sample response { "followers_count": "115", "following_count": "569", } ``` --- #### /lists/\:token_id/details Get details of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/details ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "ens": { "name": "0xthrpw.eth", "avatar": "https://euc.li/0xthrpw.eth", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "display": "0xthrpw.eth", "records": { "avatar": "https://euc.li/0xthrpw.eth", "com.github": "0xthrpw", "com.twitter": "0xthrpw", "description": "Took the mirrors off my cadillac because I don't like looking like I look back.", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "name": "throw;", }, "chains": { "eth": "0xC9C3A4337a1bba75D0860A1A81f7B990dc607334", }, "fresh": 1726680254493, "resolver": "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63", "errors": {}, }, "ranks": { "mutuals_rank": "4", "followers_rank": "17", "following_rank": "18", "top8_rank": "12", "blocks_rank": "7", }, "primary_list": "3", } ``` --- #### /lists/\:token_id/account Get account information by their EFP list id #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/details ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "ens": { "name": "0xthrpw.eth", "avatar": "https://euc.li/0xthrpw.eth", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "display": "0xthrpw.eth", "records": { "avatar": "https://euc.li/0xthrpw.eth", "com.github": "0xthrpw", "com.twitter": "0xthrpw", "description": "Took the mirrors off my cadillac because I don't like looking like I look back.", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "name": "throw;", }, "chains": { "eth": "0xC9C3A4337a1bba75D0860A1A81f7B990dc607334", }, "fresh": 1726679594366, "resolver": "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63", "errors": {}, }, "ranks": { "mutuals_rank": "4", "followers_rank": "17", "following_rank": "18", "top8_rank": "12", "blocks_rank": "7", }, "primary_list": "3", } ``` --- #### /lists/\:token_id/allFollowingAddresses Get all accounts in list format, that are being followed (including blocked and muted) by a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/allFollowingAddresses ``` ```jsonc // sample response [ "0xc6ed8696c4885dcafdc73c5ef28511e02568b472", "0x1cbf9983e0d59276a58db8e8529706004fcb1837", "0x27d311b8958ca479615522304b442e530c8073fe", "0x47c0224f6c298c4b03f2fbbb986815859a0abd20", "0x50e97e480661533b5382e33705e4ce1eb182222e", "0x8480d20583a3138fef7c23eed8f17bf3c01e73b7", "0x97b5c5ac8813bf5aaf689bbb697b56f8d897baef", "0xa6bcb89f21e0bf71e08ded426c142757791e17cf", "0xdc27cb447d713a8320db054a39ab6a42e0af49cb", "0x02ca10c62f160cdd126d1e44ef42224cac745ac8", "0x0433062f9f466c4a184b2ba0e4da38efea5e2f87", ... ] ``` --- #### /lists/\:token_id/searchFollowers Search for followers of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a followers address or ENS name #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/searchFollowers?term=crypt ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "99", "address": "0x19cf388796c31fa7a583270d82659ecd2b4fd490", "ens": { "name": "cryptomandias.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/cryptomandias.eth" }, "tags": [], "is_following": true, "is_blocked": false, "is_muted": false }, { "efp_list_nft_token_id": "553", "address": "0x3b30d44df9afffc07a51457e18410c4ca0f90896", "ens": { "name": "cryptodeadbeat.eth", "avatar": "https://euc.li/cryptodeadbeat.eth" }, "tags": [], "is_following": true, "is_blocked": false, "is_muted": false }, { "efp_list_nft_token_id": "88", "address": "0x5b0f3dbdd49614476e4f5ff5db6fe13d41fcb516", "ens": { "name": "efp.encrypteddegen.eth", "avatar": "https://euc.li/efp.encrypteddegen.eth" }, "tags": [ "top8" ], "is_following": true, "is_blocked": false, "is_muted": false }, ... ] } ``` --- #### /lists/\:token_id/searchFollowing Search for following of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a following address or ENS name #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/searchFollowing?term=bran ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x0ae93a80ef639c07ecf969735c9b3cc90ef6d803", "tags": [], "ens": { "name": "ens.brantly.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/ens.brantly.eth", }, }, { "version": 1, "record_type": "address", "data": "0xe0308a8a9095e4fc554fefdfafc819ff7b0f7103", "tags": [], "ens": { "name": "libran.eth", "avatar": "https://euc.li/libran.eth", }, }, { "version": 1, "record_type": "address", "data": "0x983110309620d911731ac0932219af06091b6744", "tags": ["top8"], "ens": { "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", }, }, ], } ``` --- #### /lists/\:token_id/allFollowers Get all followers (including blocked and muted) of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/allFollowers ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "15", "address": "0x849151d7d0bf1f34b70d5cad5149d28cc2308bf1", "tags": [], "is_following": true, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T14:45:33.882Z" }, { "efp_list_nft_token_id": "294", "address": "0xa7860e99e3ce0752d1ac53b974e309fff80277c6", "tags": [ "top8" ], "is_following": false, "is_blocked": true, "is_muted": false, "updated_at": "2024-09-24T14:45:33.882Z" }, { "efp_list_nft_token_id": "55", "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "tags": [ "bff", "top8" ], "is_following": false, "is_blocked": true, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, ... ] } ``` --- #### /lists/\:token_id/taggedAs Get the tags that are applied to a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/41/taggedAs ``` ```jsonc // sample response { "token_id": "41", "tags": ["top8", "block"], "tagCounts": [ { "tag": "top8", "count": 7, }, { "tag": "block", "count": 1, }, ], "taggedAddresses": [ { "address": "0xf9a24785cab3ed0921c41fb84dedfea935a4ad1b", "tag": "top8", }, { "address": "0x8eddf5431f5b31933bfbd8111d54fc6e9456e6c1", "tag": "top8", }, { "address": "0x8b24b1686832757e2f6d640e11e88e7f0064594a", "tag": "top8", }, { "address": "0x60377ec355857c2d06d1ce28555f624257344b0d", "tag": "top8", }, { "address": "0xfa1afc4534fc9f80a552e61dd04cd8a172c821a6", "tag": "top8", }, { "address": "0xc808ffa16d6773d6a9109b1ab92e839157eb0954", "tag": "block", }, { "address": "0x983110309620d911731ac0932219af06091b6744", "tag": "top8", }, { "address": "0x2a59071ff48936c6838dcac425fa0df6ea5979bf", "tag": "top8", }, ], } ``` --- #### /lists/\:token_id/tags Get the tags of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/tags ``` ```jsonc // sample response { "token_id": "3", "tags": ["vogu", "top8"], "tagCounts": [ { "tag": "vogu", "count": 1, }, { "tag": "top8", "count": 8, }, ], "taggedAddresses": [ { "address": "0x0f2e3e67cb000993d07e60261748963d3f4bd6d9", "tag": "vogu", }, { "address": "0x71adb34117c9408e74ed112b327a0ec97cef8fa1", "tag": "top8", }, { "address": "0x8f5906963ae276e1631efa8ff1a9cae6499ec5e3", "tag": "top8", }, { "address": "0x983110309620d911731ac0932219af06091b6744", "tag": "top8", }, { "address": "0xbe4f0cdf3834bd876813a1037137dcfad79acd99", "tag": "top8", }, { "address": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", "tag": "top8", }, { "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", "tag": "top8", }, { "address": "0xe2cded674643743ec1316858dfd4fd2116932e63", "tag": "top8", }, { "address": "0xeb6b293e9bb1d71240953c8306ad2c8ac523516a", "tag": "top8", }, ], } ``` --- #### /lists/\:token_id/\:addressOrENS/buttonState Get the following state between a given list and a given user. #### Path Parameters - `token_id` (string): The EFP List of the account - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/brantly.eth/buttonState ``` ```jsonc // sample response { "token_id": "3", "address": "0x983110309620d911731ac0932219af06091b6744", "state": { "follow": true, "block": false, "mute": false, }, } ``` --- #### /lists/\:token_id/following Get accounts being followed (excluding blocked and muted) by a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/following ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0xc6ed8696c4885dcafdc73c5ef28511e02568b472", "tags": [] }, { "version": 1, "record_type": "address", "data": "0x1cbf9983e0d59276a58db8e8529706004fcb1837", "tags": [] }, { "version": 1, "record_type": "address", "data": "0x27d311b8958ca479615522304b442e530c8073fe", "tags": [] }, ... ] } ``` --- #### /lists/\:token_id/allFollowing Get all accounts being followed (including blocked and muted) by a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/allFollowing ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0xad73eafcac4f4c6755dfc61770875fb8b6bc8a25", "tags": [] }, { "version": 1, "record_type": "address", "data": "0xfee41e0f01112d9bdaa73a5a368f4afb4d9baa08", "tags": [] }, { "version": 1, "record_type": "address", "data": "0xf972bf8592c3171b378e97bb869a980c3f476583", "tags": [] }, ... ] } ``` --- #### /lists/\:token_id/\:addressOrENS/followerState Get the follower state between a given list and a given user. #### Path Parameters - `token_id` (string): The EFP List of the account - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/3/encrypteddegen.eth/followerState ``` ```jsonc // sample response { "token_id": "3", "address": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", "state": { "follow": true, "block": false, "mute": false, }, } ``` --- #### /lists/\:token_id/followers Get followers (excluding blocked and muted) of a user by their EFP list id. #### Path Parameters - `token_id` (string): The EFP List of the account #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/lists/4/followers ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "325", "address": "0x9b2fb7a8d227cdaa8002f80e8c8a99a19bb1b969", "tags": [], "is_following": true, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, { "efp_list_nft_token_id": "728", "address": "0xca034d4438719391b5e7589242a36ec535ed6836", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, { "efp_list_nft_token_id": "723", "address": "0x8901083bb577b335a5f6fddde705c00efe8c33d9", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-09-24T16:45:26.088Z" }, ... ] } ``` --- #### /discover Get recently active accounts to follow. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/discover ``` ```jsonc // sample response { "latestFollows": [ { "address": "0xa8b4756959e1192042fc2a8a103dfe2bddf128e8", "name": "caveman.eth", "avatar": "https://euc.li/caveman.eth", "header": "https://i.imgur.com/KYD6snF.jpeg", "followers": "162", "following": "482" }, { "address": "0x8513eef11bba6a57845d10780e7e889e3be289e8", "name": "oandrade.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/oandrade.eth", "header": null, "followers": "5", "following": "15" }, { "address": "0xb6518c8304992da58de9055f1db80a37609f00a2", "name": "silvr.eth", "avatar": "https://euc.li/silvr.eth", "header": null, "followers": "1", "following": "0" }, { "address": "0x8eddf5431f5b31933bfbd8111d54fc6e9456e6c1", "name": "slowsort.eth", "avatar": "https://euc.li/slowsort.eth", "header": null, "followers": "49", "following": "565" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "name": "garypalmerjr.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/garypalmerjr.eth", "header": null, "followers": "152", "following": "3403" }, ... ] } ``` --- #### /exportState/\:token_id Get all accounts that are being followed by EFP list id, excludes blocks and mutes #### Path Parameters - `token_id` (string): The EFP List of the account #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/exportState/333 ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x4c47ab777f1f64d1f3d6efbf1cc7ab5a5224af4a", "tags": [], }, { "version": 1, "record_type": "address", "data": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", "tags": [], }, { "version": 1, "record_type": "address", "data": "0x27cd3a463df1b3f6c95a222616d474be009c2cbb", "tags": [], }, { "version": 1, "record_type": "address", "data": "0xd1efdd037566b0c75cebace9150d26ea0153faa9", "tags": [], }, ], } ``` --- ### /users/\:addressOrENS/list-records Get the list records of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/encrypteddegen.eth/list-records ``` ```jsonc // sample response { "records": [ { "version": 1, "record_type": "address", "data": "0x0116acf39cf70fefc3c23c88a37e59474e8e17be", "tags": null }, { "version": 1, "record_type": "address", "data": "0x021021ccee934b346160342f8d7f59f514c08c56", "tags": null }, { "version": 1, "record_type": "address", "data": "0x025376e7e7f161a198fb5fc90a220a553836d11a", "tags": null }, ... ] } ``` --- ### /users/\:addressOrENS/recommended Get recommended users for a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/recommended ``` ```jsonc // sample response { "recommended": [ { "name": "swindler.eth", "address": "0xf972bf8592c3171b378e97bb869a980c3f476583", "avatar": "https://rainbow.mypinata.cloud/ipfs/QmcSAHrGGdXJRPmxYUk1R86Wqpfgg4TPMAXC6MfQHPugvF", "header": "https://rainbow.mypinata.cloud/ipfs/QmVDbkDutSk4phVohMs76jV4RgT3bpSdzHnesBHFxW6jRL", "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "gratefulape.eth", "address": "0x52a4c418576dc46e4116ececc6f68d1c9b9636ed", "avatar": "https://euc.li/gratefulape.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, { "name": "treeskulltown.eth", "address": "0x2dacc0b072146b40e60b8596b99756112d45c924", "avatar": "https://euc.li/treeskulltown.eth", "header": null, "class": "B", "created_at": "2025-03-07T15:53:58.797Z" }, ... ] } ``` --- ### /users/\:addressOrENS/lists Get the lists of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/lists ``` ```jsonc // sample response { "primary_list": "4", "lists": ["4", "107"], } ``` --- ### /users/\:addressOrENS/notifications Get incoming actions received from other users by Address or ENS Name #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `opcode` (number, optional): Specifies the type of operation to return [1 for follow, 2 for un-follow, 3 for tag, 4 for un-tag, 0 for any] - `interval` (string, optional): Specifies the time range of notifications to return [hour, day, week, month, all]. - `start` (number, optional): Specifies the starting timestamp to begin the interval, default value is now. [unix timestamp ex. 1741159543] - `tag` (string, optional): Specifies a single tag string, of which each account in the response should have at least one. - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Default Values A request to this endpoint with no query parameters specified will default to: - opcode = all - interval = week - start = (current timestamp of now) - tag = "" - limit = 10 - offset = 0 Take care not to request tags joined with incorrect opcodes i.e. requests with a tag specified but opcode = 2 will not return data #### Sample Query: No params set ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/notifications ``` ```jsonc // sample response { "summary": { "interval": "168:00:00(hrs)", "opcode": "all", "total": 10, "total_follows": 9, "total_unfollows": 0, "total_tags": 1, "total_untags": 0 }, "notifications": [ { "address": "0x8004f955c7ed19b465f1f30ad7d04c6d2edc4e81", "name": "jackflash.eth", "avatar": "https://euc.li/jackflash.eth", "token_id": "25556", "action": "tag", "opcode": 3, "op": "0x01030101c9c3a4337a1bba75d0860a1a81f7b990dc607334646576", "tag": "dev", "updated_at": "2024-12-04T04:07:55.948Z" }, { "address": "0xce89b39a2c5d66040093df8013f02d1f0a124200", "name": "nomamkin.eth", "avatar": "https://ens.xyz/nomamkin.eth", "token_id": "28502", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T12:15:42.656Z" }, { "address": "0x69a00fafe7e935fde9ecb5c53f23e0e409a33e12", "name": "myavocado.eth", "avatar": "https://euc.li/myavocado.eth", "token_id": "28491", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T11:28:45.493Z" }, ... ] } ``` #### Sample Query: Follows in last week ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/notifications?opcode=1&interval=week ``` ```jsonc // sample response { "summary": { "interval": "168:00:00(hrs)", "opcode": "1", "total": 10, "total_follows": 10, "total_unfollows": 0, "total_tags": 0, "total_untags": 0 }, "notifications": [ { "address": "0xce89b39a2c5d66040093df8013f02d1f0a124200", "name": "nomamkin.eth", "avatar": "https://ens.xyz/nomamkin.eth", "token_id": "28502", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T12:15:42.656Z" }, { "address": "0x69a00fafe7e935fde9ecb5c53f23e0e409a33e12", "name": "myavocado.eth", "avatar": "https://euc.li/myavocado.eth", "token_id": "28491", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T11:28:45.493Z" }, { "address": "0x11a0cbe3548636d02506e945c77b17c5d3fd5fd5", "name": "senior01.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/senior01.eth", "token_id": "31911", "action": "follow", "opcode": 1, "op": "0x01010101c9c3a4337a1bba75d0860a1a81f7b990dc607334", "tag": "", "updated_at": "2024-12-03T11:22:00.378Z" }, ... ] } ``` #### Sample Query: All cases where tag is 'top8' ```sh curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/notifications?interval=all&tag=top8 ``` ```jsonc // sample response { "summary": { "interval": "999:00:00(hrs)", "opcode": "all", "total": 4, "total_follows": 0, "total_unfollows": 0, "total_tags": 4, "total_untags": 0 }, "notifications": [ { "address": "0x2e711004fef4751b62aeb3608d722d22ce536d84", "name": "10bitcoin.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/10bitcoin.eth", "token_id": "30340", "action": "tag", "opcode": 3, "op": "0x01030101c9c3a4337a1bba75d0860a1a81f7b990dc607334746f7038", "tag": "top8", "updated_at": "2024-11-22T07:30:15.626Z" }, { "address": "0x9a4c6ec8af626ae0c214c3bdd14ac56be15aaefd", "name": "lagovskiiigor.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/lagovskiiigor.eth", "token_id": "30054", "action": "tag", "opcode": 3, "op": "0x01030101c9c3a4337a1bba75d0860a1a81f7b990dc607334746f7038", "tag": "top8", "updated_at": "2024-11-21T06:01:02.407Z" }, ... ] } ``` --- ### /users/\:addressOrENS/stats Get stats of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `live` (bool, optional): Specifies whether to calculate stats or return cached values - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/limes.eth/stats ``` ```jsonc // sample response { "followers_count": "104", "following_count": "26", } ``` --- ### /users/\:addressOrENS/details Get account details, populates most of the data on a profile card #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/details ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "ens": { "name": "0xthrpw.eth", "avatar": "https://euc.li/0xthrpw.eth", "records": { "avatar": "https://euc.li/0xthrpw.eth", "com.github": "0xthrpw", "com.twitter": "0xthrpw", "description": "Took the mirrors off my cadillac because I don't like looking like I look back.", "header": "https://storage.googleapis.com/nftimagebucket/tokens/0xb7def63a9040ad5dc431aff79045617922f4023a/1897.svg", "name": "throw;", }, "updated_at": "2024-09-18T02:12:57.934Z", }, "ranks": { "mutuals_rank": "6", "followers_rank": "19", "following_rank": "18", "top8_rank": "12", "blocks_rank": "7", }, "primary_list": "3", } ``` --- ### /users/\:addressOrENS/commonFollowers Get common followers that are shared by two accounts #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `leader` (address, required): Specifies the account whose followers should be compared #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/commonFollowers?leader=0x0312567d78ff0c9ce0bd62a250df5c6474c71334 ``` ```jsonc // sample response { "results": [ { "address": "0x0312567d78ff0c9ce0bd62a250df5c6474c71334", "name": "pepe.eth", "avatar": "https://preview.redd.it/23tzr9qimgf51.png?auto=webp&s=d5475b2c247d3f3b4c8d9d3d3cae2521e15437ef", "mutuals_rank": "10" }, { "address": "0x038b716928a41ea42253ac043af4f8fdcd940098", "name": "aaron.box", "avatar": "https://metadata.ens.domains/mainnet/avatar/aaron.box", "mutuals_rank": "108" }, { "address": "0x074470b9a32f68de86fac393a10d5cea01c54269", "name": "pawswap.eth", "avatar": "https://euc.li/pawswap.eth", "mutuals_rank": "18" }, { "address": "0x074631095645e426e50b478d40301dd35e74f24c", "name": "pasqui.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/pasqui.eth", "mutuals_rank": "201" }, ... ], "length": 76 } ``` --- ### /users/\:addressOrENS/account Get account information by Address or ENS Name #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl http://api.ethfollow.xyz/api/v1/users/dr3a.eth/account ``` ```jsonc // sample response { "address": "0xeb6b293e9bb1d71240953c8306ad2c8ac523516a", "ens": { "name": "dr3a.eth", "avatar": "https://avatar-upload.ens-cf.workers.dev/mainnet/dr3a.eth?timestamp=1681787731836", "records": { "avatar": "https://avatar-upload.ens-cf.workers.dev/mainnet/dr3a.eth?timestamp=1681787731836", "com.discord": "dr3a.eth", "com.twitter": "dr3a_eth", "description": "dr3a.eth 💙", "email": "dr3a.eth@skiff.com", "name": "drea", "org.telegram": "dr3adoteth", "url": "https://dr3a.eth.limo", }, "updated_at": "2024-09-18T01:54:52.959Z", }, } ``` --- ### /users/\:addressOrENS/searchFollowers Search for followers of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a followers address or ENS name #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/searchFollowers?term=brant ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "4", "address": "0x983110309620d911731ac0932219af06091b6744", "ens": { "name": "brantly.eth", "avatar": "https://euc.li/brantly.eth", }, "tags": [], "is_following": true, "is_blocked": false, "is_muted": false, }, ], } ``` --- ### /users/\:addressOrENS/primary-list Get the primary list of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/encrypteddegen.eth/primary-list ``` ```jsonc // sample response { "primary_list": "1", } ``` --- ### /users/\:addressOrENS/searchFollowing Search for following of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `term` (string, optional): Specifies the string to search for in a following address or ENS name #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/searchFollowing?term=degen ``` ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x96053204967c30079529adddc56f6a37380205af", "tags": [], "ens": { "name": "alphadegen.eth", "avatar": "https://bafybeiaqof5u4bj57t36pt2t7egerky6epvutg7yb4suljnnjuqboymhvi.ipfs.dweb.link/af1f0c19f22c6ee0ea9a9e5f89d585df1ab8c677ef0ef7f0a448cce0fef21a71.png" } }, { "version": 1, "record_type": "address", "data": "0x69207d197063c6b207ff206fdba916e1700d60fa", "tags": [], "ens": { "name": "degenfam.eth", "avatar": "https://codemakes.art/image/quasars/2631" } }, { "version": 1, "record_type": "address", "data": "0x70bb434ea7b7f14709ed0dd17cc54056812cf4ad", "tags": [], "ens": { "name": "teradegen.eth", "avatar": "https://metadata.ens.domains/mainnet/avatar/teradegen.eth" } }, } ``` --- ### /users/\:addressOrENS/ens Get the ENS data of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/ens ``` ```jsonc // sample response { "ens": { "name": "brantly.eth", "address": "0x983110309620d911731ac0932219af06091b6744", "avatar": "https://euc.li/brantly.eth", "records": { "avatar": "https://euc.li/brantly.eth", "com.discord": "brantly.eth", "com.github": "brantlymillegan", "com.twitter": "brantlymillegan", "description": "Catholic, husband, father | building @efp.eth | ENS (DAO delegate, former core team) | Sign-in with Ethereum (creator)", "email": "me@brantly.xyz", "header": "https://i.imgur.com/Quo06x2.png", "location": "USA", "name": "Brantly Millegan", "org.telegram": "brantlymillegan", "url": "https://ethfollow.xyz/", }, "updated_at": "2024-09-18T03:40:58.807Z", }, } ``` --- ### /users/\:addressOrENS/taggedAs Get the tags that are applied to a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/taggedAs ``` ```jsonc // sample response { "address": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "tags": ["top8"], "tagCounts": [ { "tag": "top8", "count": 5, }, ], "taggedAddresses": [ { "address": "0x5a3bf42028901447434d12c5459954e667e5c518", "tag": "top8", }, { "address": "0x71adb34117c9408e74ed112b327a0ec97cef8fa1", "tag": "top8", }, { "address": "0x8eddf5431f5b31933bfbd8111d54fc6e9456e6c1", "tag": "top8", }, { "address": "0xfa1afc4534fc9f80a552e61dd04cd8a172c821a6", "tag": "top8", }, { "address": "0xc983ebc9db969782d994627bdffec0ae6efee1b3", "tag": "top8", }, ], } ``` --- ### /users/\:addressOrENS/tags Get the tags of a user by their address or ENS name. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/tags ``` ```jsonc // sample response { "address": "0x983110309620d911731ac0932219af06091b6744", "tags": [ "top8", "block", "degen" ], "tagCounts": [ { "tag": "top8", "count": 8 }, { "tag": "block", "count": 2 }, { "tag": "degen", "count": 4 } ], "taggedAddresses": [ { "address": "0x44a3a18df15ae79bbc6c660db59428fe9a181864", "tag": "top8" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "tag": "block" }, { "address": "0x4d982788c01402c4e0f657e1192d7736084ae5a8", "tag": "degen" }, { "address": "0x60377ec355857c2d06d1ce28555f624257344b0d", "tag": "top8" }, ... ] } ``` --- ### /users/\:addressOrENS/following Get following by Address or ENS Name #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/dr3a.eth/following ``` #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. ```jsonc // sample response { "following": [ { "version": 1, "record_type": "address", "data": "0x983110309620d911731ac0932219af06091b6744", "tags": ["efp", "ens"], }, { "version": 1, "record_type": "address", "data": "0xbdb41bff7e828e2dc2d15eb67257455db818f1dc", "tags": ["efp", "ens"], }, { "version": 1, "record_type": "address", "data": "0xf4212614c7fe0b3feef75057e88b2e77a7e23e83", "tags": ["efp"], }, ], } ``` --- ### /users/\:addressOrENS/\:addressOrENS2/followerState Get the follower state between two users. #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. - `addressOrENS2` (string): The address or ENS name of the account. #### Query Parameters - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/brantly.eth/followerState ``` ```jsonc // sample response { "addressUser": "0xc9c3a4337a1bba75d0860a1a81f7b990dc607334", "addressFollower": "0x983110309620d911731ac0932219af06091b6744", "state": { "follow": true, "block": false, "mute": false, }, } ``` --- ### /users/\:addressOrENS/followers Get followers by Address or ENS Name #### Path Parameters - `addressOrENS` (string): The address or ENS name of the account. #### Query Parameters - `limit` (number, optional): Specifies the amount of records to return in the response. If not specifed, default value is 10. - `offset` (number, optional): Specifies the starting index of the records to return in the response. If not specifed, default value is 0. - `tags` (string, optional): Specifies an array of comma separated tags, of which each account in the response should have at least one. - `sort` (string, optional): Specifies how the results should be sorted, possible values 'latest', 'earliest', 'followers'. If not specified, default sort is 'latest' - `cache` (string, optional): If set to 'fresh' the cache lookup will be skipped, fresh data will be returned and the cache record will be updated with the new data. #### Sample Query ```sh copy curl http://api.ethfollow.xyz/api/v1/users/dr3a.eth/followers ``` ```jsonc // sample response { "followers": [ { "efp_list_nft_token_id": "5895", "address": "0xd56c76b3f924e8f84a02654ff072a363a84b91d9", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T19:45:38.617Z", }, { "efp_list_nft_token_id": "6337", "address": "0x907ed289f363dbdb2ab1230dfbd2f77a05cda82d", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T17:47:20.727Z", }, { "efp_list_nft_token_id": "13986", "address": "0x7766ef005ec1b38a8472831e2f0631b12c811a5f", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T17:12:28.718Z", }, { "efp_list_nft_token_id": "6323", "address": "0x4e203e4f4bbf119f4e83763d5b143316b3b3c6cc", "tags": [], "is_following": false, "is_blocked": false, "is_muted": false, "updated_at": "2024-10-14T15:06:03.633Z", }, ], } ``` --- # Github Repositories - https://github.com/ethereumfollowprotocol/app - https://github.com/ethereumfollowprotocol/docs - https://github.com/ethereumfollowprotocol/api - https://github.com/ethereumfollowprotocol/services - https://github.com/ethereumfollowprotocol/indexer - https://github.com/ethereumfollowprotocol/notification-service - https://github.com/ethereumfollowprotocol/follow-bot - https://github.com/ethereumfollowprotocol/contracts - https://github.com/ethereumfollowprotocol/replay - https://github.com/ethereumfollowprotocol/onchain --- # Contracts > Deployed solidity contracts used by the Ethereum Follow Protocol. ## EFPListRecords.sol ```solc // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0 ^0.8.23; // lib/openzeppelin-contracts/contracts/utils/Context.sol // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } // src/interfaces/IEFPListRecords.sol /** * @title IEFPListMetadata */ interface IEFPListMetadata { event UpdateListMetadata(uint256 indexed slot, string key, bytes value); struct KeyValue { string key; bytes value; } function getMetadataValue(uint256 slot, string calldata key) external view returns (bytes memory); function getMetadataValues(uint256 slot, string[] calldata keys) external view returns (bytes[] memory); function setMetadataValue(uint256 slot, string calldata key, bytes calldata value) external; function setMetadataValues(uint256 slot, KeyValue[] calldata records) external; // List Manager Functions function claimListManager(uint256 slot) external; function claimListManagerForAddress(uint256 slot, address manager) external; function getListManager(uint256 slot) external view returns (address); function setListManager(uint256 slot, address manager) external; // List User Functions function getListUser(uint256 slot) external view returns (address); function setListUser(uint256 slot, address user) external; } /** * @title IEFPListRecords * @notice Interface for the ListRecords contract. */ interface IEFPListRecords is IEFPListMetadata { // Events event ListOp(uint256 indexed slot, bytes op); // List Operation Functions - Read function getListOpCount(uint256 slot) external view returns (uint256); function getListOp(uint256 slot, uint256 index) external view returns (bytes memory); function getListOpsInRange(uint256 slot, uint256 start, uint256 end) external view returns (bytes[] memory); function getAllListOps(uint256 slot) external view returns (bytes[] memory); // List Operation Functions - Write function applyListOp(uint256 slot, bytes calldata op) external; function applyListOps(uint256 slot, bytes[] calldata ops) external; function setMetadataValuesAndApplyListOps(uint256 slot, KeyValue[] calldata records, bytes[] calldata ops) external; } // lib/openzeppelin-contracts/contracts/access/Ownable.sol // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * By default, the owner account will be the one that deploys the contract. This * can later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _transferOwnership(_msgSender()); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // lib/openzeppelin-contracts/contracts/security/Pausable.sol // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) /** * @dev Contract module which allows children to implement an emergency stop * mechanism that can be triggered by an authorized account. * * This module is used through inheritance. It will make available the * modifiers `whenNotPaused` and `whenPaused`, which can be applied to * the functions of your contract. Note that they will not be pausable by * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { /** * @dev Emitted when the pause is triggered by `account`. */ event Paused(address account); /** * @dev Emitted when the pause is lifted by `account`. */ event Unpaused(address account); bool private _paused; /** * @dev Initializes the contract in unpaused state. */ constructor() { _paused = false; } /** * @dev Modifier to make a function callable only when the contract is not paused. * * Requirements: * * - The contract must not be paused. */ modifier whenNotPaused() { _requireNotPaused(); _; } /** * @dev Modifier to make a function callable only when the contract is paused. * * Requirements: * * - The contract must be paused. */ modifier whenPaused() { _requirePaused(); _; } /** * @dev Returns true if the contract is paused, and false otherwise. */ function paused() public view virtual returns (bool) { return _paused; } /** * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { require(!paused(), "Pausable: paused"); } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { require(paused(), "Pausable: not paused"); } /** * @dev Triggers stopped state. * * Requirements: * * - The contract must not be paused. */ function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } /** * @dev Returns to normal state. * * Requirements: * * - The contract must be paused. */ function _unpause() internal virtual whenPaused { _paused = false; emit Unpaused(_msgSender()); } } // src/lib/ENSReverseClaimer.sol interface ENS { /** * @dev Returns the address that owns the specified node. * @param node The specified node. * @return address of the owner. */ function owner(bytes32 node) external view returns (address); } interface IReverseRegistrar { /** * @dev Transfers ownership of the reverse ENS record associated with the * calling account. * @param owner The address to set as the owner of the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function claim(address owner) external returns (bytes32); /** * @dev Sets the `name()` record for the reverse ENS record associated with * the calling account. First updates the resolver to the default reverse * resolver if necessary. * @param name The name to set for this address. * @return The ENS node hash of the reverse record. */ function setName(string memory name) external returns (bytes32); } /** * @title ENSReverseClaimer * @dev This contract is used to claim reverse ENS records. */ abstract contract ENSReverseClaimer is Ownable { /// @dev The namehash of 'addr.reverse', the domain at which reverse records /// are stored in ENS. bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; /** * @dev Transfers ownership of the reverse ENS record associated with the * contract. * @param ens The ENS registry. * @param claimant The address to set as the owner of the reverse record in * ENS. * @return The ENS node hash of the reverse record. */ function claimReverseENS(ENS ens, address claimant) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).claim(claimant); } /** * @dev Sets the reverse ENS record associated with the contract. * @param ens The ENS registry. * @param name The name to set as the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function setReverseENS(ENS ens, string calldata name) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).setName(name); } } // src/EFPListRecords.sol /** * @title ListMetadata * @author Cory Gabrielsen (cory.eth) * @custom:contributor throw; (0xthrpw.eth) * @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM * * @notice Manages key-value pairs associated with EFP List NFTs. * Provides functionalities for list managers to set and retrieve metadata for their lists. */ abstract contract ListMetadata is IEFPListMetadata, Pausable, Ownable { error SlotAlreadyClaimed(uint256 slot, address manager); // error NotListManager(address manager); /////////////////////////////////////////////////////////////////////////// // Data Structures /////////////////////////////////////////////////////////////////////////// /// @dev The key-value set for each token ID mapping(uint256 => mapping(string => bytes)) private values; ///////////////////////////////////////////////////////////////////////////// // Pausable ///////////////////////////////////////////////////////////////////////////// /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ///////////////////////////////////////////////////////////////////////////// // Helpers ///////////////////////////////////////////////////////////////////////////// function bytesToAddress(bytes memory b) internal pure returns (address) { require(b.length == 20, 'Invalid length'); address addr; assembly { addr := mload(add(b, 20)) } return addr; } ///////////////////////////////////////////////////////////////////////////// // Getters ///////////////////////////////////////////////////////////////////////////// /** * @dev Retrieves metadata value for token ID and key. * @param tokenId The token Id to query. * @param key The key to query. * @return The associated value. */ function getMetadataValue(uint256 tokenId, string calldata key) external view returns (bytes memory) { return values[tokenId][key]; } /** * @dev Retrieves metadata values for token ID and keys. * @param tokenId The token Id to query. * @param keys The keys to query. * @return The associated values. */ function getMetadataValues(uint256 tokenId, string[] calldata keys) external view returns (bytes[] memory) { uint256 length = keys.length; bytes[] memory result = new bytes[](length); for (uint256 i = 0; i < length;) { string calldata key = keys[i]; result[i] = values[tokenId][key]; unchecked { ++i; } } return result; } ///////////////////////////////////////////////////////////////////////////// // Setters ///////////////////////////////////////////////////////////////////////////// /** * @dev Sets metadata records for token ID with the unique key key to value, * overwriting anything previously stored for token ID and key. To clear a * field, set it to the empty string. * @param slot The slot corresponding to the list to update. * @param key The key to set. * @param value The value to set. */ function _setMetadataValue(uint256 slot, string memory key, bytes memory value) internal { values[slot][key] = value; emit UpdateListMetadata(slot, key, value); } /** * @dev Sets metadata records for token ID with the unique key key to value, * overwriting anything previously stored for token ID and key. To clear a * field, set it to the empty string. Only callable by the list manager. * @param slot The slot corresponding to the list to update. * @param key The key to set. * @param value The value to set. */ function setMetadataValue(uint256 slot, string calldata key, bytes calldata value) external whenNotPaused onlyListManager(slot) { _setMetadataValue(slot, key, value); } /** * @dev Sets an array of metadata records for a token ID. Each record is a * key/value pair. * @param slot The slot corresponding to the list to update. * @param records The records to set. */ function _setMetadataValues(uint256 slot, KeyValue[] calldata records) internal { uint256 length = records.length; for (uint256 i = 0; i < length;) { KeyValue calldata record = records[i]; _setMetadataValue(slot, record.key, record.value); unchecked { ++i; } } } /** * @dev Sets an array of metadata records for a token ID. Each record is a * key/value pair. Only callable by the list manager. * @param slot The slot corresponding to the list to update. * @param records The records to set. */ function setMetadataValues(uint256 slot, KeyValue[] calldata records) external whenNotPaused onlyListManager(slot) { _setMetadataValues(slot, records); } /////////////////////////////////////////////////////////////////////////// // Modifiers /////////////////////////////////////////////////////////////////////////// /** * @notice Ensures that the caller is the manager of the specified list. * @param slot The unique identifier of the list. * @dev Used to restrict function access to the list's manager. */ modifier onlyListManager(uint256 slot) { bytes memory existing = values[slot]['manager']; // if not set, claim for msg.sender if (existing.length != 20) { _claimListManager(slot, msg.sender); } else { address existingManager = bytesToAddress(existing); if (existingManager == address(0)) { _claimListManager(slot, msg.sender); } else { require(existingManager == msg.sender, 'Not list manager'); } } _; } /////////////////////////////////////////////////////////////////////////// // List Manager - Claim /////////////////////////////////////////////////////////////////////////// /** * @notice Allows an address to claim management of an unclaimed list slot. * @param slot The slot that the sender wishes to claim. * @param manager The address to be set as the manager. * @dev This function establishes the first-come-first-serve basis for slot claiming. */ function _claimListManager(uint256 slot, address manager) internal { bytes memory existing = values[slot]['manager']; // require(existing.length != 20 || bytesToAddress(existing) == manager, "slot already claimed"); if (existing.length == 20) { address existingManager = bytesToAddress(existing); if (existingManager != manager) { revert SlotAlreadyClaimed(slot, existingManager); } } _setMetadataValue(slot, 'manager', abi.encodePacked(manager)); } /** * @notice Allows the sender to claim management of an unclaimed list slot. * @param slot The slot that the sender wishes to claim. */ function claimListManager(uint256 slot) external whenNotPaused { _claimListManager(slot, msg.sender); } /** * @notice Allows the sender to transfer management of a list to a new address. * @param slot The list's unique identifier. * @param manager The address to be set as the new manager. */ function claimListManagerForAddress(uint256 slot, address manager) external whenNotPaused { _claimListManager(slot, manager); } /////////////////////////////////////////////////////////////////////////// // List Manager - Read /////////////////////////////////////////////////////////////////////////// /** * @notice Retrieves the address of the manager for a specified list slot. * @param slot The list's unique identifier. * @return The address of the manager. */ function getListManager(uint256 slot) external view returns (address) { bytes memory existing = values[slot]['manager']; return existing.length != 20 ? address(0) : bytesToAddress(existing); } /////////////////////////////////////////////////////////////////////////// // List Manager - Write /////////////////////////////////////////////////////////////////////////// /** * @notice Allows the current manager to transfer management of a list to a new address. * @param slot The list's unique identifier. * @param manager The address to be set as the new manager. * @dev Only the current manager can transfer their management role. */ function setListManager(uint256 slot, address manager) external whenNotPaused onlyListManager(slot) { _setMetadataValue(slot, 'manager', abi.encodePacked(manager)); } /////////////////////////////////////////////////////////////////////////// // List User - Read /////////////////////////////////////////////////////////////////////////// /** * @notice Retrieves the address of the list user for a specified list * slot. * @param slot The list's unique identifier. * @return The address of the list user. */ function getListUser(uint256 slot) external view returns (address) { bytes memory existing = values[slot]['user']; return existing.length != 20 ? address(0) : bytesToAddress(existing); } /////////////////////////////////////////////////////////////////////////// // List Manager - Write /////////////////////////////////////////////////////////////////////////// /** * @notice Allows the current manager to change the list user to a new * address. * @param slot The list's unique identifier. * @param user The address to be set as the new list user. * @dev Only the current manager can change the list user. */ function setListUser(uint256 slot, address user) external whenNotPaused onlyListManager(slot) { _setMetadataValue(slot, 'user', abi.encodePacked(user)); } } /** * @title EFPListRecords * @notice Manages a dynamic list of records associated with EFP List NFTs. * Provides functionalities for list managers to apply operations to their lists. */ abstract contract ListRecords is IEFPListRecords, ListMetadata { /////////////////////////////////////////////////////////////////////////// // Data Structures /////////////////////////////////////////////////////////////////////////// /// @notice Stores a sequence of operations for each list identified by its slot. /// @dev Each list can have multiple operations performed over time. mapping(uint256 => bytes[]) public listOps; /////////////////////////////////////////////////////////////////////////// // List Operation Functions - Read /////////////////////////////////////////////////////////////////////////// /** * @notice Retrieves the number of operations performed on a list. * @param slot The list's unique identifier. * @return The number of operations performed on the list. */ function getListOpCount(uint256 slot) external view returns (uint256) { return listOps[slot].length; } /** * @notice Retrieves the operation at a specified index for a list. * @param slot The list's unique identifier. * @param index The index of the operation to be retrieved. * @return The operation at the specified index. */ function getListOp(uint256 slot, uint256 index) external view returns (bytes memory) { return listOps[slot][index]; } /** * @notice Retrieves a range of operations for a list. * @param slot The list's unique identifier. * @param start The starting index of the range. * @param end The ending index of the range. * @return The operations in the specified range. */ function getListOpsInRange(uint256 slot, uint256 start, uint256 end) external view returns (bytes[] memory) { if (start > end) { revert('Invalid range'); } bytes[] memory ops = new bytes[](end - start); for (uint256 i = start; i < end;) { ops[i - start] = listOps[slot][i]; unchecked { ++i; } } return ops; } /** * @notice Retrieves all operations for a list. * @param slot The list's unique identifier. * @return The operations performed on the list. */ function getAllListOps(uint256 slot) external view returns (bytes[] memory) { return listOps[slot]; } /////////////////////////////////////////////////////////////////////////// // List Operation Functions - Write /////////////////////////////////////////////////////////////////////////// /** * @notice Applies a single operation to the list. * @param slot The list's unique identifier. * @param op The operation to be applied. */ function _applyListOp(uint256 slot, bytes calldata op) internal { listOps[slot].push(op); emit ListOp(slot, op); } /** * @notice Public wrapper for `_applyOp`, enabling list managers to apply a single operation. * @param slot The list's unique identifier. * @param op The operation to be applied. */ function applyListOp(uint256 slot, bytes calldata op) external whenNotPaused onlyListManager(slot) { _applyListOp(slot, op); } /** * @notice Allows list managers to apply multiple operations in a single transaction. * @param slot The list's unique identifier. * @param ops An array of operations to be applied. */ function _applyListOps(uint256 slot, bytes[] calldata ops) internal { uint256 len = ops.length; for (uint256 i = 0; i < len;) { _applyListOp(slot, ops[i]); unchecked { ++i; } } } /** * @notice Allows list managers to apply multiple operations in a single transaction. * @param slot The list's unique identifier. * @param ops An array of operations to be applied. */ function applyListOps(uint256 slot, bytes[] calldata ops) external whenNotPaused onlyListManager(slot) { _applyListOps(slot, ops); } /** * @notice Allows list managers to set metadata values and apply list ops * in a single transaction. * @param slot The list's unique identifier. * @param records An array of key-value pairs to set. * @param ops An array of operations to be applied. */ function setMetadataValuesAndApplyListOps(uint256 slot, KeyValue[] calldata records, bytes[] calldata ops) external whenNotPaused onlyListManager(slot) { _setMetadataValues(slot, records); _applyListOps(slot, ops); } } contract EFPListRecords is IEFPListRecords, ListRecords, ENSReverseClaimer {} ``` ## EFPListRegistry.sol ```solc // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0 ^0.8.23 ^0.8.4; // lib/openzeppelin-contracts/contracts/utils/Context.sol // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } // src/interfaces/IEFPListNFTPriceOracle.sol /** * @title IEFPListNFTPriceOracle */ interface IEFPListNFTPriceOracle { function getPrice() external view returns (uint256); function getBatchPrice(uint256 quantity) external view returns (uint256); } // src/interfaces/IEFPListRegistry.sol /** * @title EFPListRegistry * @notice A registry connecting token IDs with data such as managers, users, and list locations. */ interface IEFPListRegistry { /////////////////////////////////////////////////////////////////////////// // Enums /////////////////////////////////////////////////////////////////////////// enum MintState { Disabled, OwnerOnly, PublicMint, PublicBatch } /////////////////////////////////////////////////////////////////////////// // Events /////////////////////////////////////////////////////////////////////////// /// @notice Emitted when a list storage location is set event UpdateListStorageLocation(uint256 indexed tokenId, bytes listStorageLocation); /////////////////////////////////////////////////////////////////////////// // ListStorageLocation /////////////////////////////////////////////////////////////////////////// function getListStorageLocation(uint256 tokenId) external view returns (bytes memory); function setListStorageLocation(uint256 tokenId, bytes calldata listStorageLocation) external; /////////////////////////////////////////////////////////////////////////// // Mint /////////////////////////////////////////////////////////////////////////// /// @notice Fetches the mint state. function getMintState() external view returns (MintState); /// @notice Sets the mint state. /// @param _mintState The new mint state. function setMintState(MintState _mintState) external; /// @notice Fetches the max mint batch size. function getMaxMintBatchSize() external view returns (uint256); /// @notice Sets the max mint batch size. /// @param _maxMintBatchSize The new max mint batch size. function setMaxMintBatchSize(uint256 _maxMintBatchSize) external; /// @notice Mints a new token. function mint(bytes calldata listStorageLocation) external payable; /** * @notice Mints a new token to the given address. * @param recipient The address to mint the token to. */ function mintTo(address recipient, bytes calldata listStorageLocation) external payable; /// @notice Mints a new token to the given address. function mintBatch(uint256 quantity) external payable; /// @notice Mints a new token to the given address. function mintBatchTo(address recipient, uint256 quantity) external payable; } // lib/ERC721A/contracts/IERC721A.sol // ERC721A Contracts v4.2.3 // Creator: Chiru Labs /** * @dev Interface of ERC721A. */ interface IERC721A { /** * The caller must own the token or be an approved operator. */ error ApprovalCallerNotOwnerNorApproved(); /** * The token does not exist. */ error ApprovalQueryForNonexistentToken(); /** * Cannot query the balance for the zero address. */ error BalanceQueryForZeroAddress(); /** * Cannot mint to the zero address. */ error MintToZeroAddress(); /** * The quantity of tokens minted must be more than zero. */ error MintZeroQuantity(); /** * The token does not exist. */ error OwnerQueryForNonexistentToken(); /** * The caller must own the token or be an approved operator. */ error TransferCallerNotOwnerNorApproved(); /** * The token must be owned by `from`. */ error TransferFromIncorrectOwner(); /** * Cannot safely transfer to a contract that does not implement the * ERC721Receiver interface. */ error TransferToNonERC721ReceiverImplementer(); /** * Cannot transfer to the zero address. */ error TransferToZeroAddress(); /** * The token does not exist. */ error URIQueryForNonexistentToken(); /** * The `quantity` minted with ERC2309 exceeds the safety limit. */ error MintERC2309QuantityExceedsLimit(); /** * The `extraData` cannot be set on an unintialized ownership slot. */ error OwnershipNotInitializedForExtraData(); // ============================================================= // STRUCTS // ============================================================= struct TokenOwnership { // The address of the owner. address addr; // Stores the start time of ownership with minimal overhead for tokenomics. uint64 startTimestamp; // Whether the token has been burned. bool burned; // Arbitrary data similar to `startTimestamp` that can be set via {_extraData}. uint24 extraData; } // ============================================================= // TOKEN COUNTERS // ============================================================= /** * @dev Returns the total number of tokens in existence. * Burned tokens will reduce the count. * To get the total number of tokens minted, please see {_totalMinted}. */ function totalSupply() external view returns (uint256); // ============================================================= // IERC165 // ============================================================= /** * @dev Returns true if this contract implements the interface defined by * `interfaceId`. See the corresponding * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) * to learn more about how these ids are created. * * This function call must use less than 30000 gas. */ function supportsInterface(bytes4 interfaceId) external view returns (bool); // ============================================================= // IERC721 // ============================================================= /** * @dev Emitted when `tokenId` token is transferred from `from` to `to`. */ event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); /** * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. */ event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); /** * @dev Emitted when `owner` enables or disables * (`approved`) `operator` to manage all of its assets. */ event ApprovalForAll(address indexed owner, address indexed operator, bool approved); /** * @dev Returns the number of tokens in `owner`'s account. */ function balanceOf(address owner) external view returns (uint256 balance); /** * @dev Returns the owner of the `tokenId` token. * * Requirements: * * - `tokenId` must exist. */ function ownerOf(uint256 tokenId) external view returns (address owner); /** * @dev Safely transfers `tokenId` token from `from` to `to`, * checking first that contract recipients are aware of the ERC721 protocol * to prevent tokens from being forever locked. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must exist and be owned by `from`. * - If the caller is not `from`, it must be have been allowed to move * this token by either {approve} or {setApprovalForAll}. * - If `to` refers to a smart contract, it must implement * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. * * Emits a {Transfer} event. */ function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data ) external payable; /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ function safeTransferFrom( address from, address to, uint256 tokenId ) external payable; /** * @dev Transfers `tokenId` from `from` to `to`. * * WARNING: Usage of this method is discouraged, use {safeTransferFrom} * whenever possible. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must be owned by `from`. * - If the caller is not `from`, it must be approved to move this token * by either {approve} or {setApprovalForAll}. * * Emits a {Transfer} event. */ function transferFrom( address from, address to, uint256 tokenId ) external payable; /** * @dev Gives permission to `to` to transfer `tokenId` token to another account. * The approval is cleared when the token is transferred. * * Only a single account can be approved at a time, so approving the * zero address clears previous approvals. * * Requirements: * * - The caller must own the token or be an approved operator. * - `tokenId` must exist. * * Emits an {Approval} event. */ function approve(address to, uint256 tokenId) external payable; /** * @dev Approve or remove `operator` as an operator for the caller. * Operators can call {transferFrom} or {safeTransferFrom} * for any token owned by the caller. * * Requirements: * * - The `operator` cannot be the caller. * * Emits an {ApprovalForAll} event. */ function setApprovalForAll(address operator, bool _approved) external; /** * @dev Returns the account approved for `tokenId` token. * * Requirements: * * - `tokenId` must exist. */ function getApproved(uint256 tokenId) external view returns (address operator); /** * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. * * See {setApprovalForAll}. */ function isApprovedForAll(address owner, address operator) external view returns (bool); // ============================================================= // IERC721Metadata // ============================================================= /** * @dev Returns the token collection name. */ function name() external view returns (string memory); /** * @dev Returns the token collection symbol. */ function symbol() external view returns (string memory); /** * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. */ function tokenURI(uint256 tokenId) external view returns (string memory); // ============================================================= // IERC2309 // ============================================================= /** * @dev Emitted when tokens in `fromTokenId` to `toTokenId` * (inclusive) is transferred from `from` to `to`, as defined in the * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) standard. * * See {_mintERC2309} for more details. */ event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to); } // src/interfaces/ITokenURIProvider.sol interface ITokenURIProvider { function tokenURI(uint256 tokenId) external view returns (string memory); } // lib/ERC721A/contracts/ERC721A.sol // ERC721A Contracts v4.2.3 // Creator: Chiru Labs /** * @dev Interface of ERC721 token receiver. */ interface ERC721A__IERC721Receiver { function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); } /** * @title ERC721A * * @dev Implementation of the [ERC721](https://eips.ethereum.org/EIPS/eip-721) * Non-Fungible Token Standard, including the Metadata extension. * Optimized for lower gas during batch mints. * * Token IDs are minted in sequential order (e.g. 0, 1, 2, 3, ...) * starting from `_startTokenId()`. * * Assumptions: * * - An owner cannot have more than 2**64 - 1 (max value of uint64) of supply. * - The maximum token ID cannot exceed 2**256 - 1 (max value of uint256). */ contract ERC721A is IERC721A { // Bypass for a `--via-ir` bug (https://github.com/chiru-labs/ERC721A/pull/364). struct TokenApprovalRef { address value; } // ============================================================= // CONSTANTS // ============================================================= // Mask of an entry in packed address data. uint256 private constant _BITMASK_ADDRESS_DATA_ENTRY = (1 << 64) - 1; // The bit position of `numberMinted` in packed address data. uint256 private constant _BITPOS_NUMBER_MINTED = 64; // The bit position of `numberBurned` in packed address data. uint256 private constant _BITPOS_NUMBER_BURNED = 128; // The bit position of `aux` in packed address data. uint256 private constant _BITPOS_AUX = 192; // Mask of all 256 bits in packed address data except the 64 bits for `aux`. uint256 private constant _BITMASK_AUX_COMPLEMENT = (1 << 192) - 1; // The bit position of `startTimestamp` in packed ownership. uint256 private constant _BITPOS_START_TIMESTAMP = 160; // The bit mask of the `burned` bit in packed ownership. uint256 private constant _BITMASK_BURNED = 1 << 224; // The bit position of the `nextInitialized` bit in packed ownership. uint256 private constant _BITPOS_NEXT_INITIALIZED = 225; // The bit mask of the `nextInitialized` bit in packed ownership. uint256 private constant _BITMASK_NEXT_INITIALIZED = 1 << 225; // The bit position of `extraData` in packed ownership. uint256 private constant _BITPOS_EXTRA_DATA = 232; // Mask of all 256 bits in a packed ownership except the 24 bits for `extraData`. uint256 private constant _BITMASK_EXTRA_DATA_COMPLEMENT = (1 << 232) - 1; // The mask of the lower 160 bits for addresses. uint256 private constant _BITMASK_ADDRESS = (1 << 160) - 1; // The maximum `quantity` that can be minted with {_mintERC2309}. // This limit is to prevent overflows on the address data entries. // For a limit of 5000, a total of 3.689e15 calls to {_mintERC2309} // is required to cause an overflow, which is unrealistic. uint256 private constant _MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000; // The `Transfer` event signature is given by: // `keccak256(bytes("Transfer(address,address,uint256)"))`. bytes32 private constant _TRANSFER_EVENT_SIGNATURE = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef; // ============================================================= // STORAGE // ============================================================= // The next token ID to be minted. uint256 private _currentIndex; // The number of tokens burned. uint256 private _burnCounter; // Token name string private _name; // Token symbol string private _symbol; // Mapping from token ID to ownership details // An empty struct value does not necessarily mean the token is unowned. // See {_packedOwnershipOf} implementation for details. // // Bits Layout: // - [0..159] `addr` // - [160..223] `startTimestamp` // - [224] `burned` // - [225] `nextInitialized` // - [232..255] `extraData` mapping(uint256 => uint256) private _packedOwnerships; // Mapping owner address to address data. // // Bits Layout: // - [0..63] `balance` // - [64..127] `numberMinted` // - [128..191] `numberBurned` // - [192..255] `aux` mapping(address => uint256) private _packedAddressData; // Mapping from token ID to approved address. mapping(uint256 => TokenApprovalRef) private _tokenApprovals; // Mapping from owner to operator approvals mapping(address => mapping(address => bool)) private _operatorApprovals; // ============================================================= // CONSTRUCTOR // ============================================================= constructor(string memory name_, string memory symbol_) { _name = name_; _symbol = symbol_; _currentIndex = _startTokenId(); } // ============================================================= // TOKEN COUNTING OPERATIONS // ============================================================= /** * @dev Returns the starting token ID. * To change the starting token ID, please override this function. */ function _startTokenId() internal view virtual returns (uint256) { return 0; } /** * @dev Returns the next token ID to be minted. */ function _nextTokenId() internal view virtual returns (uint256) { return _currentIndex; } /** * @dev Returns the total number of tokens in existence. * Burned tokens will reduce the count. * To get the total number of tokens minted, please see {_totalMinted}. */ function totalSupply() public view virtual override returns (uint256) { // Counter underflow is impossible as _burnCounter cannot be incremented // more than `_currentIndex - _startTokenId()` times. unchecked { return _currentIndex - _burnCounter - _startTokenId(); } } /** * @dev Returns the total amount of tokens minted in the contract. */ function _totalMinted() internal view virtual returns (uint256) { // Counter underflow is impossible as `_currentIndex` does not decrement, // and it is initialized to `_startTokenId()`. unchecked { return _currentIndex - _startTokenId(); } } /** * @dev Returns the total number of tokens burned. */ function _totalBurned() internal view virtual returns (uint256) { return _burnCounter; } // ============================================================= // ADDRESS DATA OPERATIONS // ============================================================= /** * @dev Returns the number of tokens in `owner`'s account. */ function balanceOf(address owner) public view virtual override returns (uint256) { if (owner == address(0)) revert BalanceQueryForZeroAddress(); return _packedAddressData[owner] & _BITMASK_ADDRESS_DATA_ENTRY; } /** * Returns the number of tokens minted by `owner`. */ function _numberMinted(address owner) internal view returns (uint256) { return (_packedAddressData[owner] >> _BITPOS_NUMBER_MINTED) & _BITMASK_ADDRESS_DATA_ENTRY; } /** * Returns the number of tokens burned by or on behalf of `owner`. */ function _numberBurned(address owner) internal view returns (uint256) { return (_packedAddressData[owner] >> _BITPOS_NUMBER_BURNED) & _BITMASK_ADDRESS_DATA_ENTRY; } /** * Returns the auxiliary data for `owner`. (e.g. number of whitelist mint slots used). */ function _getAux(address owner) internal view returns (uint64) { return uint64(_packedAddressData[owner] >> _BITPOS_AUX); } /** * Sets the auxiliary data for `owner`. (e.g. number of whitelist mint slots used). * If there are multiple variables, please pack them into a uint64. */ function _setAux(address owner, uint64 aux) internal virtual { uint256 packed = _packedAddressData[owner]; uint256 auxCasted; // Cast `aux` with assembly to avoid redundant masking. assembly { auxCasted := aux } packed = (packed & _BITMASK_AUX_COMPLEMENT) | (auxCasted << _BITPOS_AUX); _packedAddressData[owner] = packed; } // ============================================================= // IERC165 // ============================================================= /** * @dev Returns true if this contract implements the interface defined by * `interfaceId`. See the corresponding * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) * to learn more about how these ids are created. * * This function call must use less than 30000 gas. */ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { // The interface IDs are constants representing the first 4 bytes // of the XOR of all function selectors in the interface. // See: [ERC165](https://eips.ethereum.org/EIPS/eip-165) // (e.g. `bytes4(i.functionA.selector ^ i.functionB.selector ^ ...)`) return interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165. interfaceId == 0x80ac58cd || // ERC165 interface ID for ERC721. interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata. } // ============================================================= // IERC721Metadata // ============================================================= /** * @dev Returns the token collection name. */ function name() public view virtual override returns (string memory) { return _name; } /** * @dev Returns the token collection symbol. */ function symbol() public view virtual override returns (string memory) { return _symbol; } /** * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. */ function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); string memory baseURI = _baseURI(); return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : ''; } /** * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each * token will be the concatenation of the `baseURI` and the `tokenId`. Empty * by default, it can be overridden in child contracts. */ function _baseURI() internal view virtual returns (string memory) { return ''; } // ============================================================= // OWNERSHIPS OPERATIONS // ============================================================= /** * @dev Returns the owner of the `tokenId` token. * * Requirements: * * - `tokenId` must exist. */ function ownerOf(uint256 tokenId) public view virtual override returns (address) { return address(uint160(_packedOwnershipOf(tokenId))); } /** * @dev Gas spent here starts off proportional to the maximum mint batch size. * It gradually moves to O(1) as tokens get transferred around over time. */ function _ownershipOf(uint256 tokenId) internal view virtual returns (TokenOwnership memory) { return _unpackedOwnership(_packedOwnershipOf(tokenId)); } /** * @dev Returns the unpacked `TokenOwnership` struct at `index`. */ function _ownershipAt(uint256 index) internal view virtual returns (TokenOwnership memory) { return _unpackedOwnership(_packedOwnerships[index]); } /** * @dev Initializes the ownership slot minted at `index` for efficiency purposes. */ function _initializeOwnershipAt(uint256 index) internal virtual { if (_packedOwnerships[index] == 0) { _packedOwnerships[index] = _packedOwnershipOf(index); } } /** * Returns the packed ownership data of `tokenId`. */ function _packedOwnershipOf(uint256 tokenId) private view returns (uint256) { uint256 curr = tokenId; unchecked { if (_startTokenId() <= curr) if (curr < _currentIndex) { uint256 packed = _packedOwnerships[curr]; // If not burned. if (packed & _BITMASK_BURNED == 0) { // Invariant: // There will always be an initialized ownership slot // (i.e. `ownership.addr != address(0) && ownership.burned == false`) // before an unintialized ownership slot // (i.e. `ownership.addr == address(0) && ownership.burned == false`) // Hence, `curr` will not underflow. // // We can directly compare the packed value. // If the address is zero, packed will be zero. while (packed == 0) { packed = _packedOwnerships[--curr]; } return packed; } } } revert OwnerQueryForNonexistentToken(); } /** * @dev Returns the unpacked `TokenOwnership` struct from `packed`. */ function _unpackedOwnership(uint256 packed) private pure returns (TokenOwnership memory ownership) { ownership.addr = address(uint160(packed)); ownership.startTimestamp = uint64(packed >> _BITPOS_START_TIMESTAMP); ownership.burned = packed & _BITMASK_BURNED != 0; ownership.extraData = uint24(packed >> _BITPOS_EXTRA_DATA); } /** * @dev Packs ownership data into a single uint256. */ function _packOwnershipData(address owner, uint256 flags) private view returns (uint256 result) { assembly { // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. owner := and(owner, _BITMASK_ADDRESS) // `owner | (block.timestamp << _BITPOS_START_TIMESTAMP) | flags`. result := or(owner, or(shl(_BITPOS_START_TIMESTAMP, timestamp()), flags)) } } /** * @dev Returns the `nextInitialized` flag set if `quantity` equals 1. */ function _nextInitializedFlag(uint256 quantity) private pure returns (uint256 result) { // For branchless setting of the `nextInitialized` flag. assembly { // `(quantity == 1) << _BITPOS_NEXT_INITIALIZED`. result := shl(_BITPOS_NEXT_INITIALIZED, eq(quantity, 1)) } } // ============================================================= // APPROVAL OPERATIONS // ============================================================= /** * @dev Gives permission to `to` to transfer `tokenId` token to another account. * The approval is cleared when the token is transferred. * * Only a single account can be approved at a time, so approving the * zero address clears previous approvals. * * Requirements: * * - The caller must own the token or be an approved operator. * - `tokenId` must exist. * * Emits an {Approval} event. */ function approve(address to, uint256 tokenId) public payable virtual override { address owner = ownerOf(tokenId); if (_msgSenderERC721A() != owner) if (!isApprovedForAll(owner, _msgSenderERC721A())) { revert ApprovalCallerNotOwnerNorApproved(); } _tokenApprovals[tokenId].value = to; emit Approval(owner, to, tokenId); } /** * @dev Returns the account approved for `tokenId` token. * * Requirements: * * - `tokenId` must exist. */ function getApproved(uint256 tokenId) public view virtual override returns (address) { if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken(); return _tokenApprovals[tokenId].value; } /** * @dev Approve or remove `operator` as an operator for the caller. * Operators can call {transferFrom} or {safeTransferFrom} * for any token owned by the caller. * * Requirements: * * - The `operator` cannot be the caller. * * Emits an {ApprovalForAll} event. */ function setApprovalForAll(address operator, bool approved) public virtual override { _operatorApprovals[_msgSenderERC721A()][operator] = approved; emit ApprovalForAll(_msgSenderERC721A(), operator, approved); } /** * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. * * See {setApprovalForAll}. */ function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { return _operatorApprovals[owner][operator]; } /** * @dev Returns whether `tokenId` exists. * * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. * * Tokens start existing when they are minted. See {_mint}. */ function _exists(uint256 tokenId) internal view virtual returns (bool) { return _startTokenId() <= tokenId && tokenId < _currentIndex && // If within bounds, _packedOwnerships[tokenId] & _BITMASK_BURNED == 0; // and not burned. } /** * @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`. */ function _isSenderApprovedOrOwner( address approvedAddress, address owner, address msgSender ) private pure returns (bool result) { assembly { // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. owner := and(owner, _BITMASK_ADDRESS) // Mask `msgSender` to the lower 160 bits, in case the upper bits somehow aren't clean. msgSender := and(msgSender, _BITMASK_ADDRESS) // `msgSender == owner || msgSender == approvedAddress`. result := or(eq(msgSender, owner), eq(msgSender, approvedAddress)) } } /** * @dev Returns the storage slot and value for the approved address of `tokenId`. */ function _getApprovedSlotAndAddress(uint256 tokenId) private view returns (uint256 approvedAddressSlot, address approvedAddress) { TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId]; // The following is equivalent to `approvedAddress = _tokenApprovals[tokenId].value`. assembly { approvedAddressSlot := tokenApproval.slot approvedAddress := sload(approvedAddressSlot) } } // ============================================================= // TRANSFER OPERATIONS // ============================================================= /** * @dev Transfers `tokenId` from `from` to `to`. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must be owned by `from`. * - If the caller is not `from`, it must be approved to move this token * by either {approve} or {setApprovalForAll}. * * Emits a {Transfer} event. */ function transferFrom( address from, address to, uint256 tokenId ) public payable virtual override { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); if (address(uint160(prevOwnershipPacked)) != from) revert TransferFromIncorrectOwner(); (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); // The nested ifs save around 20+ gas over a compound boolean condition. if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) if (!isApprovedForAll(from, _msgSenderERC721A())) revert TransferCallerNotOwnerNorApproved(); if (to == address(0)) revert TransferToZeroAddress(); _beforeTokenTransfers(from, to, tokenId, 1); // Clear approvals from the previous owner. assembly { if approvedAddress { // This is equivalent to `delete _tokenApprovals[tokenId]`. sstore(approvedAddressSlot, 0) } } // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. // Counter overflow is incredibly unrealistic as `tokenId` would have to be 2**256. unchecked { // We can directly increment and decrement the balances. --_packedAddressData[from]; // Updates: `balance -= 1`. ++_packedAddressData[to]; // Updates: `balance += 1`. // Updates: // - `address` to the next owner. // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. // - `nextInitialized` to `true`. _packedOwnerships[tokenId] = _packOwnershipData( to, _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) ); // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { uint256 nextTokenId = tokenId + 1; // If the next slot's address is zero and not burned (i.e. packed value is zero). if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. _packedOwnerships[nextTokenId] = prevOwnershipPacked; } } } } emit Transfer(from, to, tokenId); _afterTokenTransfers(from, to, tokenId, 1); } /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ function safeTransferFrom( address from, address to, uint256 tokenId ) public payable virtual override { safeTransferFrom(from, to, tokenId, ''); } /** * @dev Safely transfers `tokenId` token from `from` to `to`. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must exist and be owned by `from`. * - If the caller is not `from`, it must be approved to move this token * by either {approve} or {setApprovalForAll}. * - If `to` refers to a smart contract, it must implement * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. * * Emits a {Transfer} event. */ function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) public payable virtual override { transferFrom(from, to, tokenId); if (to.code.length != 0) if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { revert TransferToNonERC721ReceiverImplementer(); } } /** * @dev Hook that is called before a set of serially-ordered token IDs * are about to be transferred. This includes minting. * And also called before burning one token. * * `startTokenId` - the first token ID to be transferred. * `quantity` - the amount to be transferred. * * Calling conditions: * * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be * transferred to `to`. * - When `from` is zero, `tokenId` will be minted for `to`. * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ function _beforeTokenTransfers( address from, address to, uint256 startTokenId, uint256 quantity ) internal virtual {} /** * @dev Hook that is called after a set of serially-ordered token IDs * have been transferred. This includes minting. * And also called after one token has been burned. * * `startTokenId` - the first token ID to be transferred. * `quantity` - the amount to be transferred. * * Calling conditions: * * - When `from` and `to` are both non-zero, `from`'s `tokenId` has been * transferred to `to`. * - When `from` is zero, `tokenId` has been minted for `to`. * - When `to` is zero, `tokenId` has been burned by `from`. * - `from` and `to` are never both zero. */ function _afterTokenTransfers( address from, address to, uint256 startTokenId, uint256 quantity ) internal virtual {} /** * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. * * `from` - Previous owner of the given token ID. * `to` - Target address that will receive the token. * `tokenId` - Token ID to be transferred. * `_data` - Optional data to send along with the call. * * Returns whether the call correctly returned the expected magic value. */ function _checkContractOnERC721Received( address from, address to, uint256 tokenId, bytes memory _data ) private returns (bool) { try ERC721A__IERC721Receiver(to).onERC721Received(_msgSenderERC721A(), from, tokenId, _data) returns ( bytes4 retval ) { return retval == ERC721A__IERC721Receiver(to).onERC721Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { revert TransferToNonERC721ReceiverImplementer(); } else { assembly { revert(add(32, reason), mload(reason)) } } } } // ============================================================= // MINT OPERATIONS // ============================================================= /** * @dev Mints `quantity` tokens and transfers them to `to`. * * Requirements: * * - `to` cannot be the zero address. * - `quantity` must be greater than 0. * * Emits a {Transfer} event for each mint. */ function _mint(address to, uint256 quantity) internal virtual { uint256 startTokenId = _currentIndex; if (quantity == 0) revert MintZeroQuantity(); _beforeTokenTransfers(address(0), to, startTokenId, quantity); // Overflows are incredibly unrealistic. // `balance` and `numberMinted` have a maximum limit of 2**64. // `tokenId` has a maximum limit of 2**256. unchecked { // Updates: // - `balance += quantity`. // - `numberMinted += quantity`. // // We can directly add to the `balance` and `numberMinted`. _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1); // Updates: // - `address` to the owner. // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `quantity == 1`. _packedOwnerships[startTokenId] = _packOwnershipData( to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) ); uint256 toMasked; uint256 end = startTokenId + quantity; // Use assembly to loop and emit the `Transfer` event for gas savings. // The duplicated `log4` removes an extra check and reduces stack juggling. // The assembly, together with the surrounding Solidity code, have been // delicately arranged to nudge the compiler into producing optimized opcodes. assembly { // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. toMasked := and(to, _BITMASK_ADDRESS) // Emit the `Transfer` event. log4( 0, // Start of data (0, since no data). 0, // End of data (0, since no data). _TRANSFER_EVENT_SIGNATURE, // Signature. 0, // `address(0)`. toMasked, // `to`. startTokenId // `tokenId`. ) // The `iszero(eq(,))` check ensures that large values of `quantity` // that overflows uint256 will make the loop run out of gas. // The compiler will optimize the `iszero` away for performance. for { let tokenId := add(startTokenId, 1) } iszero(eq(tokenId, end)) { tokenId := add(tokenId, 1) } { // Emit the `Transfer` event. Similar to above. log4(0, 0, _TRANSFER_EVENT_SIGNATURE, 0, toMasked, tokenId) } } if (toMasked == 0) revert MintToZeroAddress(); _currentIndex = end; } _afterTokenTransfers(address(0), to, startTokenId, quantity); } /** * @dev Mints `quantity` tokens and transfers them to `to`. * * This function is intended for efficient minting only during contract creation. * * It emits only one {ConsecutiveTransfer} as defined in * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309), * instead of a sequence of {Transfer} event(s). * * Calling this function outside of contract creation WILL make your contract * non-compliant with the ERC721 standard. * For full ERC721 compliance, substituting ERC721 {Transfer} event(s) with the ERC2309 * {ConsecutiveTransfer} event is only permissible during contract creation. * * Requirements: * * - `to` cannot be the zero address. * - `quantity` must be greater than 0. * * Emits a {ConsecutiveTransfer} event. */ function _mintERC2309(address to, uint256 quantity) internal virtual { uint256 startTokenId = _currentIndex; if (to == address(0)) revert MintToZeroAddress(); if (quantity == 0) revert MintZeroQuantity(); if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) revert MintERC2309QuantityExceedsLimit(); _beforeTokenTransfers(address(0), to, startTokenId, quantity); // Overflows are unrealistic due to the above check for `quantity` to be below the limit. unchecked { // Updates: // - `balance += quantity`. // - `numberMinted += quantity`. // // We can directly add to the `balance` and `numberMinted`. _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1); // Updates: // - `address` to the owner. // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `quantity == 1`. _packedOwnerships[startTokenId] = _packOwnershipData( to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) ); emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to); _currentIndex = startTokenId + quantity; } _afterTokenTransfers(address(0), to, startTokenId, quantity); } /** * @dev Safely mints `quantity` tokens and transfers them to `to`. * * Requirements: * * - If `to` refers to a smart contract, it must implement * {IERC721Receiver-onERC721Received}, which is called for each safe transfer. * - `quantity` must be greater than 0. * * See {_mint}. * * Emits a {Transfer} event for each mint. */ function _safeMint( address to, uint256 quantity, bytes memory _data ) internal virtual { _mint(to, quantity); unchecked { if (to.code.length != 0) { uint256 end = _currentIndex; uint256 index = end - quantity; do { if (!_checkContractOnERC721Received(address(0), to, index++, _data)) { revert TransferToNonERC721ReceiverImplementer(); } } while (index < end); // Reentrancy protection. if (_currentIndex != end) revert(); } } } /** * @dev Equivalent to `_safeMint(to, quantity, '')`. */ function _safeMint(address to, uint256 quantity) internal virtual { _safeMint(to, quantity, ''); } // ============================================================= // BURN OPERATIONS // ============================================================= /** * @dev Equivalent to `_burn(tokenId, false)`. */ function _burn(uint256 tokenId) internal virtual { _burn(tokenId, false); } /** * @dev Destroys `tokenId`. * The approval is cleared when the token is burned. * * Requirements: * * - `tokenId` must exist. * * Emits a {Transfer} event. */ function _burn(uint256 tokenId, bool approvalCheck) internal virtual { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); address from = address(uint160(prevOwnershipPacked)); (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); if (approvalCheck) { // The nested ifs save around 20+ gas over a compound boolean condition. if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) if (!isApprovedForAll(from, _msgSenderERC721A())) revert TransferCallerNotOwnerNorApproved(); } _beforeTokenTransfers(from, address(0), tokenId, 1); // Clear approvals from the previous owner. assembly { if approvedAddress { // This is equivalent to `delete _tokenApprovals[tokenId]`. sstore(approvedAddressSlot, 0) } } // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. // Counter overflow is incredibly unrealistic as `tokenId` would have to be 2**256. unchecked { // Updates: // - `balance -= 1`. // - `numberBurned += 1`. // // We can directly decrement the balance, and increment the number burned. // This is equivalent to `packed -= 1; packed += 1 << _BITPOS_NUMBER_BURNED;`. _packedAddressData[from] += (1 << _BITPOS_NUMBER_BURNED) - 1; // Updates: // - `address` to the last owner. // - `startTimestamp` to the timestamp of burning. // - `burned` to `true`. // - `nextInitialized` to `true`. _packedOwnerships[tokenId] = _packOwnershipData( from, (_BITMASK_BURNED | _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, address(0), prevOwnershipPacked) ); // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { uint256 nextTokenId = tokenId + 1; // If the next slot's address is zero and not burned (i.e. packed value is zero). if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. _packedOwnerships[nextTokenId] = prevOwnershipPacked; } } } } emit Transfer(from, address(0), tokenId); _afterTokenTransfers(from, address(0), tokenId, 1); // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. unchecked { _burnCounter++; } } // ============================================================= // EXTRA DATA OPERATIONS // ============================================================= /** * @dev Directly sets the extra data for the ownership data `index`. */ function _setExtraDataAt(uint256 index, uint24 extraData) internal virtual { uint256 packed = _packedOwnerships[index]; if (packed == 0) revert OwnershipNotInitializedForExtraData(); uint256 extraDataCasted; // Cast `extraData` with assembly to avoid redundant masking. assembly { extraDataCasted := extraData } packed = (packed & _BITMASK_EXTRA_DATA_COMPLEMENT) | (extraDataCasted << _BITPOS_EXTRA_DATA); _packedOwnerships[index] = packed; } /** * @dev Called during each token transfer to set the 24bit `extraData` field. * Intended to be overridden by the cosumer contract. * * `previousExtraData` - the value of `extraData` before transfer. * * Calling conditions: * * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be * transferred to `to`. * - When `from` is zero, `tokenId` will be minted for `to`. * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ function _extraData( address from, address to, uint24 previousExtraData ) internal view virtual returns (uint24) {} /** * @dev Returns the next extra data for the packed ownership data. * The returned result is shifted into position. */ function _nextExtraData( address from, address to, uint256 prevOwnershipPacked ) private view returns (uint256) { uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA); return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA; } // ============================================================= // OTHER OPERATIONS // ============================================================= /** * @dev Returns the message sender (defaults to `msg.sender`). * * If you are writing GSN compatible contracts, you need to override this function. */ function _msgSenderERC721A() internal view virtual returns (address) { return msg.sender; } /** * @dev Converts a uint256 to its ASCII string decimal representation. */ function _toString(uint256 value) internal pure virtual returns (string memory str) { assembly { // The maximum value of a uint256 contains 78 digits (1 byte per digit), but // we allocate 0xa0 bytes to keep the free memory pointer 32-byte word aligned. // We will need 1 word for the trailing zeros padding, 1 word for the length, // and 3 words for a maximum of 78 digits. Total: 5 * 0x20 = 0xa0. let m := add(mload(0x40), 0xa0) // Update the free memory pointer to allocate. mstore(0x40, m) // Assign the `str` to the end. str := sub(m, 0x20) // Zeroize the slot after the string. mstore(str, 0) // Cache the end of the memory to calculate the length later. let end := str // We write the string from rightmost digit to leftmost digit. // The following is essentially a do-while loop that also handles the zero case. // prettier-ignore for { let temp := value } 1 {} { str := sub(str, 1) // Write the character to the pointer. // The ASCII index of the '0' character is 48. mstore8(str, add(48, mod(temp, 10))) // Keep dividing `temp` until zero. temp := div(temp, 10) // prettier-ignore if iszero(temp) { break } } let length := sub(end, str) // Move the pointer 32 bytes leftwards to make room for the length. str := sub(str, 0x20) // Store the length. mstore(str, length) } } } // lib/ERC721A/contracts/interfaces/IERC721A.sol // ERC721A Contracts v4.2.3 // Creator: Chiru Labs // lib/ERC721A/contracts/extensions/IERC721AQueryable.sol // ERC721A Contracts v4.2.3 // Creator: Chiru Labs /** * @dev Interface of ERC721AQueryable. */ interface IERC721AQueryable is IERC721A { /** * Invalid query range (`start` >= `stop`). */ error InvalidQueryRange(); /** * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. * * If the `tokenId` is out of bounds: * * - `addr = address(0)` * - `startTimestamp = 0` * - `burned = false` * - `extraData = 0` * * If the `tokenId` is burned: * * - `addr =
` * - `startTimestamp = ` * - `burned = true` * - `extraData = ` * * Otherwise: * * - `addr =
` * - `startTimestamp = ` * - `burned = false` * - `extraData = ` */ function explicitOwnershipOf(uint256 tokenId) external view returns (TokenOwnership memory); /** * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. * See {ERC721AQueryable-explicitOwnershipOf} */ function explicitOwnershipsOf(uint256[] memory tokenIds) external view returns (TokenOwnership[] memory); /** * @dev Returns an array of token IDs owned by `owner`, * in the range [`start`, `stop`) * (i.e. `start <= tokenId < stop`). * * This function allows for tokens to be queried if the collection * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. * * Requirements: * * - `start < stop` */ function tokensOfOwnerIn( address owner, uint256 start, uint256 stop ) external view returns (uint256[] memory); /** * @dev Returns an array of token IDs owned by `owner`. * * This function scans the ownership mapping and is O(`totalSupply`) in complexity. * It is meant to be called off-chain. * * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into * multiple smaller scans if the collection is large enough to cause * an out-of-gas error (10K collections should be fine). */ function tokensOfOwner(address owner) external view returns (uint256[] memory); } // lib/openzeppelin-contracts/contracts/access/Ownable.sol // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * By default, the owner account will be the one that deploys the contract. This * can later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _transferOwnership(_msgSender()); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // lib/openzeppelin-contracts/contracts/security/Pausable.sol // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) /** * @dev Contract module which allows children to implement an emergency stop * mechanism that can be triggered by an authorized account. * * This module is used through inheritance. It will make available the * modifiers `whenNotPaused` and `whenPaused`, which can be applied to * the functions of your contract. Note that they will not be pausable by * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { /** * @dev Emitted when the pause is triggered by `account`. */ event Paused(address account); /** * @dev Emitted when the pause is lifted by `account`. */ event Unpaused(address account); bool private _paused; /** * @dev Initializes the contract in unpaused state. */ constructor() { _paused = false; } /** * @dev Modifier to make a function callable only when the contract is not paused. * * Requirements: * * - The contract must not be paused. */ modifier whenNotPaused() { _requireNotPaused(); _; } /** * @dev Modifier to make a function callable only when the contract is paused. * * Requirements: * * - The contract must be paused. */ modifier whenPaused() { _requirePaused(); _; } /** * @dev Returns true if the contract is paused, and false otherwise. */ function paused() public view virtual returns (bool) { return _paused; } /** * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { require(!paused(), "Pausable: paused"); } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { require(paused(), "Pausable: not paused"); } /** * @dev Triggers stopped state. * * Requirements: * * - The contract must not be paused. */ function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } /** * @dev Returns to normal state. * * Requirements: * * - The contract must be paused. */ function _unpause() internal virtual whenPaused { _paused = false; emit Unpaused(_msgSender()); } } // src/lib/ENSReverseClaimer.sol interface ENS { /** * @dev Returns the address that owns the specified node. * @param node The specified node. * @return address of the owner. */ function owner(bytes32 node) external view returns (address); } interface IReverseRegistrar { /** * @dev Transfers ownership of the reverse ENS record associated with the * calling account. * @param owner The address to set as the owner of the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function claim(address owner) external returns (bytes32); /** * @dev Sets the `name()` record for the reverse ENS record associated with * the calling account. First updates the resolver to the default reverse * resolver if necessary. * @param name The name to set for this address. * @return The ENS node hash of the reverse record. */ function setName(string memory name) external returns (bytes32); } /** * @title ENSReverseClaimer * @dev This contract is used to claim reverse ENS records. */ abstract contract ENSReverseClaimer is Ownable { /// @dev The namehash of 'addr.reverse', the domain at which reverse records /// are stored in ENS. bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; /** * @dev Transfers ownership of the reverse ENS record associated with the * contract. * @param ens The ENS registry. * @param claimant The address to set as the owner of the reverse record in * ENS. * @return The ENS node hash of the reverse record. */ function claimReverseENS(ENS ens, address claimant) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).claim(claimant); } /** * @dev Sets the reverse ENS record associated with the contract. * @param ens The ENS registry. * @param name The name to set as the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function setReverseENS(ENS ens, string calldata name) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).setName(name); } } // lib/ERC721A/contracts/extensions/ERC721AQueryable.sol // ERC721A Contracts v4.2.3 // Creator: Chiru Labs /** * @title ERC721AQueryable. * * @dev ERC721A subclass with convenience query functions. */ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { /** * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. * * If the `tokenId` is out of bounds: * * - `addr = address(0)` * - `startTimestamp = 0` * - `burned = false` * - `extraData = 0` * * If the `tokenId` is burned: * * - `addr =
` * - `startTimestamp = ` * - `burned = true` * - `extraData = ` * * Otherwise: * * - `addr =
` * - `startTimestamp = ` * - `burned = false` * - `extraData = ` */ function explicitOwnershipOf(uint256 tokenId) public view virtual override returns (TokenOwnership memory) { TokenOwnership memory ownership; if (tokenId < _startTokenId() || tokenId >= _nextTokenId()) { return ownership; } ownership = _ownershipAt(tokenId); if (ownership.burned) { return ownership; } return _ownershipOf(tokenId); } /** * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. * See {ERC721AQueryable-explicitOwnershipOf} */ function explicitOwnershipsOf(uint256[] calldata tokenIds) external view virtual override returns (TokenOwnership[] memory) { unchecked { uint256 tokenIdsLength = tokenIds.length; TokenOwnership[] memory ownerships = new TokenOwnership[](tokenIdsLength); for (uint256 i; i != tokenIdsLength; ++i) { ownerships[i] = explicitOwnershipOf(tokenIds[i]); } return ownerships; } } /** * @dev Returns an array of token IDs owned by `owner`, * in the range [`start`, `stop`) * (i.e. `start <= tokenId < stop`). * * This function allows for tokens to be queried if the collection * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. * * Requirements: * * - `start < stop` */ function tokensOfOwnerIn( address owner, uint256 start, uint256 stop ) external view virtual override returns (uint256[] memory) { unchecked { if (start >= stop) revert InvalidQueryRange(); uint256 tokenIdsIdx; uint256 stopLimit = _nextTokenId(); // Set `start = max(start, _startTokenId())`. if (start < _startTokenId()) { start = _startTokenId(); } // Set `stop = min(stop, stopLimit)`. if (stop > stopLimit) { stop = stopLimit; } uint256 tokenIdsMaxLength = balanceOf(owner); // Set `tokenIdsMaxLength = min(balanceOf(owner), stop - start)`, // to cater for cases where `balanceOf(owner)` is too big. if (start < stop) { uint256 rangeLength = stop - start; if (rangeLength < tokenIdsMaxLength) { tokenIdsMaxLength = rangeLength; } } else { tokenIdsMaxLength = 0; } uint256[] memory tokenIds = new uint256[](tokenIdsMaxLength); if (tokenIdsMaxLength == 0) { return tokenIds; } // We need to call `explicitOwnershipOf(start)`, // because the slot at `start` may not be initialized. TokenOwnership memory ownership = explicitOwnershipOf(start); address currOwnershipAddr; // If the starting slot exists (i.e. not burned), initialize `currOwnershipAddr`. // `ownership.address` will not be zero, as `start` is clamped to the valid token ID range. if (!ownership.burned) { currOwnershipAddr = ownership.addr; } for (uint256 i = start; i != stop && tokenIdsIdx != tokenIdsMaxLength; ++i) { ownership = _ownershipAt(i); if (ownership.burned) { continue; } if (ownership.addr != address(0)) { currOwnershipAddr = ownership.addr; } if (currOwnershipAddr == owner) { tokenIds[tokenIdsIdx++] = i; } } // Downsize the array to fit. assembly { mstore(tokenIds, tokenIdsIdx) } return tokenIds; } } /** * @dev Returns an array of token IDs owned by `owner`. * * This function scans the ownership mapping and is O(`totalSupply`) in complexity. * It is meant to be called off-chain. * * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into * multiple smaller scans if the collection is large enough to cause * an out-of-gas error (10K collections should be fine). */ function tokensOfOwner(address owner) external view virtual override returns (uint256[] memory) { unchecked { uint256 tokenIdsIdx; address currOwnershipAddr; uint256 tokenIdsLength = balanceOf(owner); uint256[] memory tokenIds = new uint256[](tokenIdsLength); TokenOwnership memory ownership; for (uint256 i = _startTokenId(); tokenIdsIdx != tokenIdsLength; ++i) { ownership = _ownershipAt(i); if (ownership.burned) { continue; } if (ownership.addr != address(0)) { currOwnershipAddr = ownership.addr; } if (currOwnershipAddr == owner) { tokenIds[tokenIdsIdx++] = i; } } return tokenIds; } } } // src/EFPListRegistry.sol /** * @title EFPListRegistry * @author Cory Gabrielsen (cory.eth) * @custom:contributor throw; (0xthrpw.eth) * @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM * * @notice The EPF List Registry is an ERC721A contract representing ownership * of an EFP List. EFP List NFT owners may set the List Storage Location * associated with their EFP List by calling setListStorageLocation. */ contract EFPListRegistry is IEFPListRegistry, ERC721A, ERC721AQueryable, ENSReverseClaimer, Pausable { /////////////////////////////////////////////////////////////////////////// // Events /////////////////////////////////////////////////////////////////////////// /// @notice Emitted when the mint batch size is changed. event MaxMintBatchSizeChange(uint256 maxMintBatchSize); /// @notice Emitted when the mint state is changed. event MintStateChange(MintState mintState); /// @notice Emitted when the price oracle is changed. event PriceOracleChange(address priceOracle); /// @notice Emitted when the token URI provider is changed. event TokenURIProviderChange(address tokenURIProvider); /////////////////////////////////////////////////////////////////////////// // Data Structures /////////////////////////////////////////////////////////////////////////// /// @notice The state of minting. MintState private mintState = MintState.Disabled; /// @notice The maximum number of tokens that can be minted in a single batch. uint256 private maxMintBatchSize = 10000; /// @notice The price oracle. If set, the price oracle is used to determine /// the price of minting. IEFPListNFTPriceOracle private priceOracle; /// @notice The token URI provider. ITokenURIProvider public tokenURIProvider; /// @notice The list storage location associated with a token. mapping(uint256 => bytes) private tokenIdToListStorageLocation; /////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////// /// @notice Constructs a new ListRegistry and sets its name and symbol. constructor() ERC721A('EFP', 'EFP') {} ///////////////////////////////////////////////////////////////////////////// // Pausable ///////////////////////////////////////////////////////////////////////////// /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } /////////////////////////////////////////////////////////////////////////// // token uri provider getter/setter /////////////////////////////////////////////////////////////////////////// /** * @notice Sets the token URI provider. * @param tokenURIProvider_ The new token URI provider. */ function setTokenURIProvider(address tokenURIProvider_) external onlyOwner { tokenURIProvider = ITokenURIProvider(tokenURIProvider_); emit TokenURIProviderChange(tokenURIProvider_); } /** * @dev Overrides the tokenURI function to delegate the call to the * TokenURIProvider contract. This allows the tokenURI logic to be * upgradeable. * @param tokenId The token ID for which the URI is requested. * @return A string representing the token URI. */ function tokenURI(uint256 tokenId) public view override(IERC721A, ERC721A) returns (string memory) { require(address(tokenURIProvider) != address(0), 'TokenURI provider is not set'); return tokenURIProvider.tokenURI(tokenId); } /////////////////////////////////////////////////////////////////////////// // price oracle getter/setter /////////////////////////////////////////////////////////////////////////// /// @notice Fetches the price oracle. function getPriceOracle() external view returns (address) { return address(priceOracle); } /** * @notice Sets the price oracle. * @param priceOracle_ The new price oracle. */ function setPriceOracle(address priceOracle_) external whenNotPaused onlyOwner { priceOracle = IEFPListNFTPriceOracle(priceOracle_); emit PriceOracleChange(priceOracle_); } /** * @notice Fetches the price of minting a token. */ function _getPrice(uint256 quantity) internal view returns (uint256) { return (address(priceOracle) != address(0)) ? quantity == 1 ? priceOracle.getPrice() : priceOracle.getBatchPrice(quantity) : 0; } /** * @notice Withdraws Ether from the contract. * * @param recipient The address to send the Ether to. * @param amount The amount of Ether to send. * @return Whether the transfer succeeded. */ function withdraw(address payable recipient, uint256 amount) public onlyOwner returns (bool) { require(amount <= address(this).balance, 'Insufficient balance'); (bool sent,) = recipient.call{value: amount}(''); require(sent, 'Failed to send Ether'); return sent; } /////////////////////////////////////////////////////////////////////////// // Modifiers /////////////////////////////////////////////////////////////////////////// /// @notice Restrict access to the owner of a specific token. modifier onlyTokenOwner(uint256 tokenId) { require(ownerOf(tokenId) == msg.sender, 'EFP: caller is not the owner'); _; } /// @notice Restrict mint if minting is disabled OR restricted to owner && caller is not owner. modifier mintAllowed() { require(mintState != MintState.Disabled, 'EFP: minting is disabled'); require(mintState != MintState.OwnerOnly || msg.sender == owner(), 'EFP: minting is restricted to owner'); // else PublicMint allowed // else PublicBatch allowed _; } /// @notice Restrict mint if minting is disabled OR restricted to owner && caller is not owner OR restricted to public single modifier mintBatchAllowed() { require(mintState != MintState.Disabled, 'EFP: minting is disabled'); require(mintState != MintState.OwnerOnly || msg.sender == owner(), 'EFP: minting is restricted to owner'); require(mintState != MintState.PublicMint || msg.sender == owner(), 'EFP: batch minting is restricted to owner'); // else PublicBatch allowed _; } /////////////////////////////////////////////////////////////////////////// // ListStorageLocation /////////////////////////////////////////////////////////////////////////// /** * @notice Fetches the list location associated with a specific token. * @param tokenId The ID of the token. * @return The list location. */ function getListStorageLocation(uint256 tokenId) external view override returns (bytes memory) { return tokenIdToListStorageLocation[tokenId]; } /** * @notice Associates a token with a list storage location. * @param tokenId The ID of the token. * @param listStorageLocation The list storage location to be associated with the token. */ function setListStorageLocation(uint256 tokenId, bytes calldata listStorageLocation) external override whenNotPaused onlyTokenOwner(tokenId) { _setListStorageLocation(tokenId, listStorageLocation); } /** * @notice Associates a token with a list storage location. * @param tokenId The ID of the token. * @param listStorageLocation The list storage location to be associated with the token. */ function _setListStorageLocation(uint256 tokenId, bytes calldata listStorageLocation) internal { tokenIdToListStorageLocation[tokenId] = listStorageLocation; emit UpdateListStorageLocation(tokenId, listStorageLocation); } /////////////////////////////////////////////////////////////////////////// // Mint /////////////////////////////////////////////////////////////////////////// /// @notice Fetches the mint state. function getMintState() external view returns (MintState) { return mintState; } /// @notice Sets the mint state. /// @param _mintState The new mint state. function setMintState(MintState _mintState) external whenNotPaused onlyOwner { mintState = _mintState; emit MintStateChange(_mintState); } /// @notice Fetches the max mint batch size. function getMaxMintBatchSize() external view returns (uint256) { return maxMintBatchSize; } /// @notice Sets the max mint batch size. /// @param _maxMintBatchSize The new max mint batch size. function setMaxMintBatchSize(uint256 _maxMintBatchSize) external whenNotPaused onlyOwner { maxMintBatchSize = _maxMintBatchSize; emit MaxMintBatchSizeChange(_maxMintBatchSize); } /** * @notice Mints a new token. * @param listStorageLocation The list storage location to be associated with the token. */ function mint(bytes calldata listStorageLocation) external payable whenNotPaused mintAllowed { uint256 tokenId = totalSupply(); uint256 price = _getPrice(1); require(msg.value >= price, 'insufficient funds'); _safeMint(msg.sender, 1); _setListStorageLocation(tokenId, listStorageLocation); } /** * @notice Mints a new token to the given address. * @param recipient The address to mint the token to. * @param listStorageLocation The list storage location to be associated with the token. */ function mintTo(address recipient, bytes calldata listStorageLocation) external payable whenNotPaused mintAllowed { uint256 tokenId = totalSupply(); uint256 price = _getPrice(1); require(msg.value >= price, 'insufficient funds'); _safeMint(recipient, 1); _setListStorageLocation(tokenId, listStorageLocation); } /// @notice Mints a batch of new tokens. /// @param quantity The number of tokens to mint. function mintBatch(uint256 quantity) external payable whenNotPaused mintBatchAllowed { require(quantity <= maxMintBatchSize, 'batch size too big'); uint256 price = _getPrice(quantity); require(msg.value >= price, 'insufficient funds'); _safeMint(msg.sender, quantity); // leave tokenIdToListStorageLocation unset for these tokens } /// @notice Mints a batch of new tokens. /// @param recipient The address to mint the tokens to. /// @param quantity The number of tokens to mint. function mintBatchTo(address recipient, uint256 quantity) external payable whenNotPaused mintBatchAllowed { require(quantity <= maxMintBatchSize, 'batch size too big'); uint256 price = _getPrice(quantity); require(msg.value >= price, 'insufficient funds'); _safeMint(recipient, quantity); // leave tokenIdToListStorageLocation unset for these tokens } } ``` ## EFPListMinter.sol ```solc // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0 ^0.8.23; // lib/openzeppelin-contracts/contracts/utils/Context.sol // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } // src/interfaces/IEFPAccountMetadata.sol /** * @title IEFPAccountMetadata */ interface IEFPAccountMetadata { function addProxy(address proxy) external; function removeProxy(address proxy) external; function isProxy(address proxy) external view returns (bool); event UpdateAccountMetadata(address indexed addr, string key, bytes value); /** * @title Key-value Record * @notice A key-value string pair. */ struct KeyValue { string key; bytes value; } function getValue(address addr, string calldata key) external view returns (bytes memory); function setValue(string calldata key, bytes calldata value) external; function setValueForAddress(address addr, string calldata key, bytes calldata value) external; function setValues(KeyValue[] calldata records) external; function setValuesForAddress(address addr, KeyValue[] calldata records) external; } // src/interfaces/IEFPListRecords.sol /** * @title IEFPListMetadata */ interface IEFPListMetadata { event UpdateListMetadata(uint256 indexed slot, string key, bytes value); struct KeyValue { string key; bytes value; } function getMetadataValue(uint256 slot, string calldata key) external view returns (bytes memory); function getMetadataValues(uint256 slot, string[] calldata keys) external view returns (bytes[] memory); function setMetadataValue(uint256 slot, string calldata key, bytes calldata value) external; function setMetadataValues(uint256 slot, KeyValue[] calldata records) external; // List Manager Functions function claimListManager(uint256 slot) external; function claimListManagerForAddress(uint256 slot, address manager) external; function getListManager(uint256 slot) external view returns (address); function setListManager(uint256 slot, address manager) external; // List User Functions function getListUser(uint256 slot) external view returns (address); function setListUser(uint256 slot, address user) external; } /** * @title IEFPListRecords * @notice Interface for the ListRecords contract. */ interface IEFPListRecords is IEFPListMetadata { // Events event ListOp(uint256 indexed slot, bytes op); // List Operation Functions - Read function getListOpCount(uint256 slot) external view returns (uint256); function getListOp(uint256 slot, uint256 index) external view returns (bytes memory); function getListOpsInRange(uint256 slot, uint256 start, uint256 end) external view returns (bytes[] memory); function getAllListOps(uint256 slot) external view returns (bytes[] memory); // List Operation Functions - Write function applyListOp(uint256 slot, bytes calldata op) external; function applyListOps(uint256 slot, bytes[] calldata ops) external; function setMetadataValuesAndApplyListOps(uint256 slot, KeyValue[] calldata records, bytes[] calldata ops) external; } // src/interfaces/IEFPListRegistry.sol /** * @title EFPListRegistry * @notice A registry connecting token IDs with data such as managers, users, and list locations. */ interface IEFPListRegistry { /////////////////////////////////////////////////////////////////////////// // Enums /////////////////////////////////////////////////////////////////////////// enum MintState { Disabled, OwnerOnly, PublicMint, PublicBatch } /////////////////////////////////////////////////////////////////////////// // Events /////////////////////////////////////////////////////////////////////////// /// @notice Emitted when a list storage location is set event UpdateListStorageLocation(uint256 indexed tokenId, bytes listStorageLocation); /////////////////////////////////////////////////////////////////////////// // ListStorageLocation /////////////////////////////////////////////////////////////////////////// function getListStorageLocation(uint256 tokenId) external view returns (bytes memory); function setListStorageLocation(uint256 tokenId, bytes calldata listStorageLocation) external; /////////////////////////////////////////////////////////////////////////// // Mint /////////////////////////////////////////////////////////////////////////// /// @notice Fetches the mint state. function getMintState() external view returns (MintState); /// @notice Sets the mint state. /// @param _mintState The new mint state. function setMintState(MintState _mintState) external; /// @notice Fetches the max mint batch size. function getMaxMintBatchSize() external view returns (uint256); /// @notice Sets the max mint batch size. /// @param _maxMintBatchSize The new max mint batch size. function setMaxMintBatchSize(uint256 _maxMintBatchSize) external; /// @notice Mints a new token. function mint(bytes calldata listStorageLocation) external payable; /** * @notice Mints a new token to the given address. * @param recipient The address to mint the token to. */ function mintTo(address recipient, bytes calldata listStorageLocation) external payable; /// @notice Mints a new token to the given address. function mintBatch(uint256 quantity) external payable; /// @notice Mints a new token to the given address. function mintBatchTo(address recipient, uint256 quantity) external payable; } // lib/openzeppelin-contracts/contracts/access/Ownable.sol // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * By default, the owner account will be the one that deploys the contract. This * can later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _transferOwnership(_msgSender()); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // lib/openzeppelin-contracts/contracts/security/Pausable.sol // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) /** * @dev Contract module which allows children to implement an emergency stop * mechanism that can be triggered by an authorized account. * * This module is used through inheritance. It will make available the * modifiers `whenNotPaused` and `whenPaused`, which can be applied to * the functions of your contract. Note that they will not be pausable by * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { /** * @dev Emitted when the pause is triggered by `account`. */ event Paused(address account); /** * @dev Emitted when the pause is lifted by `account`. */ event Unpaused(address account); bool private _paused; /** * @dev Initializes the contract in unpaused state. */ constructor() { _paused = false; } /** * @dev Modifier to make a function callable only when the contract is not paused. * * Requirements: * * - The contract must not be paused. */ modifier whenNotPaused() { _requireNotPaused(); _; } /** * @dev Modifier to make a function callable only when the contract is paused. * * Requirements: * * - The contract must be paused. */ modifier whenPaused() { _requirePaused(); _; } /** * @dev Returns true if the contract is paused, and false otherwise. */ function paused() public view virtual returns (bool) { return _paused; } /** * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { require(!paused(), "Pausable: paused"); } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { require(paused(), "Pausable: not paused"); } /** * @dev Triggers stopped state. * * Requirements: * * - The contract must not be paused. */ function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } /** * @dev Returns to normal state. * * Requirements: * * - The contract must be paused. */ function _unpause() internal virtual whenPaused { _paused = false; emit Unpaused(_msgSender()); } } // src/lib/ENSReverseClaimer.sol interface ENS { /** * @dev Returns the address that owns the specified node. * @param node The specified node. * @return address of the owner. */ function owner(bytes32 node) external view returns (address); } interface IReverseRegistrar { /** * @dev Transfers ownership of the reverse ENS record associated with the * calling account. * @param owner The address to set as the owner of the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function claim(address owner) external returns (bytes32); /** * @dev Sets the `name()` record for the reverse ENS record associated with * the calling account. First updates the resolver to the default reverse * resolver if necessary. * @param name The name to set for this address. * @return The ENS node hash of the reverse record. */ function setName(string memory name) external returns (bytes32); } /** * @title ENSReverseClaimer * @dev This contract is used to claim reverse ENS records. */ abstract contract ENSReverseClaimer is Ownable { /// @dev The namehash of 'addr.reverse', the domain at which reverse records /// are stored in ENS. bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; /** * @dev Transfers ownership of the reverse ENS record associated with the * contract. * @param ens The ENS registry. * @param claimant The address to set as the owner of the reverse record in * ENS. * @return The ENS node hash of the reverse record. */ function claimReverseENS(ENS ens, address claimant) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).claim(claimant); } /** * @dev Sets the reverse ENS record associated with the contract. * @param ens The ENS registry. * @param name The name to set as the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function setReverseENS(ENS ens, string calldata name) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).setName(name); } } // src/EFPListMinter.sol interface IEFPListRegistry_ERC721 is IEFPListRegistry { function ownerOf(uint256 tokenId) external view returns (address); function totalSupply() external view returns (uint256); } /** * @title EFPListMetadata * @author Cory Gabrielsen (cory.eth) * @custom:contributor throw; (0xthrpw.eth) * @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM * * @notice This contract mints and assigns primary lists to users, and sets * EFP List metadata. */ contract EFPListMinter is ENSReverseClaimer, Pausable { IEFPListRegistry_ERC721 public registry; IEFPAccountMetadata public accountMetadata; IEFPListRecords public listRecordsL1; constructor(address _registryAddress, address _accountMetadataAddress, address _listRecordsL1) { registry = IEFPListRegistry_ERC721(_registryAddress); accountMetadata = IEFPAccountMetadata(_accountMetadataAddress); listRecordsL1 = IEFPListRecords(_listRecordsL1); } ///////////////////////////////////////////////////////////////////////////// // Pausable ///////////////////////////////////////////////////////////////////////////// /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ///////////////////////////////////////////////////////////////////////////// // minting ///////////////////////////////////////////////////////////////////////////// /** * @dev Decode a list storage location with no metadata. * @param listStorageLocation The storage location of the list. * @return slot The slot of the list. * @return contractAddress The contract address of the list. */ function decodeL1ListStorageLocation(bytes calldata listStorageLocation) internal pure returns (uint256, address) { // the list storage location is // - version (1 byte) // - list storate location type (1 byte) // - chain id (32 bytes) // - contract address (20 bytes) // - slot (32 bytes) require(listStorageLocation.length == 1 + 1 + 32 + 20 + 32, 'EFPListMinter: invalid list storage location'); require(listStorageLocation[0] == 0x01, 'EFPListMinter: invalid list storage location version'); require(listStorageLocation[1] == 0x01, 'EFPListMinter: invalid list storage location type'); address contractAddress = _bytesToAddress(listStorageLocation, 34); uint256 slot = _bytesToUint(listStorageLocation, 54); return (slot, contractAddress); } /** * @dev Mint a primary list. * @param listStorageLocation The storage location of the list. */ function easyMint(bytes calldata listStorageLocation) public payable whenNotPaused { // validate the list storage location (uint256 slot, address recordsContract) = decodeL1ListStorageLocation(listStorageLocation); uint256 tokenId = registry.totalSupply(); registry.mintTo{value: msg.value}(msg.sender, listStorageLocation); _setDefaultListForAccount(msg.sender, tokenId); if (recordsContract == address(listRecordsL1)) { listRecordsL1.setListUser(slot, msg.sender); listRecordsL1.setListManager(slot, msg.sender); } } /** * @dev Mint a primary list to a specific address. * @param to The address to mint the list to. * @param listStorageLocation The storage location of the list. */ function easyMintTo(address to, bytes calldata listStorageLocation) public payable whenNotPaused { // validate the list storage location (uint256 slot, address recordsContract) = decodeL1ListStorageLocation(listStorageLocation); uint256 tokenId = registry.totalSupply(); registry.mintTo{value: msg.value}(to, listStorageLocation); _setDefaultListForAccount(msg.sender, tokenId); if (recordsContract == address(listRecordsL1)) { listRecordsL1.setListUser(slot, msg.sender); listRecordsL1.setListManager(slot, msg.sender); } } /** * @dev Mint a primary list without metadata. * @param listStorageLocation The storage location of the list. */ function mintPrimaryListNoMeta(bytes calldata listStorageLocation) public payable whenNotPaused { // validate the list storage location decodeL1ListStorageLocation(listStorageLocation); uint256 tokenId = registry.totalSupply(); _setDefaultListForAccount(msg.sender, tokenId); registry.mintTo{value: msg.value}(msg.sender, listStorageLocation); } /** * @dev Mint a primary list without metadata to a specific address. * @param listStorageLocation The storage location of the list. */ function mintNoMeta(bytes calldata listStorageLocation) public payable whenNotPaused { // validate the list storage location decodeL1ListStorageLocation(listStorageLocation); registry.mintTo{value: msg.value}(msg.sender, listStorageLocation); } /** * @dev Mint a primary list without metadata to a specific address. * @param to The address to mint the list to. * @param listStorageLocation The storage location of the list. */ function mintToNoMeta(address to, bytes calldata listStorageLocation) public payable whenNotPaused { // validate the list storage location decodeL1ListStorageLocation(listStorageLocation); registry.mintTo{value: msg.value}(to, listStorageLocation); } /** * @dev Set the default list for an account. * @param to The address to set the default list for. * @param tokenId The token ID of the list. */ function _setDefaultListForAccount(address to, uint256 tokenId) internal { accountMetadata.setValueForAddress(to, 'primary-list', abi.encodePacked(tokenId)); } function _getChainId() internal view returns (uint256) { uint256 id; assembly { id := chainid() } return id; } // Generalized function to convert bytes to uint256 with a given offset function _bytesToUint(bytes memory data, uint256 offset) internal pure returns (uint256) { require(data.length >= offset + 32, 'Data too short'); uint256 value; assembly { value := mload(add(data, add(32, offset))) } return value; } // Helper function to convert bytes to address with a given offset function _bytesToAddress(bytes memory data, uint256 offset) internal pure returns (address addr) { require(data.length >= offset + 20, 'Data too short'); assembly { // Extract 20 bytes from the specified offset addr := mload(add(add(data, 20), offset)) // clear the 12 least significant bits of the address addr := and(addr, 0x000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) } return addr; } } ``` ## EFPAccountMetadata.sol ```solc // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0 ^0.8.23; // lib/openzeppelin-contracts/contracts/utils/Context.sol // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } // src/interfaces/IEFPAccountMetadata.sol /** * @title IEFPAccountMetadata */ interface IEFPAccountMetadata { function addProxy(address proxy) external; function removeProxy(address proxy) external; function isProxy(address proxy) external view returns (bool); event UpdateAccountMetadata(address indexed addr, string key, bytes value); /** * @title Key-value Record * @notice A key-value string pair. */ struct KeyValue { string key; bytes value; } function getValue(address addr, string calldata key) external view returns (bytes memory); function setValue(string calldata key, bytes calldata value) external; function setValueForAddress(address addr, string calldata key, bytes calldata value) external; function setValues(KeyValue[] calldata records) external; function setValuesForAddress(address addr, KeyValue[] calldata records) external; } // lib/openzeppelin-contracts/contracts/access/Ownable.sol // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * By default, the owner account will be the one that deploys the contract. This * can later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _transferOwnership(_msgSender()); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // lib/openzeppelin-contracts/contracts/security/Pausable.sol // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) /** * @dev Contract module which allows children to implement an emergency stop * mechanism that can be triggered by an authorized account. * * This module is used through inheritance. It will make available the * modifiers `whenNotPaused` and `whenPaused`, which can be applied to * the functions of your contract. Note that they will not be pausable by * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { /** * @dev Emitted when the pause is triggered by `account`. */ event Paused(address account); /** * @dev Emitted when the pause is lifted by `account`. */ event Unpaused(address account); bool private _paused; /** * @dev Initializes the contract in unpaused state. */ constructor() { _paused = false; } /** * @dev Modifier to make a function callable only when the contract is not paused. * * Requirements: * * - The contract must not be paused. */ modifier whenNotPaused() { _requireNotPaused(); _; } /** * @dev Modifier to make a function callable only when the contract is paused. * * Requirements: * * - The contract must be paused. */ modifier whenPaused() { _requirePaused(); _; } /** * @dev Returns true if the contract is paused, and false otherwise. */ function paused() public view virtual returns (bool) { return _paused; } /** * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { require(!paused(), "Pausable: paused"); } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { require(paused(), "Pausable: not paused"); } /** * @dev Triggers stopped state. * * Requirements: * * - The contract must not be paused. */ function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } /** * @dev Returns to normal state. * * Requirements: * * - The contract must be paused. */ function _unpause() internal virtual whenPaused { _paused = false; emit Unpaused(_msgSender()); } } // src/lib/ENSReverseClaimer.sol interface ENS { /** * @dev Returns the address that owns the specified node. * @param node The specified node. * @return address of the owner. */ function owner(bytes32 node) external view returns (address); } interface IReverseRegistrar { /** * @dev Transfers ownership of the reverse ENS record associated with the * calling account. * @param owner The address to set as the owner of the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function claim(address owner) external returns (bytes32); /** * @dev Sets the `name()` record for the reverse ENS record associated with * the calling account. First updates the resolver to the default reverse * resolver if necessary. * @param name The name to set for this address. * @return The ENS node hash of the reverse record. */ function setName(string memory name) external returns (bytes32); } /** * @title ENSReverseClaimer * @dev This contract is used to claim reverse ENS records. */ abstract contract ENSReverseClaimer is Ownable { /// @dev The namehash of 'addr.reverse', the domain at which reverse records /// are stored in ENS. bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; /** * @dev Transfers ownership of the reverse ENS record associated with the * contract. * @param ens The ENS registry. * @param claimant The address to set as the owner of the reverse record in * ENS. * @return The ENS node hash of the reverse record. */ function claimReverseENS(ENS ens, address claimant) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).claim(claimant); } /** * @dev Sets the reverse ENS record associated with the contract. * @param ens The ENS registry. * @param name The name to set as the reverse record in ENS. * @return The ENS node hash of the reverse record. */ function setReverseENS(ENS ens, string calldata name) external onlyOwner returns (bytes32) { return IReverseRegistrar(ens.owner(ADDR_REVERSE_NODE)).setName(name); } } // src/EFPAccountMetadata.sol /** * @title EFPListMetadata * @author Cory Gabrielsen (cory.eth) * @custom:contributor throw; (0xthrpw.eth) * @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM * * @notice This contract stores records as key/value pairs, by 32-byte * EFP List Token ID. */ contract EFPAccountMetadata is IEFPAccountMetadata, ENSReverseClaimer, Pausable { event ProxyAdded(address proxy); event ProxyRemoved(address proxy); /// @dev The key-value set for each address mapping(address => mapping(string => bytes)) private values; mapping(address => bool) private proxies; ///////////////////////////////////////////////////////////////////////////// // Pausable ///////////////////////////////////////////////////////////////////////////// /** * @dev Pauses the contract. Can only be called by the contract owner. */ function pause() public onlyOwner { _pause(); } /** * @dev Unpauses the contract. Can only be called by the contract owner. */ function unpause() public onlyOwner { _unpause(); } ///////////////////////////////////////////////////////////////////////////// // add/remove proxy ///////////////////////////////////////////////////////////////////////////// /** * @dev Add proxy address. * @param proxy The proxy address. */ function addProxy(address proxy) external whenNotPaused onlyOwner { proxies[proxy] = true; emit ProxyAdded(proxy); } /** * @dev Remove proxy address. * @param proxy The proxy address. */ function removeProxy(address proxy) external whenNotPaused onlyOwner { proxies[proxy] = false; emit ProxyRemoved(proxy); } /** * @dev Check if the address is a proxy. * @param proxy The address to check. * @return True if the address is a proxy, false otherwise. */ function isProxy(address proxy) external view returns (bool) { return proxies[proxy]; } ///////////////////////////////////////////////////////////////////////////// // Modifier ///////////////////////////////////////////////////////////////////////////// modifier onlyCallerOrProxy(address addr) { require(addr == msg.sender || proxies[msg.sender], 'not allowed'); _; } ///////////////////////////////////////////////////////////////////////////// // Getters ///////////////////////////////////////////////////////////////////////////// /** * @dev Retrieves value for address and key. * @param addr The address to query. * @param key The key to query. * @return The associated value. */ function getValue(address addr, string calldata key) external view returns (bytes memory) { return values[addr][key]; } /** * @dev Retrieves values for address and keys. * @param addr The address to query. * @param keys The keys to query. * @return The associated values. */ function getValues(address addr, string[] calldata keys) external view returns (bytes[] memory) { uint256 length = keys.length; bytes[] memory result = new bytes[](length); for (uint256 i = 0; i < length;) { string calldata key = keys[i]; result[i] = values[addr][key]; unchecked { ++i; } } return result; } ///////////////////////////////////////////////////////////////////////////// // Setters ///////////////////////////////////////////////////////////////////////////// /** * @dev Sets records for address with the unique key key to value, * overwriting anything previously stored for address and key. To clear a * field, set it to the empty string. * @param addr The address to update. * @param key The key to set. * @param value The value to set. */ function _setValue(address addr, string calldata key, bytes calldata value) internal { values[addr][key] = value; emit UpdateAccountMetadata(addr, key, value); } /** * @dev Sets records for caller address with the unique key key to value, * overwriting anything previously stored for address and key. To clear a * field, set it to the empty string. * Only callable by the token owner. * @param key The key to set. * @param value The value to set. */ function setValue(string calldata key, bytes calldata value) external whenNotPaused { _setValue(msg.sender, key, value); } /** * @dev Sets records for address with the unique key key to value, * overwriting anything previously stored for address and key. To clear a * field, set it to the empty string. * Only callable by the token owner. * @param addr The address to update. * @param key The key to set. * @param value The value to set. */ function setValueForAddress(address addr, string calldata key, bytes calldata value) external onlyCallerOrProxy(addr) whenNotPaused { _setValue(addr, key, value); } /** * @dev Sets an array of records for the caller address. Each record is a key/value pair. * Only callable by the token owner. * @param records The records to set. */ function setValues(KeyValue[] calldata records) external whenNotPaused { uint256 length = records.length; for (uint256 i = 0; i < length;) { KeyValue calldata record = records[i]; _setValue(msg.sender, record.key, record.value); unchecked { ++i; } } } /** * @dev Sets an array of records for a address. Each record is a key/value pair. * Only callable by the token owner. * @param addr The address to update. * @param records The records to set. */ function setValuesForAddress(address addr, KeyValue[] calldata records) external whenNotPaused onlyCallerOrProxy(addr) { uint256 length = records.length; for (uint256 i = 0; i < length;) { KeyValue calldata record = records[i]; _setValue(addr, record.key, record.value); unchecked { ++i; } } } } ``` ## TokenURIProvider.sol ```solc // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0 ^0.8.23; // lib/openzeppelin-contracts/contracts/utils/Context.sol // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } // src/interfaces/ITokenURIProvider.sol interface ITokenURIProvider { function tokenURI(uint256 tokenId) external view returns (string memory); } // lib/openzeppelin-contracts/contracts/utils/math/Math.sol // OpenZeppelin Contracts (last updated v4.9.0) (utils/math/Math.sol) /** * @dev Standard math utilities missing in the Solidity language. */ library Math { enum Rounding { Down, // Toward negative infinity Up, // Toward infinity Zero // Toward zero } /** * @dev Returns the largest of two numbers. */ function max(uint256 a, uint256 b) internal pure returns (uint256) { return a > b ? a : b; } /** * @dev Returns the smallest of two numbers. */ function min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } /** * @dev Returns the average of two numbers. The result is rounded towards * zero. */ function average(uint256 a, uint256 b) internal pure returns (uint256) { // (a + b) / 2 can overflow. return (a & b) + (a ^ b) / 2; } /** * @dev Returns the ceiling of the division of two numbers. * * This differs from standard division with `/` in that it rounds up instead * of rounding down. */ function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { // (a + b - 1) / b can overflow on addition, so we distribute. return a == 0 ? 0 : (a - 1) / b + 1; } /** * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) * with further edits by Uniswap Labs also under MIT license. */ function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { unchecked { // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 // variables such that product = prod1 * 2^256 + prod0. uint256 prod0; // Least significant 256 bits of the product uint256 prod1; // Most significant 256 bits of the product assembly { let mm := mulmod(x, y, not(0)) prod0 := mul(x, y) prod1 := sub(sub(mm, prod0), lt(mm, prod0)) } // Handle non-overflow cases, 256 by 256 division. if (prod1 == 0) { // Solidity will revert if denominator == 0, unlike the div opcode on its own. // The surrounding unchecked block does not change this fact. // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. return prod0 / denominator; } // Make sure the result is less than 2^256. Also prevents denominator == 0. require(denominator > prod1, "Math: mulDiv overflow"); /////////////////////////////////////////////// // 512 by 256 division. /////////////////////////////////////////////// // Make division exact by subtracting the remainder from [prod1 prod0]. uint256 remainder; assembly { // Compute remainder using mulmod. remainder := mulmod(x, y, denominator) // Subtract 256 bit number from 512 bit number. prod1 := sub(prod1, gt(remainder, prod0)) prod0 := sub(prod0, remainder) } // Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1. // See https://cs.stackexchange.com/q/138556/92363. // Does not overflow because the denominator cannot be zero at this stage in the function. uint256 twos = denominator & (~denominator + 1); assembly { // Divide denominator by twos. denominator := div(denominator, twos) // Divide [prod1 prod0] by twos. prod0 := div(prod0, twos) // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one. twos := add(div(sub(0, twos), twos), 1) } // Shift in bits from prod1 into prod0. prod0 |= prod1 * twos; // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for // four bits. That is, denominator * inv = 1 mod 2^4. uint256 inverse = (3 * denominator) ^ 2; // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works // in modular arithmetic, doubling the correct bits in each step. inverse *= 2 - denominator * inverse; // inverse mod 2^8 inverse *= 2 - denominator * inverse; // inverse mod 2^16 inverse *= 2 - denominator * inverse; // inverse mod 2^32 inverse *= 2 - denominator * inverse; // inverse mod 2^64 inverse *= 2 - denominator * inverse; // inverse mod 2^128 inverse *= 2 - denominator * inverse; // inverse mod 2^256 // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1 // is no longer required. result = prod0 * inverse; return result; } } /** * @notice Calculates x * y / denominator with full precision, following the selected rounding direction. */ function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { uint256 result = mulDiv(x, y, denominator); if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) { result += 1; } return result; } /** * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down. * * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11). */ function sqrt(uint256 a) internal pure returns (uint256) { if (a == 0) { return 0; } // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target. // // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`. // // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)` // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))` // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)` // // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit. uint256 result = 1 << (log2(a) >> 1); // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128, // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision // into the expected uint128 result. unchecked { result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; result = (result + a / result) >> 1; return min(result, a / result); } } /** * @notice Calculates sqrt(a), following the selected rounding direction. */ function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = sqrt(a); return result + (rounding == Rounding.Up && result * result < a ? 1 : 0); } } /** * @dev Return the log in base 2, rounded down, of a positive value. * Returns 0 if given 0. */ function log2(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { if (value >> 128 > 0) { value >>= 128; result += 128; } if (value >> 64 > 0) { value >>= 64; result += 64; } if (value >> 32 > 0) { value >>= 32; result += 32; } if (value >> 16 > 0) { value >>= 16; result += 16; } if (value >> 8 > 0) { value >>= 8; result += 8; } if (value >> 4 > 0) { value >>= 4; result += 4; } if (value >> 2 > 0) { value >>= 2; result += 2; } if (value >> 1 > 0) { result += 1; } } return result; } /** * @dev Return the log in base 2, following the selected rounding direction, of a positive value. * Returns 0 if given 0. */ function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = log2(value); return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0); } } /** * @dev Return the log in base 10, rounded down, of a positive value. * Returns 0 if given 0. */ function log10(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { if (value >= 10 ** 64) { value /= 10 ** 64; result += 64; } if (value >= 10 ** 32) { value /= 10 ** 32; result += 32; } if (value >= 10 ** 16) { value /= 10 ** 16; result += 16; } if (value >= 10 ** 8) { value /= 10 ** 8; result += 8; } if (value >= 10 ** 4) { value /= 10 ** 4; result += 4; } if (value >= 10 ** 2) { value /= 10 ** 2; result += 2; } if (value >= 10 ** 1) { result += 1; } } return result; } /** * @dev Return the log in base 10, following the selected rounding direction, of a positive value. * Returns 0 if given 0. */ function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = log10(value); return result + (rounding == Rounding.Up && 10 ** result < value ? 1 : 0); } } /** * @dev Return the log in base 256, rounded down, of a positive value. * Returns 0 if given 0. * * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. */ function log256(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { if (value >> 128 > 0) { value >>= 128; result += 16; } if (value >> 64 > 0) { value >>= 64; result += 8; } if (value >> 32 > 0) { value >>= 32; result += 4; } if (value >> 16 > 0) { value >>= 16; result += 2; } if (value >> 8 > 0) { result += 1; } } return result; } /** * @dev Return the log in base 256, following the selected rounding direction, of a positive value. * Returns 0 if given 0. */ function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { unchecked { uint256 result = log256(value); return result + (rounding == Rounding.Up && 1 << (result << 3) < value ? 1 : 0); } } } // lib/openzeppelin-contracts/contracts/utils/math/SignedMath.sol // OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SignedMath.sol) /** * @dev Standard signed math utilities missing in the Solidity language. */ library SignedMath { /** * @dev Returns the largest of two signed numbers. */ function max(int256 a, int256 b) internal pure returns (int256) { return a > b ? a : b; } /** * @dev Returns the smallest of two signed numbers. */ function min(int256 a, int256 b) internal pure returns (int256) { return a < b ? a : b; } /** * @dev Returns the average of two signed numbers without overflow. * The result is rounded towards zero. */ function average(int256 a, int256 b) internal pure returns (int256) { // Formula from the book "Hacker's Delight" int256 x = (a & b) + ((a ^ b) >> 1); return x + (int256(uint256(x) >> 255) & (a ^ b)); } /** * @dev Returns the absolute unsigned value of a signed value. */ function abs(int256 n) internal pure returns (uint256) { unchecked { // must be unchecked in order to support `n = type(int256).min` return uint256(n >= 0 ? n : -n); } } } // lib/openzeppelin-contracts/contracts/access/Ownable.sol // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * By default, the owner account will be the one that deploys the contract. This * can later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor() { _transferOwnership(_msgSender()); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // lib/openzeppelin-contracts/contracts/utils/Strings.sol // OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol) /** * @dev String operations. */ library Strings { bytes16 private constant _SYMBOLS = "0123456789abcdef"; uint8 private constant _ADDRESS_LENGTH = 20; /** * @dev Converts a `uint256` to its ASCII `string` decimal representation. */ function toString(uint256 value) internal pure returns (string memory) { unchecked { uint256 length = Math.log10(value) + 1; string memory buffer = new string(length); uint256 ptr; /// @solidity memory-safe-assembly assembly { ptr := add(buffer, add(32, length)) } while (true) { ptr--; /// @solidity memory-safe-assembly assembly { mstore8(ptr, byte(mod(value, 10), _SYMBOLS)) } value /= 10; if (value == 0) break; } return buffer; } } /** * @dev Converts a `int256` to its ASCII `string` decimal representation. */ function toString(int256 value) internal pure returns (string memory) { return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMath.abs(value)))); } /** * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. */ function toHexString(uint256 value) internal pure returns (string memory) { unchecked { return toHexString(value, Math.log256(value) + 1); } } /** * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. */ function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { bytes memory buffer = new bytes(2 * length + 2); buffer[0] = "0"; buffer[1] = "x"; for (uint256 i = 2 * length + 1; i > 1; --i) { buffer[i] = _SYMBOLS[value & 0xf]; value >>= 4; } require(value == 0, "Strings: hex length insufficient"); return string(buffer); } /** * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. */ function toHexString(address addr) internal pure returns (string memory) { return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); } /** * @dev Returns true if the two strings are equal. */ function equal(string memory a, string memory b) internal pure returns (bool) { return keccak256(bytes(a)) == keccak256(bytes(b)); } } // src/TokenURIProvider.sol /** * @title TokenURIProvider * @author throw; (0xthrpw.eth) * @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM * * @notice This contract allows the owner to set a base URI for token URIs and * returns the token URI for a given token ID. Separating this functionality allows * the logic for generating token URIs to be upgradable. */ contract TokenURIProvider is ITokenURIProvider, Ownable { string private _baseURI; using Strings for uint256; /** * @dev Constructor * @param baseURI The base URI for token URIs */ constructor(string memory baseURI) { _baseURI = baseURI; } /** * @dev Returns the token URI for a given token ID * @param tokenId The token ID * @return The token URI */ function tokenURI(uint256 tokenId) external view override returns (string memory) { return string(abi.encodePacked(_baseURI, tokenId.toString())); } /** * @dev Sets the base URI for token URIs * @param baseURI The new base URI */ function setBaseURI(string memory baseURI) external onlyOwner { _baseURI = baseURI; } } ```