# Aggregated Ethereum Identity Context - Full
> Updated at: 22:35 05/28/25
# docs.efp.app llms-full.txt
> Ethereum Follow Protocol (EFP) is an onchain social graph protocol for Ethereum accounts.
**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) | **Download** | **Download** |
| Logo With Text (dark) | **Download** | **Download** |
| Logo Only | **Download** | **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.

### 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.
[](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.

Once all sections display 'Ready to be deployed' the 'Deploy' button at the bottom will be unlocked and you can click it to proceed.

### 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.

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`


### 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.

### 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.

### 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.
---

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.

### 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;
}
```
---

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

`/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

`/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

`/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;
}
}
```
---
# ethidentitykit.com llms-full.txt
> Ethereum Identity Kit facilitates the integration of Ethereum identity features into applications via a React component library, providing developers with tools and documentation for user profile management and on-chain transactions.
# useFollowButton
The `useFollowButton` hook manages the state and actions for a follow button component. It determines the current follow state between a `lookupAddress` and a `connectedAddress`, and provides functions to handle follow, unfollow, block, and mute actions.
### Add to your project
```tsx copy
import { useFollowButton } from 'ethereum-identity-kit'
export default function FollowButtonComponent() {
const { buttonText, buttonState, handleAction, isLoading, disableHover, setDisableHover } = useFollowButton({
lookupAddress: '0x1234...abcd',
connectedAddress: '0xabcd...1234',
})
// Create your own loading states
if (isLoading) return
Loading...
return (
)
}
```
## Parameters
| Parameter | Description | Required | Default Value |
| ------------------ | ------------------------------------------------------------------------- | -------- | ------------- |
| `lookupAddress` | Ethereum address to manage the follow state for. | Yes | - |
| `connectedAddress` | Ethereum address of the currently connected user. | No | - |
| `selectedList` | List number to manage the follow state for; defaults to the primary list. | No | - |
## Return Values
| Return Value | Description |
| ----------------- | -------------------------------------------------------------------------------------------- |
| `buttonText` | The text to display on the follow button, indicating the current or pending follow state. |
| `buttonState` | The current state of the button, such as 'Follow', 'Following', 'Blocked', etc. |
| `handleAction` | Function to handle the button click action, updating the follow state accordingly. |
| `isLoading` | Boolean indicating if the follow state is currently loading. |
| `pendingState` | The pending state of the follow action, if any (e.g., 'Pending Following', 'Pending Block'). |
| `disableHover` | Boolean indicating if hover effects should be disabled. (it is disabled after a click) |
| `setDisableHover` | Function to set the `disableHover` state. |
### Notes
- Ensure that the `lookupAddress` and `connectedAddress` are valid Ethereum addresses.
- The `handleAction` function manages the follow, unfollow, block, and mute actions based on the current state.
- The `disableHover` state can be used to control hover effects on the button during certain actions as the state is set to true every time the button is clicked.
---
# useProfileDetails
The `useProfileDetails` hook fetches and manages the profile details for a given Ethereum address or ENS name, including ENS data and primary list information.
### Add to your project
```tsx copy
import { useProfileDetails } from 'ethereum-identity-kit'
export default function ProfileComponent() {
const { ens, address, primaryList, detailsLoading, refreshProfileDetails } = useProfileDetails({
addressOrName: 'vitalik.eth',
})
if (detailsLoading) return
Loading...
// Create your own loading states
// --- Your component code here ---
}
```
## Parameters
| Parameter | Description | Required | Default Value |
| ----------------------- | -------------------------------------------------------------------------------- | -------- | ------------- |
| `addressOrName` | Ethereum Address or ENS name to fetch profile details for. | Yes | - |
| `list` | List number to fetch profile details for; overrides `addressOrName` if provided. | No | - |
| `prefetchedData` | Prefetched profile data to use initially. | No | - |
| `refetchPrefetchedData` | Function to refetch prefetched profile data. | No | - |
## Return Values
| Return Value | Description |
| ----------------------- | ---------------------------------------------------------------- |
| `ens` | ENS data for the profile, including name and records. |
| `address` | Ethereum address associated with the profile. |
| `primaryList` | Primary list number associated with the profile. |
| `detailsLoading` | Boolean indicating if the profile details are currently loading. |
| `refreshProfileDetails` | Function to manually refresh the profile details. |
---
# useFollowingState
The `useFollowingState` hook fetches and manages the following state between a given Ethereum address or ENS name and the connected user, indicating if the user follows, blocks, or mutes the address.
### Add to your project
```tsx copy
import { useFollowingState } from 'ethereum-identity-kit'
export default function FollowingStateComponent() {
const { state, isLoading } = useFollowingState({
lookupAddressOrName: 'vitalik.eth',
connectedAddress: '0x1234...abcd',
})
// Create your own loading states
if (isLoading) return
Loading...
// --- Your component code here ---
}
```
## Parameters
| Parameter | Description | Required | Default Value |
| --------------------- | ------------------------------------------------------------------------------------------ | -------- | ------------- |
| `lookupAddressOrName` | Ethereum Address or ENS name to check the following state for. | Yes | - |
| `connectedAddress` | Ethereum address of the currently connected user. | Yes | - |
| `list` | List number to check the following state for; overrides `lookupAddressOrName` if provided. | No | - |
## Return Values
| Return Value | Description |
| ------------ | ---------------------------------------------------------------------------------- |
| `state` | The following state, indicating if the user follows, blocks, or mutes the address. |
| `isLoading` | Boolean indicating if the following state is currently loading. |
---
# useFollowerState
The `useFollowerState` hook fetches and manages the follower state between a given Ethereum address or ENS name and the connected user, indicating if the user is followed, blocked, or muted by the address.
### Add to your project
```tsx copy
import { useFollowerState } from 'ethereum-identity-kit'
export default function FollowerStateComponent() {
const { followState, followerTag, isFollowerStateLoading } = useFollowerState({
addressOrName: 'vitalik.eth',
connectedAddress: '0x1234...abcd',
})
// Create your own loading states
if (isFollowerStateLoading) return
Loading...
// --- Your component code here ---
}
```
## Parameters
| Parameter | Description | Required | Default Value |
| ------------------ | ----------------------------------------------------------------------------------- | -------- | ------------- |
| `addressOrName` | Ethereum Address or ENS name to check the follower state for. | Yes | - |
| `connectedAddress` | Ethereum address of the currently connected user. | Yes | - |
| `list` | List number to check the follower state for; overrides `addressOrName` if provided. | No | - |
## Return Values
| Return Value | Description |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| `followState` | The follower state, indicating if the user is followed, blocked, or muted by the address. |
| `followerTag` | Object containing text and className for displaying the follower state. |
| `isFollowerStateLoading` | Boolean indicating if the follower state is currently loading. |
---
# useProfileStats
The `useProfileStats` hook fetches and manages the follower and following statistics for a given Ethereum address or ENS name.
### Add to your project
```tsx copy
import { useProfileStats } from 'ethereum-identity-kit'
export default function StatsComponent() {
const { followers, following, statsLoading, refreshProfileStats } = useProfileStats({
addressOrName: 'vitalik.eth',
})
// Create your own loading states
if (statsLoading) return
Loading...
// --- Your component code here ---
}
```
## Parameters
| Parameter | Description | Required | Default Value |
| ----------------------- | ------------------------------------------------------------------------------ | -------- | ------------- |
| `addressOrName` | Ethereum Address or ENS name to fetch profile stats for. | Yes | - |
| `list` | List number to fetch profile stats for; overrides `addressOrName` if provided. | No | - |
| `prefetchedData` | Prefetched stats data to use initially. | No | - |
| `refetchPrefetchedData` | Function to refetch prefetched stats data. | No | - |
## Return Values
| Return Value | Description |
| --------------------- | -------------------------------------------------------------- |
| `followers` | Number of followers for the profile. |
| `following` | Number of accounts the profile is following. |
| `statsLoading` | Boolean indicating if the profile stats are currently loading. |
| `refreshProfileStats` | Function to manually refresh the profile stats. |
---
# useTransactions
The `useTransactions` hook provides access to the transaction context, allowing components to manage and interact with on-chain transactions. It offers state management and utility functions for handling transaction modals, batching, and more.
### Add to your project
```tsx copy
import { useTransactions } from 'ethereum-identity-kit'
export default function TransactionComponent() {
const {
txModalOpen,
setTxModalOpen,
pendingTxs,
addTransactions,
goToNextTransaction,
resetTransactions,
isCheckoutFinished,
} = useTransactions()
// Example usage
if (txModalOpen) {
return
Transaction Modal is Open
}
return
}
```
## Return Values
| Return Value | Description |
| -------------------------- | -------------------------------------------------------------- |
| `txModalOpen` | Boolean indicating if the transaction modal is open. |
| `setTxModalOpen` | Function to set the `txModalOpen` state. |
| `pendingTxs` | Array of pending transactions. |
| `addTransactions` | Function to add new transactions (any transaction). |
| `goToNextTransaction` | Function to proceed to the next transaction. |
| `resetTransactions` | Function to reset all transactions. |
| `isCheckoutFinished` | Boolean indicating if the checkout process is finished. |
| `selectedChainId` | ID of the selected chain for EFP list transactions. |
| `setSelectedChainId` | Function to set the `selectedChainId`. |
| `currentTxIndex` | Index of the current transaction being processed. |
| `setCurrentTxIndex` | Function to set the `currentTxIndex`. |
| `lists` | EFP lists of the connected user. |
| `listsLoading` | Boolean indicating if the lists are loading. |
| `addListOpsTransaction` | Function to add a list operations transaction. |
| `removeTransactions` | Function to remove transactions by their IDs. |
| `removeListOpsTransaction` | Function to remove list operations transactions by their data. |
| `selectedList` | Currently selected list. |
| `setSelectedList` | Function to set the `selectedList`. |
| `nonce` | Nonce for the current transaction. |
| `setIsCheckoutFinished` | Function to set the `isCheckoutFinished` state. |
### `txModalOpen`
**Description**:
A boolean indicating whether the transaction modal is currently open.
**Example**:
```tsx
const { txModalOpen, setTxModalOpen } = useTransactions()
// Open the transaction modal
setTxModalOpen(true)
```
### `setTxModalOpen`
**Description**:
A function to set the `txModalOpen` state, controlling the visibility of the transaction modal.
**Example**:
```tsx
setTxModalOpen(false) // Closes the transaction modal
```
### `pendingTxs`
**Description**:
An array of pending transactions that are queued for processing.
**Example**:
```tsx
const { pendingTxs } = useTransactions()
console.log(pendingTxs) // Logs the list of pending transactions
```
### `addTransactions`
**Description**:
A function to add new transactions to the pending transactions list. You can add any transaction in the following format.
**Example**:
```tsx
const txs = [
{
id: 'tx1',
title: 'Transaction', // Title of the transaction to be displayed in the modal
description: 'This transaction will do something', // Description of the transaction to be displayed in the modal
address: '0x123', // Contract address
abi: contractAbi, // ABI of the contract
chainId: 1, // Chain ID
functionName: 'function', // Function name
args: [arg1, arg2], // Arguments to be passed to the function
},
// ...
]
addTransactions(txs)
```
### `addListOpsTransaction`
**Description**:
A function to add a list operations to pending transactions. This is handled by the Follow Button component, however you can use it to add a list operations transaction manually.
**Example**:
```tsx
import {
useTransactions,
listOpAddListRecord,
listOpRemoveListRecord,
listOpAddTag,
listOpRemoveTag,
} from 'ethereum-identity-kit'
const { addListOpsTransaction } = useTransactions()
const listOps = []
listOps.push(listOpAddListRecord('0x1234...')) // Add a list record - follow
listOps.push(listOpRemoveListRecord('0x1234...')) // Remove a list record - unfollow
listOps.push(listOpAddTag('0x1234...', 'myTag')) // Add a tag
listOps.push(listOpRemoveTag('0x1234...', 'myTag')) // Remove a tag
addListOpsTransaction(listOps)
```
### `removeTransactions`
**Description**:
A function to remove transactions by their IDs.
**Example**:
```tsx
removeTransactions(['tx1', 'tx2'])
```
### `removeListOpsTransaction`
**Description**:
A function to remove list operations transactions by their data.
**Example**:
```tsx
import {
useTransactions,
listOpAddListRecord,
listOpRemoveListRecord,
listOpAddTag,
listOpRemoveTag,
} from 'ethereum-identity-kit'
const { removeListOpsTransaction } = useTransactions()
const listOpsData = []
listOpsData.push(listOpAddListRecord('0x1234...').data) // Add a list record - follow
listOpsData.push(listOpRemoveListRecord('0x1234...').data) // Remove a list record - unfollow
listOpsData.push(listOpAddTag('0x1234...', 'myTag').data) // Add a tag
listOpsData.push(listOpRemoveTag('0x1234...', 'myTag').data) // Remove a tag
removeListOpsTransaction(listOpsData)
```
### `goToNextTransaction`
**Description**:
A function to proceed to the next transaction in the queue.
**Example**:
```tsx
goToNextTransaction() // Moves to the next transaction
```
### `resetTransactions`
**Description**:
A function to reset all transactions, optionally keeping the modal open.
**Example**:
```tsx
resetTransactions() // Resets all transactions and closes the modal
resetTransactions(true) // Resets all transactions but keeps the modal open
```
### `isCheckoutFinished`
**Description**:
A boolean indicating if the checkout process is complete.
**Example**:
```tsx
const { isCheckoutFinished } = useTransactions()
if (isCheckoutFinished) {
console.log('Checkout is complete')
}
```
### `selectedChainId`
**Description**:
The ID of the selected blockchain network for transactions.
**Example**:
```tsx
const { selectedChainId, setSelectedChainId } = useTransactions()
setSelectedChainId(1) // Sets the selected chain to Ethereum Mainnet
```
### `setSelectedChainId`
**Description**:
A function to set the `selectedChainId`.
**Example**:
```tsx
setSelectedChainId(137) // Sets the selected chain to Polygon
```
### `currentTxIndex`
**Description**:
The index of the current transaction being processed.
**Example**:
```tsx
const { currentTxIndex } = useTransactions()
console.log(`Current transaction index: ${currentTxIndex}`)
```
### `setCurrentTxIndex`
**Description**:
A function to set the `currentTxIndex`.
**Example**:
```tsx
setCurrentTxIndex(2) // Sets the current transaction index to 2
```
### `lists`
**Description**:
The EFP lists associated with the connected user.
**Example**:
```tsx
const { lists } = useTransactions()
console.log(lists) // Logs the user's EFP lists
```
### `listsLoading`
**Description**:
A boolean indicating if the lists are currently loading.
**Example**:
```tsx
const { listsLoading } = useTransactions()
if (listsLoading) {
console.log('Lists are loading...')
}
```
### `selectedList`
**Description**:
The currently selected list for operations.
**Example**:
```tsx
const { selectedList, setSelectedList } = useTransactions()
setSelectedList('myList')
```
### `setSelectedList`
**Description**:
A function to set the `selectedList`.
**Example**:
```tsx
setSelectedList('newList')
```
### `nonce`
**Description**:
The nonce for the current transaction.
**Example**:
```tsx
const { nonce } = useTransactions()
console.log(`Current nonce: ${nonce}`)
```
### `setIsCheckoutFinished`
**Description**:
A function to set the `isCheckoutFinished` state.
**Example**:
```tsx
setIsCheckoutFinished(true) // Marks the checkout as finished
```
### `followingAddressesToFetchFresh`
**Description**:
An array of addresses that need fresh data fetching.
**Example**:
```tsx
const { followingAddressesToFetchFresh } = useTransactions()
console.log(followingAddressesToFetchFresh) // Logs addresses to fetch fresh data for
```
### Notes
You can use all of the returned values to create your own custom transaction modal, however the hook is mostly used to manage the Ethereum Identity Kit [transaction modal](https://ethidentitykit.com/docs/components/transaction-modal) and the transaction state.
- Ensure that the `useTransactions` hook is used within a `TransactionProvider` to access the transaction context.
- The `txModalOpen` state controls the visibility of the transaction modal.
- Use the provided functions to manage transactions, such as adding, removing, and resetting them.
---
## Interpreting EFP 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](https://docs.efp.app/design/list-storage-location) (denoting chainId, listRecords contract address, and slot)
- an [account metadata record](https://docs.efp.app/design/account-metadata) specifying a tokenId as a user's primary list
- a [list metadata record](https://docs.efp.app/design/list-metadata) in the listRecords contract for 'user' role for the slot
- a [list record](https://docs.efp.app/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.

### 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)`
---
## EFP 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.
[](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.

Once all sections display 'Ready to be deployed' the 'Deploy' button at the bottom will be unlocked and you can click it to proceed.

### 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.

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`


### 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.efp.app. 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.

### 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.

### 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 Infrastructure

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
### 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://ethidentitykit.com/docs/api/users/stats/): the follower and following counts of a particular user
- [User Following](https://ethidentitykit.com/docs/api/users/following/): list of the accounts a particular user follows
- [User Followers](https://ethidentitykit.com/docs/api/users/followers/): list of the accounts that follow a particular user
- [User ENS data](https://ethidentitykit.com/docs/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.efp.app. 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.
---
# Welcome to Ethereum Identity Kit
Ethereum Identity Kit allows you to easily implement the Ethereum identity stack into your application.
> [!NOTE] **Ethereum Identity Kit is a React component library. Supprt for other frameworks coming soon.**
## Getting Started
### Install
Install the library using your package manager.
```sh npm2yarn copy
npm install ethereum-identity-kit wagmi viem@2.x @tanstack/react-query
```
### Setup
Library uses [Tanstack Query](https://tanstack.com/query) for data fetching, [Wagmi](https://wagmi.sh/) for wallet connection and handling onchain transactions, and a [Transaction provider](https://ethidentitykit.com/docs/transaction-provider) so you need to setup a query client and provider, [Wagmi provider](https://wagmi.sh/react/api/WagmiProvider) with your [Wagmi config](https://wagmi.sh/react/api/createConfig), and add Transaction Provider to your app.
```tsx copy
import { WagmiProvider } from 'wagmi'
import { wagmiConfig } from '#/lib/wagmi'
import { TransactionProvider } from 'ethereum-identity-kit'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
export default function App({ Component, pageProps }: AppProps) {
return (
)
}
```
### Apply Styles
Add the following to your `layout.tsx` or `_app.tsx` file:
```tsx copy
import 'ethereum-identity-kit/css'
```
If you are using `dark` className prop to handle dark mode, componentss will automatically adapt to the color scheme of your application.
### You're all set!
Library is typed with TypeScript, which allows for autocompletion and type safety.
```tsx copy
import { ProfileCard } from 'ethereum-identity-kit'
export default function Home() {
return
// or 0x983110309620d911731ac0932219af06091b6744
}
```
## Troubleshooting
If you are hitting the following error trying to run your project:
```
Error: cannot use import statement outside a module
```
Make sure to transpile `ethereum-identity-kit` in your `next.config.mjs`:
```js copy
transpilePackages: ['ethereum-identity-kit'],
```
Are you experiencing issues with the library? [Report an issue](https://github.com/ethereumidentitykit/identity-kit/issues/new)
---
# Media Kit
The Media Kit provides official Ethereum Identity Kit logos and branding assets. These assets are available in both light and dark mode variations to ensure optimal visibility across different themes and backgrounds.
## Logos
Our official logos are available in both standalone and text variants. Choose the appropriate version based on your needs and the background color of your application.
| Mode | Type | Preview | Download |
| ----- | -------------- | -------------------------------------------------------------------- | --------------------------------------------------------- |
| Light | Logo with Text | | Download |
| Light | Logo | | Download |
| Dark | Logo | | Download |
| Dark | Logo with Text | | Download |
## Powered By Badges
If you're integrating Ethereum Identity Kit into your application, you can showcase this using our "Powered By" badges. These badges come in different styles to match your application's design.
| Mode | Type | Preview | Download |
| ----- | ------------------ | --------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Light | Logo with Text | | Download |
| Light | Rounded Inner Text | | Download |
| Light | Rounded Outer Text | | Download |
| Dark | Logo with Text | | Download |
| Dark | Rounded Inner Text | | Download |
| Dark | Rounded Outer Text | | Download |
---
# List Operations
The list operations module provides utility functions for creating list operations in the EFP (Ethereum Follow Protocol) system.
## Functions
### listOpAddListRecord
Creates a list operation to add a new record to a list.
```tsx
const op = listOpAddListRecord('0x...')
```
#### Parameters
| Parameter | Type | Description |
| --------- | ------- | --------------------------------------- |
| `address` | Address | The Ethereum address to add to the list |
#### Return Value
Returns a `ListOpType` object:
```tsx
{
version: 1,
opcode: Opcode.FOLLOW,
data: address
}
```
### listOpRemoveListRecord
Creates a list operation to remove a record from a list.
```tsx
const op = listOpRemoveListRecord('0x...')
```
#### Parameters
| Parameter | Type | Description |
| --------- | ------- | -------------------------------------------- |
| `address` | Address | The Ethereum address to remove from the list |
#### Return Value
Returns a `ListOpType` object:
```tsx
{
version: 1,
opcode: Opcode.UNFOLLOW,
data: address
}
```
### listOpAddTag
Creates a list operation to add a tag to a list record.
```tsx
const op = listOpAddTag('0x...', 'friend')
```
#### Parameters
| Parameter | Type | Description |
| --------- | ------- | --------------------------- |
| `address` | Address | The Ethereum address to tag |
| `tag` | string | The tag to add |
#### Return Value
Returns a `ListOpType` object:
```tsx
{
version: 1,
opcode: Opcode.TAG,
data: `${address}${toHex(tag).slice(2)}`
}
```
### listOpRemoveTag
Creates a list operation to remove a tag from a list record.
```tsx
const op = listOpRemoveTag('0x...', 'friend')
```
#### Parameters
| Parameter | Type | Description |
| --------- | ------- | ------------------------------------------- |
| `address` | Address | The Ethereum address to remove the tag from |
| `tag` | string | The tag to remove |
#### Return Value
Returns a `ListOpType` object:
```tsx
{
version: 1,
opcode: Opcode.UNTAG,
data: `${address}${toHex(tag).slice(2)}`
}
```
## What are list operations?
List operations are a way to manage lists on the EFP (Ethereum Follow Protocol). They are used to add, remove, and tag records in a list.
- [EFP Docs - List Operations](https://docs.efp.app/design/list-ops/)
---
# Transactions
The transactions module provides utility functions for handling EFP (Ethereum Follow Protocol) list operations and transactions.
## Functions
### formatListOpsTransaction
Formats a list operations transaction with the provided parameters. Use this only if you are trying to execute EFP transactions yourself. If you are using [TransactionProvider](/docs/components/transaction-provider) with [useTransaction](/docs/hooks/use-transaction), and are using the [addlistopstransaction](/docs/hooks/useTransactions#addlistopstransaction) function, you don't need to use it.
```tsx
const transaction = formatListOpsTransaction({
nonce: 1n,
chainId: 1,
listOps: [{ opcode: 1, data: '0x...' }],
connectedAddress: '0x...',
isMintingNewList: false,
})
```
#### Parameters
| Parameter | Type | Description |
| ------------------ | ------------ | -------------------------------------- |
| `nonce` | bigint | The nonce for the transaction |
| `chainId` | number | The chain ID to use |
| `listOps` | ListOpType[] | Array of list operations to perform |
| `connectedAddress` | Address | The connected wallet address |
| `isMintingNewList` | boolean | Whether this is for minting a new list |
### getListOpData
Generates list operation data from an address and optional tag.
```tsx
const data = getListOpData('0x...', 'tag')
```
#### Parameters
| Parameter | Type | Description |
| --------- | ------- | ----------------------- |
| `address` | Address | The Ethereum address |
| `tag` | string | Optional tag to include |
### getListOpsFromTransaction
Extracts list operations from a transaction.
```tsx
const listOps = getListOpsFromTransaction(transaction)
```
#### Parameters
| Parameter | Type | Description |
| ------------- | --------------- | ------------------------------------------ |
| `transaction` | TransactionType | The transaction to extract operations from |
### getMintTxNonce
Extracts the nonce from a mint transaction.
```tsx
const nonce = getMintTxNonce(transaction)
```
### getMintTxChainId
Extracts the chain ID from a mint transaction.
```tsx
const chainId = getMintTxChainId(transaction)
```
### getMintTxRecordsAddress
Extracts the records address from a mint transaction.
```tsx
const recordsAddress = getMintTxRecordsAddress(transaction)
```
### getPendingTxAddresses
Gets all addresses from pending list update transactions.
```tsx
const addresses = getPendingTxAddresses(transactions)
```
### extractAddressAndTag
Extracts address and tag from list operation data.
```tsx
const { address, tag } = extractAddressAndTag(data)
```
### getPendingTxListOps
Gets all list operations from pending transactions.
```tsx
const listOps = getPendingTxListOps(transactions)
```
### getPendingTxAddressesAndTags
Gets all addresses and tags from pending transactions.
```tsx
const addressesAndTags = getPendingTxAddressesAndTags(transactions)
```
### prepareMintTransaction
Prepares a mint transaction with the given parameters.
```tsx
const mintTx = prepareMintTransaction(mintNonce, chainId)
```
#### Parameters
| Parameter | Type | Description |
| ----------- | ------ | ---------------------------------- |
| `mintNonce` | bigint | The nonce for the mint transaction |
| `chainId` | number | Optional chain ID to use |
### transformTxsForLocalStorage
Transforms transactions for local storage by converting bigint values to strings.
```tsx
const transformedTxs = transformTxsForLocalStorage(transactions)
```
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------- | ---------------------------------- |
| `txs` | TransactionType[] | Array of transactions to transform |
---
# Validity
The validity module provides utility functions for validating various types of data in the application.
## Functions
### isLinkValid
Checks if a given link is a valid http or https link, or is pulling from public or assets folders. This is used to filter out ipfs or invalid links and fall back to the [ENS Metadata Service](https://support.ens.domains/en/articles/8228750-the-ens-metadata-service) (if there is an ENS name).
```tsx
const isValid = isLinkValid('https://example.com') // true
const isValid = isLinkValid('invalid-link') // false
```
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | -------------------- |
| `link` | string | The link to validate |
#### Return Value
Returns `true` if the link contains one of the following prefixes:
- `https://`
- `http://`
- `public/`
- `/assets`
Returns `false` if the link is undefined or doesn't contain any of the valid prefixes.
### isValidEnsName
Validates if a given string is a valid [ENS (Ethereum Name Service)](https://ens.domains/) name.
```tsx
const isValid = isValidEnsName('vitalik.eth') // true
const isValid = isValidEnsName('invalid-ens') // false
```
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------ |
| `name` | string | The ENS name to validate |
#### Return Value
Returns `true` if the name is a valid ENS name, `false` otherwise. Uses the `normalize` function from `viem/ens` for validation.
---
# List Storage Location
The list storage location module provides functionality to retrieve the storage location of an EFP list from the list registry contract.
## Functions
### getListStorageLocation
Retrieves the chain ID and storage slot for a given list number from the EFP List Registry contract.
```tsx
const { chainId, slot } = await getListStorageLocation('1')
```
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | ----------------------------------------------- |
| `list` | string | The list number to get the storage location for |
#### Return Value
Returns an object containing:
- `chainId`: number - The chain ID where the list is stored
- `slot`: bigint - The storage slot where the list data is stored
#### Example
```tsx
const location = await getListStorageLocation('1')
console.log(location)
// {
// chainId: 1,
// slot: 123456789n
// }
```
#### Technical Details
The function:
1. Creates a public client for the default chain
2. Gets the list registry contract instance
3. Calls `getListStorageLocation` on the contract with the list number
4. Extracts the chain ID and slot from the returned storage location
5. Returns the parsed data
For more information about list storage locations, see the [EFP documentation](https://docs.efp.app/design/list-storage-location/).
## What is a list storage location?
A list storage location is a unique identifier for a list on a specific chain. It is a combination of a chain ID and a storage slot. The chain ID is the ID of the chain where the list is stored, and the storage slot is the slot where the list data is stored.
- [EFP Docs - List Storage Location](https://docs.efp.app/design/list-storage-location/)
---
# Formatters
The formatters module provides utility functions for formatting various types of data in the application.
## Functions
### formatNumber
Formats a number to include separators ("," for thousands, "." for decimals)
```tsx
const formattedNumber = formatNumber(1234.56) // "1,234.56" (en-US)
```
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | -------------------- |
| `number` | number | The number to format |
### formatFollowersYouKnowText
Formats the text shown in the common followers component next to the avatars.
```tsx
const text = formatFollowersYouKnowText(3) // "1 other you know follows them"
```
#### Parameters
| Parameter | Type | Description |
| -------------- | ------ | ------------------------------ |
| `resultLength` | number | The number of common followers |
#### Return Values
| Result Length | Return Value |
| ------------- | --------------------------------- |
| 0 | "No common followers" |
| 1 | " follows them" |
| 2 | " follow them" |
| 3 | "1 other you know follows them" |
| >3 | "{n} others you know follow them" |
### formatQueryParams
Formats query parameters for API calls.
```tsx
const queryString = formatQueryParams({
address: '0x...',
tags: ['tag1', 'tag2'],
limit: 10,
}) // "address=0x...&tags=tag1,tag2&limit=10"
```
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------------------- | ---------------------------------- |
| `inputs` | Record | Object containing query parameters |
#### Return Value
Returns a URL-encoded query string with all non-null/undefined parameters.
---
# Profile
The profile module provides utility functions for handling profile-related operations.
## Functions
### defaultOnStatClick
Opens a new tab with the EFP profile page for a given address or ENS name, focusing on a specific stat tab.
```tsx
defaultOnStatClick({
addressOrName: 'vitalik.eth',
stat: 'followers',
})
```
#### Parameters
| Parameter | Type | Description |
| --------------- | ------ | ---------------------------------------------------- |
| `addressOrName` | string | Ethereum address or ENS name to open the profile for |
| `stat` | string | The stat tab to focus on in the profile page |
#### Behavior
The function:
1. Opens a new tab with the URL `https://efp.app/{addressOrName}?tab={stat}`
2. Uses `noopener,noreferrer` for security when opening the new tab
#### Example
```tsx
// Opens https://efp.app/vitalik.eth?tab=followers in a new tab
defaultOnStatClick({
addressOrName: 'vitalik.eth',
stat: 'followers',
})
```
---
# Generate Slot
The generateSlot utility generates a random storage slot value using keccak256 and the current timestamp.
## Functions
### generateSlot
Generates a random storage slot value for use in EFP list storage or other unique slot requirements.
```tsx
const slot = generateSlot()
```
#### Return Value
Returns a `bigint` representing a random storage slot value.
#### Example
```tsx
const slot = generateSlot()
console.log(slot) // e.g., 12345678901234567890n
```
#### Technical Details
- Uses `keccak256` hash of the current timestamp multiplied by a random number.
- Ensures the result fits within 255 bits.
---
# fetchFollowersYouKnow
Fetches the list of followers that you (connected address) follow, who also follow another address.
## Usage
```tsx
const followersYouKnow = await fetchFollowersYouKnow('0x...', 'vitalik.eth')
```
## Parameters
| Parameter | Type | Description |
| --------------------- | ------- | ---------------------------------------------------------- |
| `connectedAddress` | Address | Your connected wallet address |
| `lookupAddressOrName` | string | Ethereum address or ENS name to find common followers with |
## Return Value
Returns a `FollowersYouKnowResponse` object containing the list of common followers, or `noFollowersYouKnow` if the request fails.
## Example Response
```tsx
{
followers: [
{
address: '0x...',
name: 'vitalik.eth',
avatar: 'https://example.com/avatar.png',
mutuals_rank: '1',
header: 'https://example.com/header.png',
},
]
}
```
---
# fetchFollowState
Fetches the follow state between two addresses or a list and an address.
## Usage
```tsx
const followState = await fetchFollowState({
lookupAddressOrName: 'vitalik.eth',
connectedAddress: '0x...',
type: 'following',
})
// or for a list
const listFollowState = await fetchFollowState({
lookupAddressOrName: 'vitalik.eth',
list: 1,
type: 'following',
})
// or with fresh data
const freshState = await fetchFollowState({
lookupAddressOrName: 'vitalik.eth',
connectedAddress: '0x...',
type: 'following',
fresh: true,
})
```
## Parameters
| Parameter | Type | Description |
| --------------------- | ------------------------- | ------------------------------------------------------ |
| `lookupAddressOrName` | string | Ethereum address or ENS name to check follow state for |
| `connectedAddress` | string | Optional connected wallet address |
| `list` | number | Optional list number to check follow state for |
| `type` | 'following' \| 'follower' | Type of follow state to check |
| `fresh` | boolean | Optional flag to fetch fresh data bypassing cache |
## Return Value
Returns a `FollowStatusResponse` object containing the follow state, or `null` if:
- The request fails
- `type` is 'following' and no `list` is provided
- Neither `connectedAddress` nor `list` is provided
## Example Response
The following response is the connected address following the lookup address.
```tsx
{
token_id: '123',
address: '0x...',
state: {
follow: true,
block: false,
mute: false,
};
}
```
---
# fetchProfileEFPPoaps
Fetches EFP [POAPs (Proof of Attendance Protocol)](https://poap.xyz/) badges for a given address, ENS name, or list.
## Usage
```tsx
const poaps = await fetchProfileEFPPoaps('vitalik.eth')
// or for a list
const listPoaps = await fetchProfileEFPPoaps('0x...', 1)
// or with fresh data
const freshPoaps = await fetchProfileEFPPoaps('vitalik.eth', undefined, true)
```
## Parameters
| Parameter | Type | Description |
| --------------- | ------------------------- | ------------------------------------------------- |
| `addressOrName` | Address \| string \| null | Ethereum address or ENS name to fetch POAPs for |
| `list` | number \| null | Optional list number to fetch POAPs for |
| `fresh` | boolean | Optional flag to fetch fresh data bypassing cache |
## Return Value
Returns an array of `ProfileEFPPoapResponse` objects containing POAP badges, or an empty array if the request fails.
## Example Response
```tsx
;[
{
eventId: '123',
participated: 'EFP Early Adopter',
collection: {
event: {
id: 123,
fancy_id: 'EFP Early Adopter',
name: 'EFP Early Adopter',
event_url: 'https://example.com',
image_url: 'https://example.com/image.png',
country: 'United States',
city: 'San Francisco',
description: 'EFP Early Adopter',
year: 2021,
start_date: '2021-01-01',
end_date: '2021-01-01',
expiry_date: '2021-01-01',
};
tokenId: '123',
owner: '0x...',
}
},
]
```
---
# fetchEthPrice
Fetches the current ETH price in USD from the CoinGecko API.
## Usage
```tsx
const ethPrice = await fetchEthPrice()
```
## Return Value
Returns the current ETH price in USD as a number, or `null` if the request fails.
## Example Response
```tsx
2500.42 // Current ETH price in USD
```
---
# fetchProfileDetails
Fetches profile details for a given Ethereum address, ENS name, or list number.
## Usage
```tsx
const profileDetails = await fetchProfileDetails('vitalik.eth')
// or
const listDetails = await fetchProfileDetails('0x...', 1)
// or with fresh data
const freshDetails = await fetchProfileDetails('vitalik.eth', undefined, true)
```
## Parameters
| Parameter | Type | Description |
| --------------- | --------------- | ------------------------------------------------- |
| `addressOrName` | string | Ethereum address or ENS name to fetch details for |
| `list` | ProfileListType | Optional list number to fetch details for |
| `fresh` | boolean | Optional flag to fetch fresh data bypassing cache |
## Return Value
Returns a `ProfileDetailsResponse` object containing the profile details, or `null` if the request fails.
## Example Response
```tsx
{
address: '0x...',
ens: {
name: 'user.eth',
avatar: 'https://example.com/avatar.png',
header: 'https://example.com/header.png',
records: {
// ... other ENS records
},
},
primary_list: '123',
ranks: {
mutuals_rank: 1,
followers_rank: 1,
following_rank: 1,
blocks_rank: 1,
top8_rank: 1,
},
}
```
---
# fetchProfileLists
Fetches all lists associated with a given address or ENS name.
## Usage
```tsx
const lists = await fetchProfileLists('vitalik.eth')
// or with fresh data
const freshLists = await fetchProfileLists('vitalik.eth', true)
```
## Parameters
| Parameter | Type | Description |
| --------------- | ------- | ------------------------------------------------- |
| `addressOrName` | string | Ethereum address or ENS name to fetch lists for |
| `fresh` | boolean | Optional flag to fetch fresh data bypassing cache |
## Return Value
Returns a `ProfileListsResponse` object containing:
- `primary_list`: The primary list number (or null if none)
- `lists`: Array of all lists associated with the address
## Example Response
```tsx
{
primary_list: "123",
lists: [
"123",
"456",
]
}
```
---
# fetchProfileStats
Fetches profile statistics (followers and following counts) for a given address, ENS name, or list.
## Usage
```tsx
const stats = await fetchProfileStats('vitalik.eth')
// or for a list
const listStats = await fetchProfileStats('0x...', 1)
// or with live data
const liveStats = await fetchProfileStats('vitalik.eth', undefined, true)
```
## Parameters
| Parameter | Type | Description |
| --------------- | --------------- | ------------------------------------------------ |
| `addressOrName` | string | Ethereum address or ENS name to fetch stats for |
| `list` | ProfileListType | Optional list number to fetch stats for |
| `isLive` | boolean | Optional flag to fetch live data bypassing cache |
## Return Value
Returns a `StatsResponse` object containing follower and following counts, or default values if the request fails.
## Example Response
```tsx
{
followers_count: 1234,
following_count: 567
}
```
---
# fetchAccount
Fetches condensed account information for a given address, ENS name, or list.
## Usage
```tsx
const account = await fetchAccount('vitalik.eth')
// or for a list
const listAccount = await fetchAccount('0x...', 1)
```
## Parameters
| Parameter | Type | Description |
| --------------- | --------------- | ------------------------------------------------- |
| `addressOrName` | string | Ethereum address or ENS name to fetch account for |
| `list` | ProfileListType | Optional list number to fetch account for |
## Return Value
Returns an `AccountResponseType` object containing account information, or `null` if the request fails.
## Example Response
```tsx
{
address: '0x...',
ens: {
name: 'user.eth',
avatar: 'https://example.com/avatar.png',
},
primary_list: '1',
}
```
---
# fetchRecommended
Fetches recommended profiles or recent activity from the EFP API, with optional pagination.
## Usage
```tsx
// Fetch recommended profiles for a user
const recommended = await fetchRecommended('recommended', 'vitalik.eth')
// Fetch recommended profiles for a list
const listRecommended = await fetchRecommended('recommended', '0x...', '1')
// Fetch recent activity
const recentActivity = await fetchRecommended('discover')
// With pagination
const paginatedResults = await fetchRecommended('recommended', 'vitalik.eth', undefined, 20, 2)
```
## Parameters
| Parameter | Type | Description |
| --------------- | --------------------------- | ------------------------------------------------------- |
| `endpoint` | 'discover' \| 'recommended' | The type of recommendations to fetch |
| `addressOrName` | string \| Address | Optional address or ENS name to get recommendations for |
| `list` | string \| null | Optional list number to get recommendations for |
| `limit` | number | Optional number of results per page (default: 10) |
| `pageParam` | number | Optional page number for pagination (default: 1) |
## Return Value
Returns an array of recommended profiles or latest follows, or an empty array if the request fails.
## Example Response
```tsx
;[
{
address: '0x...',
name: 'user.eth',
avatar: 'https://example.com/avatar.png',
header: 'https://example.com/header.png',
},
]
```
---
# fetchAllFollowersYouKnow
Fetches a paginated list of followers that you (connected address) follow, who also follow another address, with optional search functionality.
## Usage
```tsx
const { followersYouKnow, nextPageParam } = await fetchAllFollowersYouKnow({
connectedAddress: '0x...',
lookupAddressOrName: 'vitalik.eth',
limit: 20,
search: 'search term',
pageParam: 0,
})
```
## Parameters
| Parameter | Type | Description |
| --------------------- | ----------------- | --------------------------------------------------------------- |
| `connectedAddress` | Address | Your connected wallet address |
| `lookupAddressOrName` | Address \| string | Ethereum address or ENS name to find common followers with |
| `limit` | number | Optional number of results per page (defaults to `FETCH_LIMIT`) |
| `search` | string | Optional search term to filter followers |
| `pageParam` | number | Page number for pagination (0-based) |
## Return Value
Returns an object containing:
- `followersYouKnow`: Array of `FollowerYouKnow` objects
- `nextPageParam`: Next page number for pagination
## Example Response
```tsx
{
followersYouKnow: [
{
address: '0x...',
name: 'vitalik.eth',
avatar: 'https://example.com/avatar.png',
mutuals_rank: '1',
header: 'https://example.com/header.png',
},
],
nextPageParam: 1
}
```
---
#### /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"
},
...
]
}
```
---
#### /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`
---
WIP
---
#### /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",
},
}
```
---
#### /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": [],
},
],
}
```
---
#### /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
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/latestFollowers
Get the latest 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.
- `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
curl https://api.ethfollow.xyz/api/v1/lists/4/latestFollowers
```
```jsonc
// sample response
{
"followers": [
{
"address": "0xc6fbb49a26ffdba6b7f2bd1a85219831b2a2a7aa",
"efp_list_nft_token_id": "26281",
"updated_at": "2024-10-29T21:44:59.600Z"
},
{
"address": "0x410169a13a34b3760dd258343853b8895fe74adb",
"efp_list_nft_token_id": "26267",
"updated_at": "2024-10-29T14:00:40.549Z"
},
{
"address": "0x5cacbc9de3c67a2b8de680913e2d844c5cc8b6cc",
"efp_list_nft_token_id": "26266",
"updated_at": "2024-10-29T14:00:40.549Z"
},
{
"address": "0x1248ebadcba3ff83720b5be154dc8b74bbfa11c3",
"efp_list_nft_token_id": "26265",
"updated_at": "2024-10-29T13:58:40.542Z"
},
...
]
}
```
---
#### /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
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
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
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
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
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/badges
Get EFP POAPs 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
curl https://api.ethfollow.xyz/api/v1/lists/9/badges
```
```jsonc
// sample response
{
"poaps": [
{
"eventId": "177709",
"participated": true,
"collection": {
"event": {
"id": 177709,
"fancy_id": "efp-beta-tester-2024",
"name": "EFP Beta Tester",
"event_url": "https://ethfollow.xyz",
"image_url": "https://assets.poap.xyz/335d8e5a-920f-4c62-aa41-d8745f350a26.png",
"country": "",
"city": "",
"description": "Thank you for helping us test EFP on testnet before our mainnet launch! You are truly an OG. 🤝",
"year": 2024,
"start_date": "06-Sep-2024",
"end_date": "21-Sep-2024",
"expiry_date": "21-Sep-2025"
},
"tokenId": "7189762",
"owner": "0xC9C3A4337a1bba75D0860A1A81f7B990dc607334"
}
},
{
"eventId": "178064",
"participated": false,
"collection": null
},
{
"eventId": "178065",
"participated": false,
"collection": null
}
...
]
}
```
---
#### /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
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
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
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
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
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
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
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
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
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"
},
...
]
}
```
---
### /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
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
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
curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/lists
```
```jsonc
// sample response
{
"primary_list": "4",
"lists": ["4", "107"],
}
```
---
### /users/\:addressOrENS/latestFollowers
Get a user's latest 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.
- `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
curl http://api.ethfollow.xyz/api/v1/users/dr3a.eth/latestFollowers
```
```jsonc
// sample response
{
"followers": [
{
"address": "0x9bfd90ab010c63e85453f3e40be4e3106ccf895c",
"efp_list_nft_token_id": "3413",
"updated_at": "2024-10-29T16:13:15.040Z"
},
{
"address": "0x2e11e3b40ca0c4aba93a2cd7c9046394b8dd7501",
"efp_list_nft_token_id": "26124",
"updated_at": "2024-10-28T15:26:19.215Z"
},
{
"address": "0xfdacb11c3b6703bc5525b650bd231046b3f878cd",
"efp_list_nft_token_id": "26123",
"updated_at": "2024-10-28T15:24:07.275Z"
},
{
"address": "0x22d10dd07ad572f6d53a70a4093e2d8761a3e650",
"efp_list_nft_token_id": "26110",
"updated_at": "2024-10-28T12:53:02.481Z"
},
...
]
}
```
---
### /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
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
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
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/qr
Get a QR code that links to a user's profile page.
#### 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
curl https://api.ethfollow.xyz/api/v1/users/brantly.eth/qr
```
#### Response
This endpoint responds with a formatted svg of content-type `image/svg+xml;charset=utf-8`

---
### /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
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
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/badges
Get EFP POAPs 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
curl https://api.ethfollow.xyz/api/v1/users/0xthrpw.eth/badges
```
```jsonc
// sample response
{
"poaps": [
{
"eventId": "177709",
"participated": true,
"collection": {
"event": {
"id": 177709,
"fancy_id": "efp-beta-tester-2024",
"name": "EFP Beta Tester",
"event_url": "https://ethfollow.xyz",
"image_url": "https://assets.poap.xyz/335d8e5a-920f-4c62-aa41-d8745f350a26.png",
"country": "",
"city": "",
"description": "Thank you for helping us test EFP on testnet before our mainnet launch! You are truly an OG. 🤝",
"year": 2024,
"start_date": "06-Sep-2024",
"end_date": "21-Sep-2024",
"expiry_date": "21-Sep-2025"
},
"tokenId": "7189762",
"owner": "0xC9C3A4337a1bba75D0860A1A81f7B990dc607334"
}
},
{
"eventId": "178064",
"participated": false,
"collection": null
},
{
"eventId": "178065",
"participated": false,
"collection": null
}
...
]
}
```
---
### /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
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
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
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://efp.app/",
},
"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
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
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
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
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
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",
},
],
}
```
---
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.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://ethidentitykit.com/docs/api/users/stats/): the follower and following counts of a particular user
- [User Following](https://ethidentitykit.com/docs/api/users/following/): list of the accounts a particular user follows
- [User Followers](https://ethidentitykit.com/docs/api/users/followers/): list of the accounts that follow a particular user
- [User ENS data](https://ethidentitykit.com/docs/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).
---
#### /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
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
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
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
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
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
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
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
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
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
},
...
]
```
---
# Followers you know
The Followers you know component displays a list of common followers between two Ethereum addresses or ENS names, showing their avatars and names.
Followers you know is a relation of addresses that `connectedAddress` follows and `lookupAddress` is being followed by.
### Add to your project
```tsx copy
import { FollowersYouKnow } from 'ethereum-identity-kit'
export default function Home() {
return
}
```
Try it out!
{' - https://playground.ethidentitykit.com/?path=/story/molecules-followers-you-know--followers-you-know'}
## Parameters
| Parameter | Description | Required | Default Value |
| ------------------ | ------------------------------------------------------------------------- | -------- | ------------- |
| `lookupAddress` | The Ethereum address or ENS name to find common followers with. | Yes | - |
| `connectedAddress` | The Ethereum address of the currently connected user. | No | - |
| `displayEmpty` | Whether to display the component when there are no common followers. | No | true |
| `hasModal` | Whether to display the modal with all common followers. | No | false |
| `onProfileClick` | The function to call when a profile is clicked. | No | - |
| `className` | Additional CSS class names to apply to the component. | No | - |
| `props` | Additional props for the `
## Parameters
| Parameter | Description | Required | Default Value |
| ------------- | ---------------------------------------------------------------------------- | -------- | ------------- |
| `userAddress` | The Ethereum address of the user. | Yes | - |
| `name` | The ENS name of the user. | No | - |
| `records` | The records of the user, including URLs and content hashes. | Yes | - |
| `darkMode` | Whether the profile is in dark mode. | No | false |
| `includeUrls` | Whether to include the URLs in the profile. | No | false |
| `iconSize` | The size of the icons displayed. | No | 32 |
| `isLoading` | Whether the profile is loading, showing placeholders instead of actual data. | No | false |
### Styling
The component uses predefined styles and can be further customized using the `className` prop. The appearance of the modal can be manually toggled between light and dark modes using the `darkMode` prop, however, if you have a `dark` class applied in your application, the modal will automatically use that.
---
# Transaction Modal
The Transaction Modal component allows users to initiate and manage on-chain transactions. It provides a user interface for viewing transaction details, selecting chains, and managing transaction batches.
## Add to your project
```tsx copy
import TransactionModal from 'identity-kit/src/components/transaction-modal/TransactionModal'
export default function App() {
return
}
```
Try it out!
{
' - https://playground.ethidentitykit.com/?path=/story/organisms-follow-button-transaction-modal--follow-button-single-tx'
}
The transaction modal displayed here has the "batchTransactions" prop set to false. Meaning it will open
only if you submit a transaction. Pressing the follow button will open the transaction modal.
### First time user flow
## Parameters
| Parameter | Description | Required | Default Value |
| --------------------- | -------------------------------------------------------------- | -------- | ------------- |
| `darkMode` | Enables dark mode styling for the transaction modal. | No | false |
| `showRecommendations` | Enables recommendations for the cart modal. | No | true |
| `onCartProfileClick` | The function to call when a profile is clicked. | No | - |
| `showPoapClaim` | Whether to show the POAP claim modal after minting a new list. | No | false |
| `className` | Additional CSS class names to apply to the transaction modal. | No | - |
| `props` | Additional HTML div element props. | No | - |
### Features
- **Batch Transactions**: Allows users to manage multiple transactions at once.
- **Chain Selector**: Provides an interface for selecting the blockchain network.
- **Transaction Summary**: Displays a summary of all pending transactions.
- **Transaction Flow**: Nice transaction flow with good visual feedback and animations.
### Styling
The component uses predefined styles and can be further customized using the `className` prop. The appearance of the modal can be manually toggled between light and dark modes using the `darkMode` prop, however, if you have a `dark` class applied in your application, the modal will automatically use that.
### Notes
- The modal is controlled by the `txModalOpen` state from the [`useTransactions`](https://ethidentitykit.com/docs/hooks/useTransactions) context, which determines its visibility.
- Ensure that the `TransactionModal` is wrapped within a [TransactionProvider](https://ethidentitykit.com/docs/components/transaction-provider) that supplies the necessary transaction context.
Transaction Provider parameters:
- batchTransactions (boolean) - Whether to batch transactions or not (default: false). This will allow yout to either execute transactions one by one or batch them together and have a cart-like experience.
---
# Avatar
The Avatar component displays an avatar image for a given Ethereum address or ENS name, with support for fallback images.
### Add to your project
```tsx copy
import { Avatar } from 'ethereum-identity-kit'
export default function Home() {
return
}
```
Try it out!
{' - https://playground.ethidentitykit.com/?path=/docs/molecules-avatar--component-docs'}
## Parameters
| Parameter | Description | Required | Default Value |
| ---------- | ------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| `address` | The Ethereum address for the profile of the avatar. | Yes | - |
| `src` | The source URL of the avatar image. | No | `https://metadata.ens.domains/mainnet/avatar/{name}` |
| `name` | The ENS name of the avatar. | No | - |
| `fallback` | The fallback image to display if the avatar image cannot be loaded. | No | [https://efp.app/assets/art/default-avatar.svg](https://efp.app/assets/art/default-avatar.svg) |
| `style` | Inline styles to apply to the avatar. | No | - |
| `props` | Additional props for the `
## Parameters
| Parameter | Description | Required | Default Value |
| -------------------- | ------------------------------------------------------------------------------------------------------- | -------- | -------------------- |
| `addressOrName` | Address or ENS name to lookup stats for. | Yes | - |
| `list` | List to lookup stats for; overrides `addressOrName` if provided. | No | - |
| `containerDirection` | Direction of the container layout (e.g., 'row', 'column'). | No | 'row' |
| `statsDirection` | Direction of the stats layout (e.g., 'row', 'column'). | No | 'column' |
| `statsStyle` | Inline styles for the stats section. | No | - |
| `containerStyle` | Inline styles for the container. | No | - |
| `onStatClick` | Function to be called when a stat is clicked; defaults to navigating to EFP profile with selected stat. | No | `defaultOnStatClick` |
| `props` | Additional props for the `
` element. | No | - |
---
# Follower Tag
The Follower Tag component displays the relationship status between a given Ethereum address and the connected user, such as whether the user follows, blocks, or mutes the address.
### Add to your project
```tsx copy
import { FollowerTag } from 'ethereum-identity-kit'
export default function Home() {
return (
)
}
```
Try it out!
{' - https://playground.ethidentitykit.com/?path=/docs/molecules-follower-tag--component-docs'}
## Parameters
| Parameter | Description | Required | Default Value |
| ------------------ | --------------------------------------------------------------------- | -------- | ------------- |
| `addressOrName` | The Ethereum address or ENS name of the follower. | Yes | - |
| `connectedAddress` | The Ethereum address of the currently connected user. | Yes | - |
| `list` | The list of the user (selected list in EFP app). | No | - |
| `showLoading` | Whether to show a loading state while fetching follower status. | No | false |
| `className` | Additional CSS class names to apply to the component. | No | - |
| `props` | Additional props for the `
` element containing the follower tag. | No | - |
---
# Full Width Profile
The Full Width Profile component displays a comprehensive Ethereum Profile with ENS and EFP details like:
- Avatar and Header Image
- Name and Status
- Role (if provided)
- Description (Bio)
- Follower State (follows you, blocks you, mutes you)
- Profile Statistics (Followers, Following)
- Common Followers
- Social Media Links
- POAP Badges
### Add to your project
```tsx copy
import { FullWidthProfile } from 'ethereum-identity-kit'
export default function Home() {
return
}
```
Try it out!
{
' - https://playground.ethidentitykit.com/?path=/story/organisms-full-width-profile--full-width-profile-by-address'
}
## Parameters
| Parameter | Description | Required | Default Value |
| --------------------- | --------------------------------------------------------------------------------------------------- | -------- | ------------- |
| `addressOrName` | Ethereum Address or ENS name to fetch profile data for. | Yes | - |
| `list` | Search profile data by list number; overrides `addressOrName` if provided. | No | - |
| `connectedAddress` | Address of the user connected to the app. | No | - |
| `darkMode` | Enables dark mode styling for the profile. | No | false |
| `role` | Additional information to display on the profile (used for roles on ethid.org). | No | - |
| `showFollowerState` | Shows follower state tag (follows you, blocks you, mutes you). | No | false |
| `onStatClick` | Action to perform when a stat is clicked; defaults to navigating to EFP profile with selected stat. | No | - |
| `selectedList` | List number selected in your application for the connected user. | No | - |
| `onProfileClick` | Action to perform when the profile is clicked. | No | - |
| `showPoaps` | Shows EFP related POAPs on the profile. | No | true |
| `alignProfileContent` | Aligns the profile content when max-width is surpassed (center, start, end). | No | 'center' |
| `options` | Additional options for profile data and functionality. See `ProfileCardOption` type for details: | No | - |
| | - `followButton`: React node for a follow button. | | |
| | - `nameMenu`: React node for a name menu. | | |
| | - `profileData`: Prefetched profile data. | | |
| | - `prefetchedProfileLoading`: Loading state for prefetched profile data. | | |
| | - `refetchProfileData`: Function to refetch profile data. | | |
| | - `statsData`: Prefetched stats data. | | |
| | - `openListSettings`: Function to open list settings modal. | | |
| `className` | Additional CSS class names to apply to the component. | No | - |
| `style` | Inline CSS styles to apply to the component. | No | - |
### Styling
The component uses predefined styles and can be customized using the `className` and `style` props. The appearance can be manually toggled between light and dark modes using the `darkMode` prop. The component also supports responsive design with different layouts for desktop and mobile views.
---
# Follow Button
The Follow Button component allows users to manage their social connection with another user. It displays the current relationship status between the `lookupAddress` and the `connectedAddress` and provides actions to change this state.
### Add to your project
### Add Transaction Modal component
Follow button will work with the Transaction Modal component to handle the transaction flow, therefore you need to add the Transaction Modal component to your project.
Make sure to add the Transaction Provider to your project as well.
For all the other providers take a look at the [setup](https://ethidentitykit.com/docs#setup) documentation.
```tsx copy
import { TransactionProvider, TransactionModal } from 'ethereum-identity-kit'
export default function App() {
return (
// Other Providers
)
}
```
### Add Follow Button component
Add the follow button wherever you wish, give it the `lookupAddress` and `connectedAddress` props, and you are good to go.
```tsx copy
import FollowButton from 'ethereum-identity-kit'
export default function YourComponent() {
return (
alert('Please connect your wallet')}
/>
)
}
```
Try it out!
{
' - https://playground.ethidentitykit.com/?path=/story/organisms-follow-button-transaction-modal--follow-button-single-tx'
}
Follow button for vitalik.eth - 0xd8da6bf26964af9d7eed9e03e53415d37aa96045
## Parameters
| Parameter | Description | Required | Default Value |
| --------------------- | ------------------------------------------------------------------------------ | -------- | ------------- |
| `lookupAddress` | The address of the user to follow or unfollow. | Yes | - |
| `connectedAddress` | The address of the currently connected user. | No | - |
| `selectedList` | The EFP list number to manage follow state for. (defaults to connectedAddress) | No | - |
| `disabled` | Disables the button if set to true. | No | false |
| `onDisconnectedClick` | Function to call when the button is clicked and the user is not connected. | No | - |
| `sounds` | Object containing sound files to play on button actions. | No | - |
| `customClassName` | Custom CSS class names to apply to the button. (overwrites the default styles) | No | - |
| `customLoader` | Custom loader component to display while loading. | No | - |
| `props` | Additional HTML button element props. | No | - |
### Styling
The component uses predefined styles from `FOLLOW_BUTTON_STYLES` and can be further customized using the `className` prop. The button's appearance changes based on its state (e.g., pending, disabled).
### Sound Effects
You can provide sound effects for different button states by passing a `sounds` object. Each key in the object corresponds to a [button state](https://ethidentitykit.com/docs/hooks/useFollowButton), and the value is the path to the sound file.
```tsx copy
```
### Custom Loader
If you want to use a custom loader while the button is in a loading state, pass a React component to the `customLoader` prop.
### Notes
- Ensure that the `lookupAddress` and `connectedAddress` are valid Ethereum addresses.
- The `onDisconnectedClick` function is optional but recommended to handle cases where the user is not connected.
- The `sounds` prop is optional and can enhance user experience with auditory feedback.
---
# Transaction Provider
The Transaction Provider component supplies the necessary context for managing on-chain transactions within the application. It provides state management and utility functions for handling transaction modals, batching, and more.
## Add to your project
```tsx copy
import { TransactionProvider } from 'identity-kit/src/context/transactionContext'
export default function App() {
return {/* Your application components */}
}
```
## Parameters
| Parameter | Description | Required | Default Value |
| ------------------- | ---------------------------------------------------------------------- | -------- | ------------- |
| `batchTransactions` | Enables batching of transactions if set to true. | No | false |
| `paymasterService` | The paymaster service to use for sponsored transactions. (string) | No | - |
| `defaultChainId` | The default chain ID to use for the transaction provider. (number) | No | - |
| `children` | The child components that will have access to the transaction context. | Yes | - |
## Usage
The `TransactionProvider` component is used to wrap parts of your application that require access to transaction-related state and functions. It manages the state of transaction modals, pending transactions, and provides utility functions for transaction operations.
### Features
- **Transaction Modal Management**: Controls the visibility and state of transaction modals.
- **Batch Transactions**: Supports batching of multiple transactions for a streamlined user experience.
- **Paymaster Service**: Supports paymaster services for sponsored transactions. [(https://docs.cdp.coinbase.com/paymaster/docs/welcome)](https://docs.cdp.coinbase.com/paymaster/docs/welcome)
- **Default Chain ID**: The default chain ID to use if the user does not have an EFP list yet (Has to mint the new list).
- **Transaction State Management**: Provides state and functions for managing pending transactions, current transaction index, and more.
### Batching Transactions
If you set `batchTransactions` to `true`, the `TransactionProvider` will manage a batch of transactions. This will allow you to either execute transactions one by one or batch them together and have a cart-like experience.
```tsx
```
You will have to provide a button in your application that will open the transaction modal.
```tsx
import { useTransactions } from 'ethereum-identity-kit'
/* ... your code ... */
const { setTxModalOpen } = useTransactions()
/* ... your code ... */
```
### Context Values
The `TransactionProvider` supplies the following context values:
- `txModalOpen`: Boolean indicating if the transaction modal is open.
- `batchTransactions`: Boolean indicating if transactions are batched.
- `pendingTxs`: Array of pending transactions.
- `currentTxIndex`: Index of the current transaction being processed.
- `selectedChainId`: ID of the selected blockchain network.
- `addTransactions`: Function to add new transactions.
- `resetTransactions`: Function to reset all transactions.
- `goToNextTransaction`: Function to proceed to the next transaction.
- `lists`: The EFP lists of the connected user.
- `listsLoading`: Boolean indicating if the EFP lists of the connected user are loading.
### Notes
- Ensure that the `TransactionProvider` wraps components that need access to transaction context.
- The `batchTransactions` parameter determines if transactions should be batched.
- Use the `useTransactions` hook to access the context values and functions within your components.
---
# Profile Card
The Profile Card component displays a user's ENS and EFP details like:
- Primary List Number (EFP List)
- Avatar
- Header Image
- Name
- Description (Bio)
- Links
- Socials
- Followers
- Following
- Common Followers
### Add to your project
```tsx copy
import { ProfileCard } from 'ethereum-identity-kit'
export default function Home() {
return
}
```
Try it out!
{' - https://playground.ethidentitykit.com/?path=/docs/organisms-profile-card--component-docs'}
## Parameters
| Parameter | Description | Required | Default Value |
| ------------------------- | --------------------------------------------------------------------------------------------------- | -------- | -------------------- |
| `addressOrName` | Ethereum Address or ENS name to fetch profile data for. | Yes | - |
| `list` | Search profile data by list number; overrides `addressOrName` if provided. | No | - |
| `connectedAddress` | Address of the user connected to the app. | No | - |
| `darkMode` | Enables dark mode styling for the profile card. | No | false |
| `showFollowerState` | Shows follower state tag (e.g., follows you, blocks you, mutes you). | No | - |
| `onStatClick` | Action to perform when a stat is clicked; defaults to navigating to EFP profile with selected stat. | No | `defaultOnStatClick` |
| `hasCommonFollowersModal` | Whether to show the [common followers modal](./common-followers.mdx). | No | true |
| `options` | Additional options for profile data and stats. See `ProfileCardOption` type for details: | No | - |
| | - `followButton`: React node for a follow button. | | |
| | - `nameMenu`: React node for a name menu. | | |
| | - `profileData`: Prefetched profile data. | | |
| | - `prefetchedProfileLoading`: Loading state for prefetched profile data. | | |
| | - `refetchProfileData`: Function to refetch profile data. | | |
| | - `statsData`: Prefetched stats data. | | |
| | - `prefetchedStatsLoading`: Loading state for prefetched stats data. | | |
| | - `refetchStatsData`: Function to refetch stats data. | | |
| `className` | Additional CSS class names to apply to the component. | No | - |
| `style` | Inline CSS styles to apply to the component. | No | - |
| `props` | Additional props for the `
` element. | No | - |
### Styling
The component uses predefined styles and can be further customized using the `className` prop. The appearance of the modal can be manually toggled between light and dark modes using the `darkMode` prop, however, if you have a `dark` class applied in your application, the modal will automatically use that.
---
# docs.ens.domains llms.txt
> The Ethereum Name Service (ENS) is a distributed, open, and extensible naming system based on the Ethereum blockchain. ENS maps human-readable names like 'alice.eth' to machine-readable identifiers such as Ethereum addresses, other cryptocurrency addresses, content hashes, metadata, and more.
# Reverse Registrar [Registrar responsible for Primary Names]
Reverse resolution in ENS - the process of mapping from an Ethereum address (eg, 0x1234...) to an ENS name - is handled using a special namespace, .addr.reverse. A special-purpose registrar controls this namespace and allocates subdomains to any caller based on their address. The reverse registrar is specified in EIP 181.
For example, the account `0x314159265dd8dbb310642f98f50c066173c1259b` can claim `314159265dd8dbb310642f98f50c066173c1259b.addr.reverse`.
After doing so, it can configure a resolver and expose metadata, such as a canonical ENS name for this address.
The reverse registrar provides functions to `claim` a reverse record,
as well as a convenience function (`setName`) to configure the record as it's most commonly used, as a way of specifying a canonical name for an address.
## Lookup Primary Name
If you are interested in [querying the primary name for an address](/web/reverse) you can checkout the web section.
## Set Primary Name
```solidity
function setName(string memory name) public returns (bytes32)
```
Configures the caller's reverse ENS record to point to the provided `name`.
This convenience function streamlines the process of setting up a reverse record for the common case where a user only wants to configure a reverse name and nothing else. It performs the following steps:
1. Sets the reverse record for the caller to be owned by the ReverseRegistrar.
2. Sets the reverse record for the caller to have `defaultResolver` as its resolver.
3. Sets the `name()` field in the `defaultResolver` for the caller's reverse record to `name`.
In short, after calling this, a user has a fully configured reverse record claiming the provided name as that account's canonical name.
Users wanting more flexibility will need to use `claim` or `claimWithResolver` and configure records manually on their chosen resolver contract.
## Multichain Considerations
The current infrastructure for primary names across multiple chains is being worked on.
## Other Functions
### Claim Address
```solidity
function claim(address owner) public returns (bytes32);
```
Claims the caller's address in the reverse registrar, assigning ownership of the reverse record to `owner`. Equivalent to calling `claimWithResolver(owner, 0)`.
```solidity
function claimWithResolver(address owner, address resolver) public returns (bytes32)
```
Claims the caller's address in the reverse registrar, assigning `ownership` of the reverse record to owner. If `resolver` is nonzero, also updates the record's resolver.
After calling this function:
- The reverse record for the caller (1234....addr.reverse) is owned by `owner`.
- If `resolver` is nonzero, the reverse record for the caller has its resolver set to `resolver`; otherwise it is left unchanged.
### Get Default Resolver
```solidity
Resolver public defaultResolver;
```
Returns the address of the resolver contract that the `ReverseRegistrar` uses for `setName`.
---
# DNS Registrar
In [DNS on ENS](/learn/dns) we learned how ENS aims to extend the functionality of the DNS.
On this page we will explore the implementation of DNSSEC, the DNSRegistrar, and the building blocks for gasless DNSSEC.
:::note
Not all top level domains support DNSSEC, and some might have custom ENS
implementations. Please refer to the [TLD List](/dns/tlds) for TLD-specific
information.
:::
## DNSSEC
DNSSEC (Domain Name System Security Extensions) is an added layer of security on top of DNS that allows for cryptographic verification of records. It establishes a chain of trust from the root key (which is signed by ICANN) down to each key.
All ENS-enabled DNS names are required to use DNSSEC, and the [DNSSECOracle](https://github.com/ensdomains/ens-contracts/tree/master/contracts/dnssec-oracle) is responsible for verifying the signatures.
### Claiming a Name Onchain
Since 2021, it has been possible to [import a DNS name](/learn/dns#importing-a-dns-name) and use that as an ENS name. This process involves enabling DNSSEC, setting a specific TXT record, and submitting a proof to the [DNSRegistrar](https://github.com/ensdomains/ens-contracts/tree/master/contracts/dnsregistrar) smart contract.
Expect your `TXT` record to look something like this:
```
TXT @ _ens a=<eth-address>
```
You can learn more about [how to import a DNS name](/learn/dns#importing-a-dns-name) in the DNS section, or see how to [programmatically complete these steps](#programming-dnssec-proofs).
There is no ENS protocol fee to import a DNS name, but it requires a large amount of gas (up to a few million) to submit the proof onchain. Continue reading to learn how this has been mitigated.
## Offchain Verification (Gasless)
[EP 5.1](/dao/proposals/5.1) introduced a new DNSSECOracle and DNSRegistrar which makes it possible to perform DNSSEC verification at query time, enabling truly free usage of DNS names in ENS. We call this "gasless DNSSEC".
It works by enabling [wildcard resolution](/ensip/10) at the DNS TLD level. During the [name resolution process](/resolution), if a name doesn't already exist onchain but has been configured for usage in ENS, the DNSSEC proof will be fetched offchain via [CCIP Read](https://eips.ethereum.org/EIPS/eip-3668) and then verified onchain with the same DNSSECOracle mentioned above.
### Import a DNS name gaslessly
To configure a DNS name for usage in ENS, simply add a `TXT` record with the following format:
```
TXT @ ENS1 <resolver-address>
```
The `resolver-address` implementation is customizable just like any other ENS resolver. To get started quickly, a special ExtendedDNSResolver has been deployed which allows users to specify an ETH address that the name should resolve to within the same `TXT` record. To use this setup, simply add a record with the following format:
```
TXT @ ENS1 <extended-resolver-address> <eth-address>
TXT @ ENS1 0x238A8F792dFA6033814B18618aD4100654aeef01 0x225f137127d9067788314bc7fcc1f36746a3c3B5
```
## Other
### TLD Ownership
You can lookup the `owner` of any TLD by calling the `Registry.owner(bytes32 node)` function.
If at least one domain has been imported for this TLD (via the onchain method), the owner will be either the `DNSRegistrar` or a smart contract controlled by the respective registry operator.
If a TLD has not yet been activated, the `owner` will return `0x0` and it may require one user to import a name onchain to activate the TLD. See the [supported TLD list](/dns/tlds) for more info.
### Programming DNSSEC Proofs
To help you interact with DNSSEC data and the DNSRegistrar, we provide a few libraries.
- [DNSProvejs](https://github.com/ensdomains/dnsprovejs) = A library for querying and validating DNSSEC data from DNS
- [ENSjs](https://github.com/ensdomains/ensjs) = A library for interacting with ENS smart contracts
#### Retrieving a proof
```ts
import { addEnsContracts } from '@ensdomains/ensjs'
import { getDnsImportData } from '@ensdomains/ensjs/dns'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const client = createPublicClient({
chain: addEnsContracts(mainnet),
transport: http(),
})
const dnsImportData = await getDnsImportData(client, {
name: 'example.com',
})
```
#### Submitting the proof to the DNSRegistrar
```ts
import { addEnsContracts } from '@ensdomains/ensjs'
import { getDnsImportData, importDnsName } from '@ensdomains/ensjs/dns'
import { createPublicClient, createWalletClient, custom, http } from 'viem'
import { mainnet } from 'viem/chains'
const mainnetWithEns = addEnsContracts(mainnet)
const client = createPublicClient({
chain: mainnetWithEns,
transport: http(),
})
const wallet = createWalletClient({
chain: mainnetWithEns,
transport: custom(window.ethereum),
})
const dnsImportData = await getDnsImportData(client, {
name: 'example.com',
})
await importDnsName(wallet, {
name: 'example.com',
dnsImportData,
})
```
## Other functions
```ts
// Get the list of suffixes
DNSRegistrar.suffixes
// Get Oracle
DNSRegistrar.oracle
```
```ts
DNSRegistrar.claim(bytes name, bytes proof)
DNSRegistrar.proveAndClaim(bytes name, tuple[] input, bytes proof)
DNSRegistrar.proveAndClaimWithResolver(bytes name, tuple[] input, bytes proof, address resolver, address addr)
```
---
import { FiBookOpen, FiClock, FiHash } from 'react-icons/fi'
import { Card } from '../../components/ui/Card'
# ETH Registrar [Smart contracts responsible for the ".eth" TLD]
The ETH Registrar is a special registrar. It allows for trustless on-chain name registration and is in charge of the ".eth" TLD.
## BaseRegistrar vs Controller
The ETH Registrar is split into two contracts. The [BaseRegistrar](https://github.com/ensdomains/ens-contracts/blob/staging/contracts/ethregistrar/BaseRegistrarImplementation.sol) and the [ETHRegistrarController](https://github.com/ensdomains/ens-contracts/blob/staging/contracts/ethregistrar/ETHRegistrarController.sol).
The BaseRegistrar is responsible for name ownership, transfers, etc (ownership related), while the Controller is responsible for registration & renewal (pricing related). This separation is done to reduce the attack surface of the registrar, and provides users with the guarantees of continued ownership of a name so long as the registrar is in place.
### Controllers
The [ETHRegistrarController](https://github.com/ensdomains/ens-contracts/blob/staging/contracts/ethregistrar/ETHRegistrarController.sol) is the main controller for the ETH Registrar, and provides a straightforward registration and renewal mechanism.
## Pricing Structure
The ETH Registrar charges a fee for registration.
This fee is paid in ETH and is set to prevent spamming the registrar.
Any protocol fees are sent to the ENS Treasury.
### Pricing Oracle
Initially, a single pricing oracle was deployed, the [StablePriceOracle](https://github.com/ensdomains/ens-contracts/blob/master/contracts/ethregistrar/StablePriceOracle.sol).
This contract has owner-set prices for each name length (1, 2, 3, 4, 5 or more).
Users do not have to interact with this oracle directly, as the controller provides functionality to determine the pricing for a registration or renewal.
### 3, 4, and 5 Letter Names
The ETH Registrar has special pricing for 3, 4, and 5 (and more) letter names. At the time of writing, a `5+` letter `.eth` will cost you `5 USD` per year.
A `4` letter `160 USD` per year, and a `3` letter `640 USD` per year.
This pricing structure is done to promote market diversity as there are an exponentially less amount of names the shorter they become.
The minimum length of a name is 3 characters.
| Name Length | Price (USD) |
| ----------- | ----------- |
| 5+ | 5 |
| 4 | 160 |
| 3 | 640 |
### Premium & Auctions
In addition to length-based pricing the ETH Registrar also has a premium pricing structure.
90 days after a name expires (aka after the grace period), the name will go into a Temporary Premium Auction.
The Auction is a 21 day dutch auction, meaning that the price starts high (~100 Million USD) and exponentially decrease till it hits 0 or a bid goes through.
This is done to prevent sniping of names, and ensures the name goes to the highest bidder fairly.
You can read more about the temporary premium in [this article](https://support.ens.domains/en/articles/7900612-temporary-premium).
### Where does the money go?
Upon registration funds are sent to the ETHRegistrarController. The controller then sends the funds to the ENS Treasury (anyone can call the `withdraw` method to trigger this).
Income from the ETH Registrar is used to fund the development of ENS, its ecosystem, and other public goods.
Read more about our spending in [Article III of the Constitution](/dao/constitution#iii-income-funds-ens-and-other-public-goods).
## ERC721 and NFTs
In the early days of ENS, the ERC721 standard did not exist.
The original ETH Registrar formed the pre-cursor to the ERC721 standard.
As we witnessed the ERC721 being standardized, support for it was added to the ETH Registrar.
Today, users can interact with the ETH Registrar to transfer their name just like with any other ERC721 token.
## Registering a Name
Registering a name is a trustless process that takes place onchain (more on this below). Some open source frontends for registering names are the [ENS Manager App](https://app.ens.domains/), [ENS Fairy](https://ensfairy.xyz/), Rainbow Wallet.
The process of registering a `.eth` name uses a commit-reveal process.
Commit
Wait
Reveal
### Commit-Reveal
The ETHRegistrarController, the highest level contract that users register names through, implements a commit reveal scheme to prevent frontrunning registrations.
We first call the `commit` function with an opaque bit of data (the `commitmenthash`), wait 60 seconds, and then call the `register` function. The `commit` function takes a commitment hash, which can be generated using the `makeCommitment` function. The commitment hash is opaque and revealed during the `register` function.
The commit-reveal process is to prevent a malicious actor from seeing your `register` transaction in the public mempool and frontrunning it.
```solidity
ETHRegistrarController.makeCommitment(
name string,
owner address,
duration uint256,
secret bytes32,
resolver address,
data bytes[],
reverseRecord bool,
ownerControlledFuses uint16
)
// For example
makeCommitment(
"myname", // "myname.eth" but only the label
0x1234..., // The address you want to own the name
31536000, // 1 year (in seconds)
0x1234..., // A randomly generated 32 byte secret you create
0x1234..., // The address of the resolver you want to use
[0x8b95dd71...], // Encoded function calls you want to pass to the resolver, like `setAddr()`
false, // Whether or not to set the new name as your primary name
0 // The NameWrapper fuses you want to set
);
```
Once you have calculated the commitment hash, submit the `commit` transaction.
```solidity
ETHRegistrarController.commit(commitment bytes32)
```
After having committed, it is required to wait at least the `MIN_COMMITMENT_AGE` (60 seconds) before making the subsequent `register` transaction.
### Registering
Once you have made the onchain commitment and waited 60 seconds, you can register your name.
Registration takes in the same parameters as the `makeCommitment` function above.
Before initiating registration, ensure that:
- `available(label)` == `true`, where `label` is "name" in "name.eth"
- `duration` >= `MIN_REGISTRATION_DURATION`
- `commitments[commitment]` is between 1 min and 24 hrs old
- `msg.value` >= `rentPrice(name, duration)` + `5-10% (slippage)`
Because the rent price is paid in ETH but denominated in USD, callers are recommended to send slightly more than the value returned by `rentPrice` to avoid issues with fast price changes. A premium of 3-5% will likely be sufficient.
Any excess funds sent during registration are automatically returned to the caller.
```solidity
ETHRegistrarController.register(
name string,
owner address,
duration uint256,
secret bytes32,
resolver address,
data bytes[],
reverseRecord bool,
ownerControlledFuses uint16
)
// For example
register(
"myname", // "myname.eth" but only the label
0x1234..., // The address you want to own the name
31536000, // 1 year (in seconds)
0x1234..., // The same secret you used in the `commit` transaction
0x1234..., // The address of the resolver you want to use
[0x8b95dd71...], // Encoded function calls you want to pass to the resolver, like `setAddr()`
false, // Whether or not to set the new name as your primary name
0 // The NameWrapper fuses you want to set
);
```
## Renewing a Name
```solidity
ETHRegistrarController.renew()
```
Any user can renew a domain, not just the owner. This means that if you want to ensure a name doesn't expire you can renew it for someone.
By allowing renewal for any arbitrary amount of time users can ensure their name will not expire.
As per the separation between registry and controller, even with upgraded controller your name will still be yours.
## Other features
```solidity
ETHRegistrarController.MIN_COMMITMENT_AGE uint
ETHRegistrarController.MAX_COMMITMENT_AGE uint
ETHRegistrarController.MIN_REGISTRATION_DURATION uint
// Get Commitment Timestamp
ETHRegistrarController.commitments mapping(bytes32=>uint)
// Get Rent Price
ETHRegistrarController.rentPrice(string name, uint duration) view returns (uint)
// Check Name Validity
ETHRegistrarController.valid(string name) view returns (bool)
// Check Name Availability
// Returns true if the name is both valid and available for registration by this controller.
ETHRegistrarController.available(string name) view returns (bool)
// Calculate Commitment Hash
ETHRegistrarController.makeCommitment(string name, address owner, uint256 duration, bytes32 secret, address resolver, bytes[] data, bool reverseRecord, uint16 ownerControlledFuses) view returns (bytes32)
// Get Name Expiry (unix timestamp at which registration expires)
BaseRegistrar.nameExpires(uint256 label) view returns (uint)
// Check Name Availability (less specific, use ETHRegistrarController.available instead)
BaseRegistrar.available(uint256 label) view returns (bool)
// Get Transfer Period End (unix timestamp at which transfer period (from legacy registrar) ends)
BaseRegistrar.transferPeriodEnds uint
// Get Controller Status
BaseRegistrar.controllers mapping(address=>bool)
// Check Token Approval
BaseRegistrar.getApproved(uint256 tokenId) view returns (address operator)
// Check All Tokens Approval
BaseRegistrar.isApprovedForAll(address owner, address operator) view returns (bool)
// Get Token Owner
BaseRegistrar.ownerOf(uint256 tokenId) view returns (address)
// Get Token URI
BaseRegistrar.tokenURI(uint256 tokenId) view returns (string)
```
Writable
```solidity
// Transfer a Name
BaseRegistrar.transferFrom(address from, address to, uint256 tokenId)
BaseRegistrar.safeTransferFrom(address from, address to, uint256 tokenId)
BaseRegistrar.safeTransferFrom(address from, address to, uint256 tokenId, bytes _data)
// Approve Operator
BaseRegistrar.approve(address to, uint256 tokenId)
// Set Approval For All
BaseRegistrar.setApprovalForAll(address operator, bool approved)
// Reclaim ENS Record
BaseRegistrar.reclaim(uint256 label)
```
Events
```solidity
// BaseRegistrar
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event NameMigrated(uint256 indexed hash, address indexed owner, uint expires);
event NameRegistered(uint256 indexed hash, address indexed owner, uint expires);
event NameRenewed(uint256 indexed hash, uint expires);
// Controller
event NameRegistered(string name, bytes32 indexed label, address indexed owner, uint cost, uint expires);
event NameRenewed(string name, bytes32 indexed label, uint cost, uint expires);
```
---
# The Registry [Root Registry of the Ethereum Name Service]
The ENS registry is the core contract that lies at the heart of ENS resolution. All ENS lookups start by querying the registry. The registry maintains a list of domains, recording the owner, resolver, and TTL for each, and allows the owner of a domain to make changes to that data.
The ENS registry is specified in [EIP 137](https://eips.ethereum.org/EIPS/eip-137).
## Why Registries?
Top-Level Domains (TLDs), like `.eth`, `.com`, and `.test`, are owned by smart contracts called registrars, which specify rules governing the allocation of their names.
Anyone may, by following the rules imposed by these registrar contracts, obtain ownership of a domain for their own use.
| TLD | Registrar Contract |
| ------------------- | -------------------------------------- |
| `[root]` | [The Registry](/registry/ens) |
| `.eth` | [ETH Registry](/registry/eth) |
| `.com`, `.xyz`, etc | [DNS Registrar](/registry/dns) |
| `.addr.reverse` | [Reverse Registrar](/registry/reverse) |
## Who owns the root Registry?
The [ENS Registry](https://etherscan.io/address/0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e) is owned by the [ENS Root](https://etherscan.io/address/0xaB528d626EC275E3faD363fF1393A41F581c5897) which is owned by the [ENS DAO Wallet](https://etherscan.io/address/0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7).
To verify this you can run the `owner` function on the registry & root contracts.
## Other Functions
```ts
// Get Owner
ENS.owner(bytes32 node) view returns (address)
// Get Resolver
ENS.resolver(bytes32 node) view returns (address)
// Get TTL
ENS.ttl(bytes32 node) view returns (uint64)
// Get Approval
ENS.isApprovedForAll(address owner, address operator) view returns (bool)
// Check Record Existence
ENS.recordExists(bytes32 node) view returns (bool)
```
```ts
// Set Owner (only callable by current owner)
ENS.setOwner(bytes32 node, address owner)
// Set Resolver
ENS.setResolver(bytes32 node, address resolver)
// Set TTL
ENS.setTTL(bytes32 node, uint64 ttl)
// Set Subnode Owner
ENS.setSubnodeOwner(bytes32 node, bytes32 label, address owner)
// Set Multiple (convenience function (setResolver, setTTL, setOwner))
ENS.setRecord(bytes32 node, address owner, address resolver, uint64 ttl)
// Set Multiple Subnode
ENS.setSubnodeRecord(bytes32 node, bytes32 label, address owner, address resolver, uint64 ttl)
// Set Approval
ENS.setApprovalForAll(address operator, bool approved)
```
Events
```ts
// Transfer Event
event Transfer(bytes32 indexed node, address owner)
// New Resolver Event
event NewResolver(bytes32 indexed node, address resolver)
// New TTL Event
event NewTTL(bytes32 indexed node, uint64 ttl)
// New Owner Event
event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner)
```
---
# FAQ
## Which wallets and dApps support ENS?
ENS is supported by a wide range of wallets and dApps, some notable ones can be found on the [integrations page](https://ens.domains/).
This page is currently under construction however a link to add yourself will be put here soon.
## Can I hold my name with one address, and point it at the other?
Yes, you can hold your name with one address and point it at another.
Simply visit the [ENS Manager App](https://ens.app/) and update the appropriate address record (by chain) for your name to point to the address you wish.
## Once I own a name, can I create my own subdomains?
Yes. You can create whatever subdomains you wish and assign ownership of them to other people if you desire. You can even set up your own registrar for your domain.
Some resolvers might provide even more advanced features, read more [about Resolvers](/resolvers/quickstart).
## Can I change the address my name points to after I've bought it?
Yes, you can update the addresses and other resources pointed to by your name at any time.
To update your name checkout the [ENS Manager App](https://ens.app/).
## ETH Registration
### Why are names registered as hashes?
Hashes provide a fixed length identifier that can easily be passed around between contracts with fixed overhead and no issues passing around variable-length strings.
Read more about [labelhash, namehash, and encodings](/resolution/names).
### What characters are supported?
ENS names are generally encoded using UTS-46.
This means there is partial support for Unicode characters, including emoji.
However technically possible to register any name, names that are not valid UTS-46 will not be resolvable by most resolvers.
Therefore it is generally recommended for apps that implement registration to limit the characters that can be registered to ensure a smooth experience.
To read more about supported characters [name normalization](/resolution/names).
### What does it cost to register a .eth domain?
Currently, registration costs are set at the following prices:
- 5+ character .eth names: $5 in ETH per year.
- 4 character .eth names: $160 in ETH per year.
- 3 character .eth names: $640 in ETH per year.
3 and 4 character names have higher pricing to reflect the small number of these names available.
To read more about the pricing structure of .eth names [read more about pricing](/registry/eth)
### How long can I register a name for?
You can register a name for as long as you would like.
There is no maximum registration duration.
### What happens if I forget to renew my name?
If you forget to renew your name, it will be released back to the public pool of available names.
Luckily the expiration process has a 90 day grace period.
This means that once the name expires the original owner has 90 days to renew the name before it is released.
After the grace period, the name is released for registration by anyone with a temporary premium which decreases over a 21 days period.
The released name continues to resolve your ETH address until the new owner overwrites it.
### In what way could I lose access to my name?
The .eth registrar is built to ensure once issued, a name cannot be revoked or taken away from its owner.
Potential loss can occur if the owner loses access to their private key, or if the owner forgets to renew their name.
## Root Registry
### Who owns the ENS rootnode? What powers does it grant them?
The ENS rootnode is currently owned by the ENS DAO. It used to be owned by the ENS Multi-sig, a group of keyholders from different parts of the ecosystem, however as of [EP4.10](/dao/proposals/4.10) the ownership has been transferred to the ENS DAO.
Ownership of the rootnode grants the ability to do the following:
- Control allocation and replacement of TLDs other than .eth - this is required to implement DNSSEC integration.
- Enable and disable controllers for the .eth registrar, which affect registration and renewal policies for .eth names.
- Update the pricing for .eth names.
- Receive and manage registration revenue.
### Can I register a TLD of my own within ENS?
Yes and No, We consider ENS to be part of the 'global namespace' in co-existence with DNS, and it is our priority to not pollute the namespace.
ENS-specific TLDs are restricted to only '.eth' on Mainnet Ethereum, or .eth and .test on testnets.
By default ENS allows users to [import their DNS name](/learn/dns) through the use of the [DNS Registrar](/registry/dns).
Existing DNS TLDs can [reach out to us](mailto:info@ens.domains) to take control of their TLD.
## What are the differences between ENS and other naming services such as Namecoin or Handshake?
ENS complements and extends the usefulness of DNS with decentralised, trustworthy name resolution for web3 resources such as blockchain addresses and distributed content, while Namecoin and Handshake are efforts to replace all or part of DNS with a blockchain-based alternative.
## Governance Token
### Can I recover tokens accidentally sent to the wrong address?
The answer depends on the address the token was sent to. If you accidentally sent the token to the token.ensdao.eth address (0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72) or the wallet.ensdao.eth address (0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7) then the tokens might be recoverable. Contact the [Meta-governance working group](/dao/stewards/) at the [ENS Forum](https://discuss.ens.domains) and explain the situation. Tokens can only be sent back to the address they were sent from, so if it was sent from an exchange, contact your exchange support to make sure the address can receive tokens.
If the tokens were sent to the null address (0x000..) or an address with a typo, then the tokens are unrecoverable and there's nothing that anyone can do. If the tokens were sent to an exchange or a third party, then contact that third party for help.
---
import { Badge } from '../../components/ui/Badge'
import { Card } from '../../components/ui/Card'
# Hosting a Decentralized Website [Introduction to hosting a decentralized website using ENS]
## ContentHash
The ContentHash is a very popular component of an ENS name, first introduced in [ENSIP-7](/ensip/7).
It can be queried by hitting the [contenthash(bytes32)](/resolvers/interfaces#0xbc1c58d1) function on a name's resolver.
You can also [set the contenthash on a name](/resolvers/interfaces#0x304e6ade) if the resolver supports it.
{['ipfs://qMhx...', 'bzz:/2477', 'ar://HGa8...'].map((tag) => (
{tag}
))}
## Hosting & Pinning
When it comes to hosting your files there are many options to choose from.
{['IPFS / Filecoin', 'Swarm', 'Arweave'].map((tag) => (
{tag}
))}
Popular options include [IPFS](https://ipfs.io), [Swarm](https://ethswarm.org), and [Arweave](https://arweave.org).
Depending on what option you go with your files are either permanently stored on a network,
or require to be actively stored on at least one machine, also known as "pinning".
### Deploy your sites
Several helpful tools and platforms exist that you can use to deploy your website to IPFS, Swarm, or Arweave.
Most notably [fleek](https://fleek.xyz), [Pinata](https://pinata.cloud), and [Blumen](https://blumen.stauro.dev/).
Helping you easily deploy your website to a decentralized storage network.
## Setting your ContentHash
If you are using the public resolver (the default for names registered using the ENS Manager App), you can set the contenthash directly from within the [ENS Manager App](https://app.ens.domains).
If you are using a custom resolver, or are writing your own resolver you will be able to have more fine grained control over the contenthash field.
See [ENSIP-7](/ensip/7) for more information on the contenthash field.
## Browser Support & Gateways
In the ideal world every browser supports decentralized websites out of the box.
If you are using [MetaMask](https://metamask.io) or [Brave Browser](https://brave.com) you can already access IPFS websites directly.
On non-conforming browsers you can use a gateway such as [eth.link](https://eth.link) or [eth.limo](https://eth.limo) to access your website.
You can test if your browser supports decentralized websites by visiting [ens.eth](https://ens.eth) or use a gateway via [ens.eth.link](https://ens.eth.link).
---
import { Table } from '../../components/ui/Table'
import ensips from '../../data/generated/ensips.json'
# ENS Improvement Proposals
This page contains a summary of all the ENS Improvement Proposals (ENSIPs) that have been proposed, and their current status.
Improvement Proposals have included anything from new contract features, to text record standards, protocol features, and more.
## ENSIPs
[ensip.title, ensip.status])}
/>
## Propose an ENSIP
Feel free to [open a pull request](https://github.com/ensdomains/ensips/pulls) on the `ensdomains/ensips` repository.
---
import { Button } from '../components/ui/Button'
# 🪲 Bug Bounty Program
The ENS bug bounty program rewards anyone who finds a bug in covered ENS smart contracts and ENS Labs assets. This page provides a brief overview of the program which is operated by Immunefi and ENS Labs.
[See the full program](https://immunefi.com/bug-bounty/ens)
## Bounties 💸
Reward sizes are guided by the rules below, but are in the end, determined at the sole discretion of the ENS Labs team.
### Smart Contracts
- **Critical**: up to $250,000 USD
- **High**: up to $150,000 USD
- **Medium**: up to $100,000 USD
### Websites and Applications
- **Critical**: up to $50,000 USD
- **High**: up to $20,000 USD
- **Medium**: up to $5,000 USD
- **Low**: up to $1,000 USD
The ENS Labs team reserves the right to adjust bounty amounts at any time in the future.
---
# 📝 Changelog
This page contains a list of changes and events that happened to the ENS protocol & ecosystem.
## Dentity Announcement
On August 21st, 2024 the ENS Labs team announced a new integration with Dentity, an independent identity provider that allows users to verify information and share it on their ENS profile.0
This integration leverages a draft ENSIP that allows for W3C Verifiable Credentials to be stored inside ENS profiles.
## ENSv2 Announcement
On March 28th, 2024 the ENS Labs team announced our plans and roadmap for scaling ENS to the entire internet and beyond.
This involves migrating .eth registrations to a brand new system, in addition to improving support for existing L2 solutions.
You can read more [on our blog](https://blog.ens.domains/post/ensv2), [on X](https://twitter.com/ensdomains/status/1795440186513576318), and [the forums](https://discuss.ens.domains/t/technical-feedback-thread-for-ensv2/19233).
---
# Creating a Subname Registrar
In the [Use Cases](/wrapper/usecases#sell-or-rent-subnames) section, we talked about the ability to stand up your own "registrar" to allow other people to register/claim subnames automatically. Maybe you want to give wrapped subnames out for free, or maybe you want to charge for them. Maybe you want to apply specific rules to the subnames, such as only allowing alphanumeric names. All of this is possible, and this article will break down what you need to do. It's recommended to first read the [Use Cases](/wrapper/usecases#sell-or-rent-subnames) section to get an overview of the decisions you'll need to make.
## Prerequisites
This guide assumes that your parent name (such as `myname.eth`) is already wrapped. If you're not sure whether your name is wrapped, look at the More tab on the Manager app. If the name is unwrapped, it will say so, and it will show you a "Wrap Name" button.
If you want to issue [Emancipated](/wrapper/states#emancipated) subnames, or subnames with [any other fuses](/wrapper/fuses) burned, then your parent name must first be [Locked](/wrapper/states#locked). You can do this on the Permissions tab in the ENS manager app.
:::note
Locking your name (in other words revoking the permission to unwrap) is an **irreversible** change. After you lock the name, you will no longer be able to unwrap it. This is a security guarantee for the holders of all subnames. It ensures that the owner of the parent name cannot get around the security guarantees of the Name Wrapper.
Best to do this on a testnet (Sepolia/Holesky) name first, for development or testing purposes.
:::
## Creating and Deploying your Registrar Contract
In order to create a new subname, your contract should call either `setSubnodeOwner` or `setSubnodeRecord` on the [NameWrapper contract](/learn/deployments#deployments). Also pass in the fuses and expiry at the same time, as needed.
```solidity
NameWrapper.setSubnodeOwner(bytes32 parentNode, string label, address owner, uint32 fuses, uint64 expiry)
// For example
setSubnodeOwner(
0x6cbc..., // The namehash of the parent node, e.g. "myname.eth"
"sub", // The label of the subname to create
0x1234..., // The address you want to be the owner of the new subname
65536, // The fuse bits OR'd together, that you want to burn
2021232060 // The expiry for the subname
)
NameWrapper.setSubnodeRecord(bytes32 parentNode, string label, address owner, address resolver, uint64 ttl, uint32 fuses, uint64 expiry)
// For example
setSubnodeRecord(
0x6cbc..., // The namehash of the parent node, e.g. "myname.eth"
"sub", // The label of the subname to create
0x1234..., // The address you want to be the owner of the new subname
0x5678..., // The address of the resolver to set for the new subname
0, // The TTL to set for the new subname
65536, // The fuse bits OR'd together, that you want to burn
2021232060 // The expiry for the subname
)
```
Your public-facing registration function would typically take at _least_ the parent node (namehash) and subname label as inputs, such as:
```solidity
register(bytes32 parentNode, string calldata label)
```
Then under the hood, your contract will call `setSubnodeRecord` and fill in the rest of the parameters on behalf of the user:
- owner: Typically the caller account, `msg.sender`
- resolver: Typically the default public resolver, `resolver.eth`
- ttl: 0
- fuses: Up to you and your goals. See the [Use Cases](/wrapper/usecases#sell-or-rent-subnames) section for a discussion on this. Typically 65536 for an enamcipated rental subname, or 327680 for an emancipated "forever" name.
- expiry: Up to you and your goals. If you are renting subnames for a particular length of time, this expiry would reflect that. If you are allowing registration of "forever" names, then you can just set the expiry equal to the parent name's current expiry.
Of course, if you want to give the registrant more power/convenience, you could allow some of those parameters to be passed in to your public register function as well.
### Setting Resolver Records
If you want your subname registrar to set records on a subname in the same registration transaction, then the flow will be slightly different. In that case, perform these steps:
- Call `setSubnodeOwner`, setting the _contract itself_ (`address(this)`) as the owner of the subname, temporarily. This first step is needed for the default Public Resolver so that the contract has the authority to set records for the subname.
- Call whatever [resolver methods](/resolvers/interacting) you need to. Perhaps these are records that you want to be pre-set on your subnames (such as an ETH address that the subname points to). Or perhaps these are records that you allow the registrant to pass in, so that they can register their subname and set whatever records they want all in one transaction.
- Call `setSubnodeRecord`, but this time set the owner to the actual intended owner of the subname. This is the point at which you should set the appropriate fuses and expiry you want to, as well.
In addition, you will need to make sure your contract follows the [ERC-1155 Token Receiver rules](https://eips.ethereum.org/EIPS/eip-1155#erc-1155-token-receiver). This means implementing the `onERC1155Received` and `onERC1155BatchReceived` methods, and signaling support for them in your ERC-165 `supportsInterface` method. OpenZeppelin has an easy abstract contract you can include for all this: [ERC1155Holder.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/utils/ERC1155Holder.sol)
### Taking fees
If you are setting up a "rental" registrar, then your registration function should require a certain amount of ETH to be sent in as well.
Alternatively, you could choose to allow users to spend ERC-20 tokens instead. To accomplish that, you would typically call the ERC-20 method `transferFrom` on the token contract. This also means that the registrant would first need to approve your contract as a spender for that token, meaning they would need to execute a separate approval transaction first (either to approve unlimited spending, or to approve the specific number of tokens needed to register the subname).
### Reference Implementation
Luckily, you don't need to start from scratch! The ENS Labs devs have created some example contracts you can start from:
https://github.com/ensdomains/ens-contracts/tree/feature/subdomain-registrar/contracts/subdomainregistrar
These contracts include two different implementations:
#### Forever Subname Registrar
This is a basic FIFS (First in first serve) registrar. The registration can take a fixed fee, or this fee can be set to 0 if you wish for subnames to be free. Names automatically are set to the parent's expiry can the fuse for `CAN_EXTEND_EXPIRY` will be burnt on registration so the user can extend their expiry if the parent also extends theirs. For a better UX, it is recommended that the parent sets their expiration as high as possible to allow their users to not have to think about renewing.
#### Rental Subname Registrar
This is a basic FIFS (First in first serve) registrar. The key difference between this and the ForeverSubdomainRegistrar is that it does not auto-burn the `CAN_EXTEND_EXPIRY` fuse and instead exposes a `renew()` function that allows paid renewal. This registrar also needs to be paired with a rental-based pricing contract. For simplicity, the deployer can deploy this pricing contract and the UI can pass this address through to `setupDomain()` when a new user wants to setup a subname.
## Setting Everything Up
Once you have a parent name ready and a subname registrar contract deployed, then you just need a few extra steps to set everything up:
### (If needed) Call setupDomain on your contract
This will only apply to you if you have a specific `setupDomain` method or something similar on your contract, such as the [reference implementation](/wrapper/creating-subname-registrar#reference-implementation) contracts do.
Calling this method will "enable" a specific parent name in your subname registrar. It can also allow you to set or update the pricing terms or beneficiary account, if needed.
### Approve your contract
Call `setApprovalForAll` on the NameWrapper contract, approving your subname registrar contract as an operator for any names you own. This allows you to keep ownership of the parent name, and just delegate subname creation to your contract.
### (If needed) Approve token spending
If your registrar contract takes ERC-20 tokens as a registration fee, then a potential registrant will need to approve your contract as a spender first.
### Register a subname
Finally, the registrant will call your public registration method. Upon transaction success, they will own the wrapped name (ERC-1155 NFT) with whatever fuse/expiry guarantees that you setup in your registrar.
If you are allowing "forever" subnames to be registered (meaning that you've burned the `CAN_EXTEND_EXPIRY` fuse on the subnames), then the registrant can extend their own expiry at any time. Note that a subname's expiry can be set up to a maximum of whatever the parent name's expiry is.
And that's it!
---
# Name Wrapper Fuses
A "fuse" is a permission or perk that can be granted/revoked on a name. As the name implies, once the fuse is "burned", it cannot be unburned.
Fuses will only reset when the **expiry** is reached. In the ENS Manager UI, this is available in the "Permissions" section of the name.
By **wrapped expiry**, we mean that for .eth second-level names (like `name.eth`), this is the end of the 90-day grace period, the time at which the .eth 2LD is truly released. For all other names (such as subnames), there is no grace period, so the expiry is just the expiration date for that specific subname.
For example, by default when you wrap a name, you can transfer that NFT around freely, just as you can with other NFTs. However, if the **`CANNOT_TRANSFER`** fuse is burned, then the NFT becomes non-transferrable. In the ENS Manager UI, you would do this by revoking the "Can send this name" permission.
In order to burn fuses on a name, the parent name must be **Locked** (meaning, you cannot unwrap the name). The reason is, if the parent name was not locked, then the owner of the parent name could simply get around the constraints of the Name Wrapper by unwrapping the name, and replacing/revoking subnames against the core ENS Registry.
There are parent-controlled and owner-controlled fuses:
## Parent-Controlled Fuses
Only the owner of the parent name can burn one of these fuses on a name. These can generally be thought of as "perks" that can be granted to a name, though they can be used in other ways.
| Fuse name | Description |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`PARENT_CANNOT_CONTROL`** | Allows a parent owner to **Emancipate** a child name. After this is burned, the parent will no longer be able to burn any further fuses, and will no longer be able to replace/delete the child name. This fuse must be burned in order for any owner-controlled fuses to be burned on the name. |
| **`IS_DOT_ETH`** | This fuse cannot be burned by users of the Name Wrapper, it is only set internally when a .eth 2LD is wrapped. |
| **`CAN_EXTEND_EXPIRY`** | The owner of the child name will be able to extend their own expiry. Normally, only the parent owner can extend the expiry of a child name. See the [Expiry](/wrapper/expiry) section for more information. |
| **Custom Fuses** | There are 13 other parent-controlled fuses that are not reserved, and can be used in any custom way you want! |
## Owner-Controlled Fuses
Either the owner of the name or the owner of the parent name can burn one of these fuses. These can generally be thought of as "permissions" that can be revoked on a name, though they can be used in other ways.
| Fuse name | Description |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`CANNOT_UNWRAP`** | The name will now be **Locked**, and can no longer be unwrapped. This fuse must be burned in order for any other owner-controlled fuses to be burned on the name. |
| **`CANNOT_BURN_FUSES`** | No further fuses can be burned on the name. |
| **`CANNOT_TRANSFER`** | The name (wrapped NFT) can no longer be transferred. |
| **`CANNOT_SET_RESOLVER`** | The resolver contract for the name can no longer be updated. |
| **`CANNOT_SET_TTL`** | The TTL for the name can no longer be updated. |
| **`CANNOT_CREATE_SUBDOMAIN`** | New subdomains can no longer be created. |
| **`CANNOT_APPROVE`** | The approved "subname renewal manager" for the name can no longer be updated. See the [Approved Operators](#approved-operators) section for more information. |
| **Custom Fuses** | There are 9 other owner-controlled fuses that are not reserved, and can be used in any custom way you want! |
## The Emancipated and Locked States
This is also covered in the Wrapped States section, but here is a quick recap:
All .eth second-level names (like `name.eth`) are automatically placed into the Emancipated state when wrapped.
**Emancipated** means that the parent no longer has control over the child name. It can no longer burn any fuses or replace the subname, up until the expiry.
A name is Emancipated when the parent burns the **`PARENT_CANNOT_CONTROL`** (PCC) fuse. The parent must first be in the Locked state to be able to do this.
**Locked** means that the name cannot be unwrapped. This provides assurance to subnames that the parent owner cannot unwrap and then, for example, start replacing subnames directly against the registry.
An Emancipated name is Locked when the **`CANNOT_UNWRAP`** (CU) fuse is burned.
Think of the special PCC / CU fuses recursively:
- To burn owner-controlled or subname fuses, CU must be burned.
- To burn CU, PCC must be burned.
- Only the parent can burn PCC on the child name, and only if CU is first burned on the parent.
- Only the grandparent can burn PCC on the parent name, and only if CU is first burned on the grandparent.
- And so on...
Follow that chain up until you hit a .eth second-level name like `name.eth`, since .eth second-level names will have PCC automatically burned when wrapping. The parent `eth` node is already in the Locked state.
A parent name can burn all the fuses it needs to on a child name in one transaction. This can be done when the subname is created, or on an existing subname that has not yet been Emancipated.
## DNS Domains and Fuses
Currently, only .eth names support fuses, because only the `eth` node is on-chain native and completely locked beyond anyone's control.
Technically speaking, the owner of a DNS TLD has the ability to burn fuses on that TLD in the Name Wrapper, and set it to the "Locked" state. And then from there, all subnames under that DNS TLD _will_ be able to use fuses.
The DNS TLD owner would need to:
- Request the Controller of that TLD from the ENS DAO
- Wrap the TLD node in the Name Wrapper
- Burn the **`PARENT_CANNOT_CONTROL`** and **`CANNOT_UNWRAP`** fuses on the wrapped TLD to lock it
However, this still does not have all the immutable guarantees that .eth names do. This is because for DNS names, the "source of truth" always lies not in the Ethereum network, but in the DNS network, and the DNS root zone governed by ICANN stakeholders.
So even if the DNS TLD owner "Locks" that TLD in the ENS Name Wrapper, if that TLD were to ever change ownership on the DNS side, then (per the [ENS DAO Constitution](https://docs.ens.domains/v/governance/ens-dao-constitution#iv.-ens-integrates-with-the-global-namespace)) the new owner would be able to override control of that TLD on the ENS side, unwrap it, and replace/revoke all 2LDs. This is just something to keep in mind for wrapped DNS domains.
Even if wrapped DNS domains do not support fuses, you can still use them as ERC-1155 NFTs. They will still have their own NFT metadata and show up in your wallet, with whatever avatar you have set, etc. They just won't have all the extra functionality that comes with the fuse/permission system.
---
import { Card } from '../../components/ui/Card'
# Name Wrapper Expiry
In order to burn any fuses on a name, you must also set an **expiry** on it. This expiry determines how long any burned fuses are active for, and may also determine whether the name itself has expired.
If the name is a .eth 2LD, then the expiry will automatically be set to the same expiry in the .eth Registrar. But for all other names, the parent can choose what expiry to set for a child name.
## Max Expiry for Subnames
By default, the expiry for a name can only be set by the parent, and can only be increased, not decreased. The maximum value for the expiry of a name is the expiry of its parent name.
For example, say a name expires in 5 years. The owner of the name can then set the expiry of its subnames to a maximum of 5 years as well. But the parent could also choose to set the expiry to something less. Let's say the parent sets the expiry of one of its subnames to 2 years.
Then in turn, the owner of the subname can set the expiry of its own subnames up to a maximum of 2 years, but it could also set it to something less, like 1 year.
The parent can set a different expiry for different subnames too, just as it can burn different fuses for different subnames.
## Renewals
When a wrapped .eth second-level name (like `name.eth`) is renewed, that new expiry is automatically set in the Name Wrapper as well as in the .eth Registrar. However, the expiry for any other .eth names (like `sub.name.eth`) will not be automatically extended when the parent expiry is extended.
The parent can extend the expiry for an existing subname at any time, even if the subname has been emancipated.
The parent can also choose to approve a separate contract to allow the expiry for subnames to be extended by the subname owner or other accounts.
That is basically how .eth second-level names work: Since the `eth` node is locked in the registrar contract and has the Name Wrapper (which exposes a renew method) approved as a controller, .eth second-level names can be directly renewed by their owners.
The parent can further lock this approved contract in by burning the **`CANNOT_APPROVE`** fuse.
There is also a special parent-controlled fuse called **`CAN_EXTEND_EXPIRY`**. If the parent burns this fuse on a subname, then the owner of that subname (or any approved controller) can also extend the expiry.
So, if you are running a subname registrar and you want to enable "unruggable renewals", you can use one of the above options (or both).
## Special Cases for .eth 2LDs
For .eth second-level names, the end of the name's grace period (from the .eth Registrar) is used for the expiry inside of the Name Wrapper.
So if the name's expiration date in the Registrar is January 1st, then the expiry in the Name Wrapper will reflect that date _plus_ the grace period (currently 90 days, so approximately April 1st, depending on the year).
When the name's expiration date (from the .eth Registrar) has been reached, and the name is now in the grace period, all Name Wrapper operations on the name will be restricted.
The owner will _not_ yet lose ownership of the name, but they will also not be able to unwrap or update the name until it has been renewed.
## Expiry Implications
When a name is merely **Wrapped** but not **Emancipated** or **Locked**, parent-controlled fuses can still be burned. This means that the parent can burn a custom fuse for a limited amount of time.
When the expiry (end of grace period for .eth 2LDs) is reached, all fuses will be reset, but the name will otherwise be unaffected.
When a name is **Emancipated** or **Locked**, the expiry has an important additional effect. In this scenario, when the expiry (end of grace period for .eth 2LDs) has been reached, **the name itself will expire**, and the owner **loses ownership** of the name.
---
# Name Wrapper Use-Cases
## Lock the resolved records for a name
By default, newly registered names will use the Public Resolver, which just allows the current manager/controller of the name to update any records.
However, in some cases perhaps you want to make sure that a name resolves to specific records and **never** changes. You can accomplish this with the **`CANNOT_SET_RESOLVER`** fuse.
Say you own `mycoolcontract.eth` representing a smart contract. You can use ENS subnames to refer to specific versions of that contract, like `1.mycoolcontract.eth`. And perhaps you want those versioned subnames to always point to:
- The ETH address of that immutable contract
- The ABI for that contract
- The contenthash for some versioned documentation page
- etc.
One way to do this is just to make sure the name is **Locked**, all the records are set correctly, and then transfer the owner to some burn address so it can never be updated again.
But of course this isn't ideal, because maybe there are some records that you _do_ want to update in the future. Or maybe you still want to keep ownership of that subname for other reasons.
Instead of essentially burning the name, you could create a custom resolver that locks in certain records forever. Then:
1. Set the resolver of that name to your custom contract
2. Set the records however you want and lock them into the resolver
3. Burn these fuses on the name:
- `PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_SET_RESOLVER`
Now you can still keep ownership and even some limited management power over the name, while still guaranteeing that the ETH address, ABI, and whatever other records are completely immutable, as long as the expiry is set appropriately.
## Issue subdomains as tickets to an event
Maybe you have `mycoolevent.eth` and you want to issue tickets like `1.ticket.2023.mycoolevent.eth`.
If you want, you can choose to not Emancipate those subnames, but still burn some custom parent-controlled fuses. Those fuses might:
- Indicate what "tier" their event ticket is
- Maybe they can upgrade their ticket to a higher tier, which would burn some additional fuses
- Allow them access to the express line or some VIP room
- Maybe even automatically via some smart door
When you burn those fuses, perhaps you also set the expiry to the day after the event ends.
Or, maybe you want your attendees to be able to keep their subnames as a souvenir or proof-of-attendance!
If so, then instead of letting the names expire at the end of the event, you could extend the expiry and burn some additional fuses to allow the attendees to keep them forever! In that case you might want to burn these fuses:
- `CAN_EXTEND_EXPIRY | PARENT_CANNOT_CONTROL`
If you want those tickets to be non-transferrable (soulbound to the address that attended), then burn these fuses:
- `CAN_EXTEND_EXPIRY | PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER`
## Sell or rent subnames
### I want to sell / rent out subnames!
Say you own the wrapped name `verypopularname.eth`. Obviously you can just manually create wrapped subnames like `my.verypopularname.eth` and then sell them on an NFT marketplace. But that sure doesn't scale well.
To accomplish this, you will want to create a **subname registrar**. This is a contract that will handle all the registration / renewal for you, and then users will be able to interact with that contract in order to register their own subnames.
In fact, this is exactly how .eth 2LDs are registered. The owner of the `eth` TLD (the NFT contract) delegates registration / renewal to the ETHRegistrarController contract. It is acting as a subname registrar for the name `eth`.
Your contract would expose a `register` method that anyone can call. Under the hood it will use the [setSubnodeOwner](https://github.com/ensdomains/ens-contracts/tree/master/contracts/wrapper#setsubnodeowner) or [setSubnodeRecord](https://github.com/ensdomains/ens-contracts/tree/master/contracts/wrapper#setsubnoderecord) methods to create subnames, passing in the **fuses** and **expiry** you want to set.
### What fuses should I burn???
First, note that if you want to burn any fuses on subnames, then your name must be **Locked** (meaning **`CANNOT_UNWRAP`** is burned).
Assuming that you want your subnames to be "unruggable", such that you cannot replace / revoke them, then you will want to burn **`PARENT_CANNOT_CONTROL`** on the subnames. This will place them in the **Emancipated** state upon registration.
If you want to sell "forever" subnames, where users register once and can then keep them for as long as they wish, then you can consider burning the **`CAN_EXTEND_EXPIRY`** fuse.
This will allow the subname owner to extend their own expiry whenever they want. The max expiry is the expiry of the parent name, but the .eth Registrar allows _anyone_ to renew/extend a .eth 2LD as well.
If you just want to **rent** subnames, then do not burn **`CAN_EXTEND_EXPIRY`**. Instead, you could include a `renew` method on your contract that users can call for another fee.
If you want to enable "unruggable renewals" for your registrar, to guarantee that users will always be able to renew, then you can call `approve` on the Name Wrapper and approve your registrar contract as the "subname renewal manager" for your name.
Then, burn the **`CANNOT_APPROVE`** fuse on your name, to guarantee that you can never revoke that contract for subname renewals. See the Approved Operators section above for more info.
If you want to impose other restrictions on your registered subnames, then you can burn the **`CANNOT_UNWRAP`** fuse to Lock the subname, and also burn whatever other fuses you want.
For example, if you want to prevent owners of your subnames (like `my.verypopularname.eth` from creating their own subnames (like `buy.my.verypopularname.eth`), then you would burn **`CANNOT_UNWRAP`** and **`CANNOT_CREATE_SUBDOMAIN`**.
To recap on fuses...
- Sell permanent names:
- `CAN_EXTEND_EXPIRY | PARENT_CANNOT_CONTROL`
- Sell permanent names, but prevent them from creating their own subnames:
- `CAN_EXTEND_EXPIRY | PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_CREATE_SUBDOMAIN`
- Rent out names:
- `PARENT_CANNOT_CONTROL`
- Rent out names, but prevent them from transferring or reselling them:
- `PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER`
And so on, it's up to you. You can also burn whatever custom parent-controlled or owner-controlled fuses you want to.
### Can I customize my own rules and fees?
Yes! It's your registrar contract, so you can impose whatever rules and fees you want.
For example, the .eth Registrar imposes a 3-character minimum on all names, as well as a custom fee structure and a temporary premium auction upon expiration.
By default there is no character limit on subnames, but your contract could have its own rules and fee structure or whatever you want. For example, you can:
- Allow or disallow specific addresses from registering / renewing
- Only allow registration based on some custom criteria like holding a specific NFT
- Custom length restrictions like only 3+ characters or < 100 characters
- Only allow names with characters `[a-z0-9]` and nothing else
- Use a custom fee structure based on:
- The length of the name
- The specific characters that are in the name, like emojis
- A pre-curated list of "good" names like people's first names
- And whatever other rules you want.
### More information
See this page for a step-by-step guide on creating and setting up your own subname registrar: Creating a Subname Registrar
There is even a set of reference implementation contracts you can use as a starting base!
## Give subnames out to NFT holders
### I want to give subnames out to all of my DAO members / NFT holders!
Say you own the wrapped name `mycoolnft.eth`, representing a popular NFT project you created. You want to distribute subnames like `6529.mycoolnft.eth` to all holders.
One option is to just bulk create the subnames and drop the wrapped NFTs into their wallets. This might be good at least as an initial drop, because then the holders don't need to interact with any contract or spend any gas, you're doing that for them!
To create the subnames, you'd use the setSubnodeOwner or setSubnodeRecord methods.
You must also decide:
### How much control over the subnames do you want to relinquish?
Do you want to be able to revoke subnames? Or do you want them to be completely outside your control?
One thing to consider is whether you want the **current** holder of your NFT to always be able to claim/reclaim the corresponding ENS subname. If so, then you will **not** want to Emancipate those subnames (in other words, do not burn **`PARENT_CANNOT_CONTROL`**).
If the subname is Emancipated, then the NFT holder could sell/transfer the NFT but keep the subname (up until the expiry).
To make it easy for anyone to claim/reclaim a subname after your initial drop, you can set up a contract for this.
### Setting up a subname claim contract
The claim method of your contract could:
1. Call `ownerOf` or `balanceOf` on your NFT contract to get or verify the current owner of the NFT
2. Call `ownerOf` or `balanceOf` on the ENS Name Wrapper contract to get or verify the current owner of the wrapped subname
- If both owner addresses are the same, just return, nothing to do
3. Call `setSubnodeOwner` or `setSubnodeRecord` on the ENS Name Wrapper:
- **owner:** The current owner of the NFT
- **fuses:** What fuses you want to burn (if any) on that subname. If you burn any fuses, you must also set an expiry.
- **expiry:** When the subname will expire.
Then, to give that contract access to create subnames on your behalf, you would call `setApprovalForAll` on the Name Wrapper to approve your contract as an operator.
Now, even if the NFT gets sold / transferred, the new owner will be able to claim their `mycoolnft.eth` subname at any time.
In addition, if you expand your NFT collection in the future and there are new owners, then those new owners would be able to claim their subnames as well.
If you are creating a new NFT contract, you could even bake this functionality **directly into the NFT contract** too, instead of needing a separate contract! By doing this, you wouldn't need a separate `claim` method either, your NFT contract would just **automatically transfer the wrapped ENS subname** whenever the NFT itself gets transferred!
### Giving your subname owners perks
If you decide to not Emancipate the subnames that you issue, you _will_ still be able to burn any Parent-Controlled Fuses. There are 13 unreserved parent-controlled fuses that you can use however you wish!
For example, perhaps you want to grant on-chain "perks" or "roles" to certain holders. You would call [setChildFuses](https://github.com/ensdomains/ens-contracts/tree/master/contracts/wrapper#setchildfuses) on the Name Wrapper and pass in the fuses you want to burn, and the expiry.
This means that those "perks" or "roles" can also be time-boxed if you want. Maybe a perk expires in 1 week or something, up to you.
There is also the reserved **`CAN_EXTEND_EXPIRY`** parent-controlled fuse. If you burn this, then the subname owner will be able to extend their own expiry whenever they want.
---
# Name Wrapper Contract Details
The Name Wrapper contract is deployed on these chains:
- Mainnet: [wrapper.ens.eth](https://etherscan.io/address/0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401#code) ( `0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401` )
- Sepolia: [wrapper.ens.eth](https://sepolia.etherscan.io/address/0x0635513f179D50A207757E05759CbD106d7dFcE8#code) ( `0x0635513f179D50A207757E05759CbD106d7dFcE8` )
## Wrapping and Unwrapping
When wrapping a .eth 2LD, you transfer the Owner (Registrant) of the ERC-721 NFT to the Name Wrapper contract.
The contract will then automatically take over the Manager (Controller) for the name as well.
You can do this by calling the wrapETH2LD method. Or, you can directly transfer the ERC-721 NFT to the Name Wrapper contract. In return, the contract issues you an ERC-1155 NFT.
```solidity
NameWrapper.wrapETH2LD(string label, address wrappedOwner, uint16 ownerControlledFuses, address resolver)
// For example
wrapETH2LD(
"myname", // "myname.eth" but only the label
0x1234..., // The address you want to own the wrapped name
0, // The owner-controlled fuse bits OR'd together, that you want to burn
0x1234... // The address of the resolver you want to use
)
```
When wrapping any other ENS name, you transfer the Manager (Controller) of the name to the Name Wrapper contract. You can do this by calling the wrap method. In return, the contract issues you an ERC-1155 NFT.
```solidity
NameWrapper.wrap(bytes name, address wrappedOwner, address resolver)
// For example
wrapETH2LD(
0x03737562046e616d650365746800, // The DNS-encoded version of "sub.myname.eth"
0x1234..., // The address you want to own the wrapped name
0x1234... // The address of the resolver you want to use
)
```
As the owner of the wrapped name, you can unwrap at any time by calling either unwrapETH2LD or unwrap. You can do this as long as the permission to unwrap has not been revoked.
```solidity
NameWrapper.unwrapETH2LD(bytes32 labelhash, address registrant, address controller)
// For example
unwrapETH2LD(
0x952f..., // "myname.eth" but only the labelhash: keccak256('myname')
0x1234..., // The address you want to own the unwrapped name
0x1234... // The address you want to be the manager of the unwrapped name
)
NameWrapper.unwrap(bytes32 parentNode, bytes32 labelhash, address controller)
// For example
unwrap(
0x6cbc..., // The namehash of the parent node, e.g. "myname.eth"
0xfa1e..., // The labelhash of the child to unwrap, e.g. keccak256('sub')
0x1234... // The address you want to be the manager of the unwrapped name
)
```
## Burning Fuses / Setting Expiry
If you are wrapping an existing .eth 2LD, then you can pass in the owner-controlled fuses at that time, see the above [Wrapping and Unwrapping](#wrapping-and-unwrapping) section. If you are creating a new subname, and you want to burn fuses at the same time, see the below [Creating Subnames](#creating-subnames) section.
For other existing wrapped names, you can burn fuses with either the `setFuses` or `setChildFuses` methods.
The `setFuses` method is used for a name that you own, but you do not necessarily own the parent of. You have the ability to burn any [Owner-Controlled Fuses](/wrapper/fuses#owner-controlled-fuses) you want. Note that your name must first be [Emancipated](/wrapper/states#emancipated) in order for you to be able to burn any owner-controlled fuses. All .eth 2LDs are automatically emancipated upon wrapping.
When burning owner-controlled fuses, at a minimum you must burn the **`CANNOT_UNWRAP`** fuse (if it has not already been burned).
```solidity
NameWrapper.setFuses(bytes32 node, uint16 ownerControlledFuses)
// For example
setFuses(
0x6cbc..., // The namehash of the node, e.g. "myname.eth"
1 // The owner-controlled fuse bits OR'd together, that you want to burn
)
```
The `setChildFuses` method is used for a subname that you own the parent of. As long as the subname has not yet been [Emancipated](/wrapper/states#emancipated), you can burn whatever [Parent-Controlled Fuses](/wrapper/fuses#parent-controlled-fuses) and [Owner-Controlled Fuses](/wrapper/fuses#owner-controlled-fuses) you want. At the same time, you must set an expiry for those fuses, if one is not already set. Note that your name must first be [Locked](/wrapper/states#locked) in order for you to burn fuses on any subnames.
If you are only burning parent-controlled fuses, then there are no further restrictions. However, if you are burning owner-controlled fuses, then you must at a minimum burn both **`PARENT_CANNOT_CONTROL`** and **`CANNOT_UNWRAP`** on the subname to lock it at the same time.
```solidity
NameWrapper.setChildFuses(bytes32 parentNode, bytes32 labelhash, uint32 fuses, uint64 expiry)
// For example
setChildFuses(
0x6cbc..., // The namehash of the parent node, e.g. "myname.eth"
0xfa1e..., // The labelhash of the child, e.g. keccak256('sub')
65537, // The fuse bits OR'd together, that you want to burn
2021232060 // The expiry for the subname
)
```
## Creating Subnames
This is done very similarly to how unwrapped subnames are created. You call either `setSubnodeOwner` or `setSubnodeRecord` on the wrapper contract. When a name is wrapped, all subnames created will also be wrapped by default.
You can also pass in the fuses and expiry at the same time, so that the subname will be created in the fuse/permission state that you want, without needing to perform an extra transaction.
```solidity
NameWrapper.setSubnodeOwner(bytes32 parentNode, string label, address owner, uint32 fuses, uint64 expiry)
// For example
setSubnodeOwner(
0x6cbc..., // The namehash of the parent node, e.g. "myname.eth"
"sub", // The label of the subname to create
0x1234..., // The address you want to be the owner of the new subname
65536, // The fuse bits OR'd together, that you want to burn
2021232060 // The expiry for the subname
)
NameWrapper.setSubnodeRecord(bytes32 parentNode, string label, address owner, address resolver, uint64 ttl, uint32 fuses, uint64 expiry)
// For example
setSubnodeRecord(
0x6cbc..., // The namehash of the parent node, e.g. "myname.eth"
"sub", // The label of the subname to create
0x1234..., // The address you want to be the owner of the new subname
0x5678..., // The address of the resolver to set for the new subname
0, // The TTL to set for the new subname
65536, // The fuse bits OR'd together, that you want to burn
2021232060 // The expiry for the subname
)
```
## Approved Operators
### Full-Control Operator Batch Approvals
Your wrapped name is an ERC-1155 NFT that supports the `setApprovalForAll` method. When you approve an address using this method, it will have **full control** over all wrapped ENS names that you own.
This method is typically used by NFT marketplace contracts.
### Name-Specific Subname Renewal Manager Approvals
The Name Wrapper also supports the ERC-721 `approve` method. This method is used to approve a single "Subname Renewal Manager" for a specific name.
The "Renewal Manager" does not have full control over your wrapped name, it can only set / extend the expiry on subnames.
Further, if you burn the **`CANNOT_APPROVE`** fuse on your name, then the approved renewal manager can no longer be changed. You can use this to "lock in" that contract, so that you can guarantee to all subname owners that renewals/extensions can always be done.
This approved renewal manager will be reset if the wrapped NFT is burned or re-minted, which happens if you unwrap the name, or if an expired name gets re-registered. It will also be reset if the wrapped NFT is transferred, **unless** the **`CANNOT_APPROVE`** fuse is burned.
### Example - Subname Registrar Contract
You can use these operator approval methods to setup a separate contract that can take certain actions on your behalf. One example is setting up a "subname registrar" to allow users to register/renew subnames.
That subname registrar contract would act on your behalf and allow users to register subnames. To allow this, you would call `setApprovalForAll` to give that contract full control over your name (and thus the ability to create subnames).
Then, to enable "unruggable renewals", you could call `approve` on that same contract (or a separate one specific to renewals if you wish) and burn **`CANNOT_APPROVE`** to lock in subname renewals for that contract.
If you need to later on, you would still be able to revoke with `setApprovalForAll`. So the contract would lose full control over your name (and the ability to create new subnames), but it would still be able to perpetually renew/extend existing subnames.
And you can do all of this **without** needing to send your wrapped NFT to that contract.
---
# Wrapped States
```mermaid
graph LR;
unregistered((Unregistered));
unwrapped(Unwrapped);
wrapped(Wrapped);
emancipated(Emancipated);
locked(Locked);
unregistered--->|register|unwrapped;
unwrapped--->|wrap|wrapped;
wrapped--->|unwrap|unwrapped;
wrapped--->|protect|emancipated;
emancipated--->|lock|locked;
emancipated--->|unwrap|unwrapped;
emancipated--->|expire|unregistered;
locked-->|expire|unregistered;
```
Taking the Name Wrapper into account, an ENS name can be in one of these possible states:
### Unregistered
The name has not even been registered/created yet, or it has expired.
### Unwrapped
The name exists and has not expired (in the case of .eth second-level names). The Name Wrapper contract does not have ownership over the name. You own the name in the registry and/or .eth registrar.
### Wrapped
The Name Wrapper contract has ownership of the name (in the registry/registrar). You are issued an ERC-1155 NFT in return, which proves that you are the actual owner.
You can unwrap the name at any time, which burns the ERC-1155 NFT, and returns ownership in the registry/registrar back to you.
If your name is a subname like `sub.name.eth`, then the owner of `name.eth` can technically replace the subname and transfer it to a different owner.
In addition, the parent owner can burn parent-controlled fuses on your name.
### Emancipated
The owner of the parent name is no longer able to replace this name, or burn any additional fuses on it. All .eth second-level names (like `name.eth`) are automatically put into the Emancipated state when they are wrapped.
The name can still be unwrapped and rewrapped by the owner.
### Locked
The name can no longer be unwrapped. The owner can now burn owner-controlled fuses on the name. Fuses for subnames of this name can now be burned as well.
---
import { Card } from '../../components/ui/Card'
# Name Wrapper Overview
:::note
Are you looking for user-facing guides on how to interact with the Name Wrapper in the ENS Manager App? If so, see here instead: Name Wrapper Guides
:::
The **Name Wrapper** is a contract for ENS that allows you to "wrap" any ENS name into a ERC-1155 NFT.
## Without the Name Wrapper
Before the Name Wrapper, only .eth 2LDs (second-level domains, like `ens.eth`) had ERC-721 NFTs associated with them, unless the owner created a separate custom contract.
## With the Name Wrapper
**Parent-Controlled Fuses:**
- [Fuses](/wrapper/fuses) that only the parent owner can burn
- "Perks" that can be given to the owner of a name
**Example:** By burning `CAN_EXTEND_EXPIRY`, you allow the owner to
extend/renew their own subname
**Owner-Controlled Fuses:**
- [Fuses](/wrapper/fuses) that either the owner or parent owner can burn
- "Permissions" that can be revoked on a name
**Example:** By burning `CANNOT_TRANSFER`, the wrapped NFT can no longer be
transferred or sold.
**Subname Fuses:**
- The parent owner has the power to burn fuses when creating subnames
- Decides what perks, permissions, or guarantees to give to subname owners
With this new contract, you can wrap:
- Any .eth name or subname (e.g. `name.eth`, `sub.name.eth`)
- Any DNS name or subname (e.g. `name.com`, `sub.name.com`)
Unwrapped .eth 2LDs have the concept of a separate **Owner** (Registrant) and **Manager** (Controller).
This changes after you wrap the name, because there is only a single account that serves as both the **Owner** and **Manager** for the wrapped name.
---
import { EmbedLink } from '../components/EmbedLink'
# Terminology
This page contains a glossary of terms used in the ENS documentation.
## Name
An ENS identifier such as 'alice.eth'. Names may consist of multiple parts, called labels, separated by dots. This also includes DNS names like `name.xyz`, or subnames like `sub.name.eth`.
## 2LD
Second-level domain.
This refers to a subname/subdomain of a top-level domain.
For example, `name.eth` and `name.com` are both second-level names.
A subname of a 2LD is a third-level domain or 3LD.
## Subname / Subdomain
A child name like `sub.name.eth`, whose parent is `name.eth`. Also referred to as a "subdomain". Every name (except for the root node) has a parent. For example, `name.eth` is a subname of `eth`.
```
vault.luc.eth
```
## TLD
Top-level domain. This refers to names like `eth`, `com`, `xyz` which lie at the "top" of the naming hierarchy.
```
.eth .com .xyz
```
## Controller
The account that may edit the records of a name. The Controller may be changed by the Registrant or Controller.
## Label
An individual component of a name, such as 'alice'.
## Labelhash
The keccak256 hash of an individual label.
## Namehash
The algorithm used to process an ENS name and return a cryptographic hash uniquely identifying that name. Namehash takes a name as input and produces a _node_.
## Node
A cryptographic hash uniquely identifying a name.
## Owner
The owner of a name is the entity referenced in the ENS registry's owner field. An owner may transfer ownership, set a resolver or TTL, and create or reassign subdomains.
## Record
A piece of information that an ENS name "resolves" to (points to). The most common record is the ETH Address record, which determines what ETH 0x address an ENS name points to.
## Registration
A registration is a registrar's record of a user's ownership of a name. This is distinct from the owner field in the Registry; registrations are maintained in the registrar contract and additionally store information on expiry date, fees paid, etc.
### Registrar
A registrar is a contract responsible for allocating subdomains. Registrars can be configured at any level of ENS, and are pointed to by the owner field of the registry.
### Registrant
The owner of a registration. The registrant may transfer the registration, set the Controller, and reclaim ownership of the name in the registry if required.
### Registry
The core contract of ENS, the registry maintains a mapping from domain name (at any level - x, y.x, z.y.x etc) to owner, resolver, and time-to-live. All lookups start with the Registry.
### Expiry
The date and time at which an ENS name expires.
The implications of expiration depend on the type of name it is.
When a .eth 2LD expires (and its grace period elapses), then you lose ownership of the name.
When a wrapped subname expires, you may or may not lose ownership, depending on whether the name was emancipated.
### Grace Period
This is a short window of time after an ENS .eth name expires, in which the owner can still renew and retain the name. Currently this window is 90 days.
### TTL
Stands for "Time To Live". This is a field in the core registry that can be set alongside the resolver. It can be used as a hint for clients to decide how long to cache resolved data.
## DNS
This is the Domain Name Service used by the internet to resolve addresses and other records from human-readable names. ENS aims to be fully complementary and compatible with DNS, and supports easy importing of DNS names via a special [DNSSEC](#dnssec) registrar.
### DNSSEC
Stands for Domain Name System Security Extensions. When a particular DNS TLD supports DNSSEC, then the owners of names can cryptographically sign records. This allows ENS to support easy importing of DNS names into the ENS registry, as the owner of the DNS name can prove ownership with those signed records.
## Resolver
A resolver is a contract that maps from name to the resource (e.g., cryptocurrency addresses, content hash, etc). Resolvers are pointed to by the resolver field of the registry.
### Wildcard Resolver
This refers to a resolver that supports [ENSIP-10](/ensip/10). This scheme allows clients to resolve data for subnames that either don't have a resolver of their own, or subnames that may not even exist on-chain at all. For offchain names, this is typically used in conjunction with [CCIP Read](#ccip-read).
## Public Resolver
This is a standard resolver contract implementation written by ENS Labs. It supports all record types and anyone can use it. This is the default resolver used when registering a new name via the official manager app.
## Offchain
This term is typically used with respect to the Ethereum Mainnet blockchain. If data is not posted to the chain via an actual Ethereum Mainnet transaction, then it is "offchain". ENS names can also be offchain. For example names can use a special resolver to resolve records for subnames that don't exist on-chain in the Registry. This is also typically done with [CCIP Read](#ccip-read).
### CCIP Read
The "Cross Chain Interoperability Protocol Read" specification, also known as [EIP-3668](https://eips.ethereum.org/EIPS/eip-3668), authored by Nick Johnson, is a specification that allows for secure and trustless offchain data retrieval.
It allows for an Ethereum call to defer to an [offchain gateway](/resolvers/ccip-read#writing-a-gateway) and then securely verify the resulting data on-chain.
With respect to ENS, this is typically used for offchain subnames that don't exist in the core Registry.
## Primary Name
The ENS name that you want a particular ETH account to be associated with. When set, it will be displayed instead of your 0x address on integrating websites/apps. This is also often referred to as the "reverse record".
### Reverse Node
A node in the Registry that can be claimed for any Ethereum account. The name this node represents is `[addr].addr.reverse`, where `[addr]` is the Ethereum public address (lowercase, without the "0x"). These reverse nodes are typically used to set a [Primary Name](#primary-name) for an account.
### Reverse Record
Usually, this is referring to the [Primary Name](#primary-name). Technically speaking, a [Reverse Node](#reverse-node) can have multiple records set on it, the same as any node.
## NameWrapper
### Wrapped Name
The [ENS Name Wrapper](/wrapper/overview) is a contract for ENS that allows you to "wrap" any ENS name into a ERC-1155 NFT. This includes not only .eth 2LDs like `name.eth`, but also DNS names like `name.xyz`, or subnames like `sub.name.eth`.
### Fuse
The technical term for a specific "permission" bit for a wrapped name. As the name implies, once that bit is flipped on, the fuse is burnt and cannot be unburnt (unless the name expires).
## Subgraph
An indexed collection of data using TheGraph protocol.
In this documentation portal, "the subgraph" usually refers to the official ENS subgraph maintained by ENS Labs.
This is a useful offchain service that allows clients to query for information about names or accounts.
---
# Resolvers Quickstart
At the heart of every ENS name is its resolver. A resolver is a smart contract that implements a specific set of Resolver features (see [Resolver Interface](/resolvers/interfaces)).
The resolvers smart contract functions have control over the resolution process of a ["node"](/resolution/names#namehash) (a name or subdomain) and onwards (subdomains of itself).
## Basic Resolver
A naive but very plausible example of a resolver is the following.
```solidity
contract MyResolver {
function addr(bytes32 node) external pure returns (address) {
return 0x225f137127d9067788314bc7fcc1f36746a3c3B5;
}
function supportsInterface(
bytes4 interfaceID
) external pure returns (bool) {
return
interfaceID == this.addr.selector ||
interfaceID == this.supportsInterface.selector;
}
}
```
Notice how the above would always return the same address regardless of the name it is queried for.
If you want to [write your own resolver resolver](/resolvers/writing), or see the [interface reference](/resolvers/interfaces).
## Public Resolver
The default resolver for all names is the Public Resolver, a swiss army knife of resolvers, written by the ENS Labs team, feature-packed with everything your everyday user might need.
You can read more about the [Public Resolver](/resolvers/public).
## Interacting with a resolver
Depending on the resolver in charge of a name, certain frontend apps will be able to interact with them as well.
This means you can set your favourite records, upgrade your name to different logic, and more, from your dApp of choice.
Are you writing a dApp and want to build this? Checkout the [Interacting with a Resolver](/resolvers/interacting) section.
## Offchain Resolution
Although by default ENS resolution is done on-chain. You can leverage the power of CCIP Read to redirect resolution to an off-chain gateway.
More about writing a CCIP Read-enabled resolver [here](/resolvers/ccip-read).
---
# Public Resolver
The public resolver is a general-purpose ENS resolver that is suitable for most user needs.
It permits the owner of a name to update their records, includes permissions, and stores its data on layer-1 ethereum.
Most ENS names registered through the ENS Manager will use the latest version of the public resolver by default.
Names that resolve to a supported public resolver are editable from within the ENS Manager.
If you'd like to take a peek under the hood you can view the [public resolver source code](https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/PublicResolver.sol) on GitHub.
## Features
The public resolver supports the following features:
- [EIP-137](https://eips.ethereum.org/EIPS/eip-137) - Contract address interface (`addr()`)
- [EIP-165](https://eips.ethereum.org/EIPS/eip-165) - Interface Detection (`supportsInterface()`)
- [EIP-181](https://eips.ethereum.org/EIPS/eip-181) - Reverse Resolution (`name()`)
- [EIP-205](https://eips.ethereum.org/EIPS/eip-205) - ABI Resolution for contracts (`ABI()`)
- [EIP-619](https://eips.ethereum.org/EIPS/eip-619) - SECP256k1 public keys (`pubkey()`)
- [EIP-634](https://eips.ethereum.org/EIPS/eip-634) - Text records (`text()`)
- [EIP-1577](https://eips.ethereum.org/EIPS/eip-1577) - Content hash resolution (`contenthash()`)
- [EIP-2304](https://eips.ethereum.org/EIPS/eip-2304) - Multicoin support (`addr()`)
:::note
While the `PublicResolver` provides a convenient default implementation. Many
versions & variations may exist, and it is therefore not recommended to
hardcode any addresses. To ensure a safe implementation, always use the
`supportsInterface()` method to check for the existence of a specific
interface. See [Interacting with a Resolver](/resolvers/interacting) for how
to do this.
:::
## Permissions
The public resolver supports the notion of a "owner" and "manager".
---
# Writing a Resolver
Every ENS name has a resolver, which is responsible for resolving information about a name.
Resolvers are a core part of the ENS protocol. They give each name, represented as a ["node"](/resolution/names#namehash), the power to control the resolution process for itself and all of its subnames. Resolvers were originally standardized in [EIP 137](https://eips.ethereum.org/EIPS/eip-137), but have since received a few updates such as [EIP 181](https://eips.ethereum.org/EIPS/eip-181), [EIP 2304](https://eips.ethereum.org/EIPS/eip-2304), and [ENSIP-10](/ensip/10).
You can find the latest default resolver implementation, called the Public Resolver, on [GitHub](https://github.com/ensdomains/ens-contracts/blob/mainnet/contracts/resolvers/PublicResolver.sol) and [Etherscan](/learn/deployments).
## Resolver Interface
You can view an extended list of resolver methods [here](/resolvers/interfaces), however a simple interface might look something like this:
```solidity
interface IMyResolver {
function supportsInterface(bytes4 interfaceId) external view returns (bool);
function addr(bytes32 node) external view returns (address payable);
function addr(bytes32 node, uint256 coinType) external view returns (bytes memory);
function contenthash(bytes32 node) external view returns (bytes memory);
function text(bytes32 node, string calldata key) external view returns (string memory);
function setAddr(bytes32 node, address addr) external;
function setAddr(bytes32 node, uint256 coinType, bytes calldata a) external;
function setContenthash(bytes32 node, bytes calldata hash) external;
function setText(bytes32 node, string calldata key, string calldata value) external;
}
```
## Wildcard Resolution
In [ENSIP-10](/ensip/10) a new `resolve()` method was added to the resolver interface to allow for wildcard resolution.
```solidity
interface IExtendedResolver {
/**
* @dev Performs ENS name resolution for the supplied name and resolution data.
* @param name The name to resolve, in normalised and DNS-encoded form.
* @param data The resolution data, as specified in ENSIP-10.
* @return The result of resolving the name.
*/
function resolve(
bytes memory name,
bytes memory data
) external view returns (bytes memory);
}
```
:::note
Don't forget to add `0x9061b923` to your [EIP-165](https://eips.ethereum.org/) `supportsInterface()` implementation.
:::
## Onchain Resolvers
By default, ENS names use the [Public Resolver](/resolvers/public) which stores all data onchain. An extremely basic resolver that stores a mapping of ENS names to addresses might look like this:
```solidity
contract OnchainResolver {
mapping(bytes32 node => address addr) public addr;
function setAddr(bytes32 node, address _addr) external {
addr[node] = _addr;
}
function supportsInterface(
bytes4 interfaceID
) external pure returns (bool) {
return
interfaceID == OnchainResolver.supportsInterface.selector ||
// function addr(bytes32 node) external view returns (address)
interfaceID == 0x3b3b57de;
}
}
```
:::note
This is not secure since it doesn't implement any form of access control for updating a name's address, and is only suitable for demonstration purposes.
:::
Since the mapping is stored internally, it costs gas for the owner of a name to update their address. This is great for a maximal decentralization, but is not always practical.
## Offchain Resolvers
An offchain resolver is a resolver that implements CCIP Read to defer a name's resolution to an HTTP server. This server can then load data from any source including offchain databases, APIs, or other blockchains. [Learn more about CCIP Read](/resolvers/ccip-read).
An equivalent offchain resolver to the above onchain example looks something like this:
```solidity
contract OffchainResolver {
string public url =
"https://docs.ens.domains/api/example/basic-gateway";
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);
function addr(bytes32 node) external view returns (address) {
bytes memory callData = abi.encodeWithSelector(
OffchainResolver.addr.selector,
node
);
string[] memory urls = new string[](1);
urls[0] = url;
revert OffchainLookup(
address(this),
urls,
callData,
OffchainResolver.addrCallback.selector,
abi.encode(callData, address(this))
);
}
function addrCallback(
bytes calldata response,
bytes calldata
) external pure returns (address) {
address _addr = abi.decode(response, (address));
return _addr;
}
function supportsInterface(
bytes4 interfaceID
) external pure returns (bool) {
return
interfaceID == OffchainResolver.supportsInterface.selector ||
interfaceID == OffchainResolver.addr.selector;
}
}
```
:::note
This is not secure since it doesn't implement any form of verification in the callback function, and is only suitable for demonstration purposes.
:::
Any ENS name that sets its resolver to this contract would resolve to whatever address the Gateway returns, which can be changed at any time offchain for free. See the [gateway code here](https://github.com/ensdomains/docs/blob/master/functions/api/example/basic-gateway.ts).
For the same functionality to work with subnames, you'd need to implement the `resolve()` method from ENSIP-10. A feature-complete example can be found [here](https://github.com/ensdomains/ccip-tools/blob/master/contracts/OffchainResolver.sol), and easily deployed via [ccip.tools](https://ccip.tools/).
---
import { InterfaceDetails } from '../../components/InterfaceDetails'
import { universalresolver_methods } from '../../data/universal-resolver'
# Universal Resolver [A swiss army knife for resolution.]
## Overview
The Universal Resolver is a contract that handles the work of resolving a name entirely onchain, making it possible to make a single smart contract call to resolve an ENS name.
Note that this contract is already built into libraries like [viem](https://viem.sh) and [ENSjs](https://github.com/ensdomains/ensjs), so most developers don't typically need to use it directly.
### Forward Resolution
To resolve one or more records for a name, use one of the `resolve` methods, such as `resolve(bytes name, bytes data)`.
The `name` argument for these methods will be the [DNS-encoded](/resolution/names#dns) version of the name. Make sure to [normalize](/resolution/names#normalize) the name first, as well! For example, given the name `My.Name.eth`:
1. Normalize:
- `My.Name.eth` -> `my.name.eth`
2. DNS Encode:
- `my.name.eth` -> `0x026d79046e616d650365746800`
The `data` argument for these methods will be an ABI-encoded call to the resolver for that name.
For example, if you want to resolve the ETH address for `my.name.eth`, using the `addr(bytes32 node)` method, then the corresponding ABI-encoded call would be:
```
0x3b3b57def61adbd8ee36cf930560efc644af752731733dc6421afe47608f8e2cfeaabe2b
```
See here for standard resolver methods: [Resolver Interface Standards](/resolvers/interfaces)
### Reverse Resolution
To reverse-resolve an address to an ENS name, call one of the `reverse` methods, such as `reverse(bytes reverseName)`.
The `reverseName` argument for these methods will be the [DNS-encoded](/resolution/names#dns) version of the reverse name. The "reverse name" is `[addr].addr.reverse`, where `[addr]` is the lowercased Ethereum public address without the "0x". Make sure to [normalize](/resolution/names#normalize) the name as well! For example, given the address `0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63`:
1. Construct Reverse Name:
```
0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63 ->
231b0Ee14048e9dCcD1d247744d114a4EB5E8E63.addr.reverse
```
2. Normalize:
```
231b0Ee14048e9dCcD1d247744d114a4EB5E8E63.addr.reverse ->
231b0ee14048e9dccd1d247744d114a4eb5e8e63.addr.reverse
```
3. DNS Encode:
```
231b0ee14048e9dccd1d247744d114a4eb5e8e63.addr.reverse ->
0x28323331623065653134303438653964636364316432343737343464313134613465623565386536330461646472077265766572736500
```
## Interface Standards
---
import { InterfaceDetails } from '../../components/InterfaceDetails'
import { resolver_methods } from '../../data/resolver'
# Resolver Interface Standards
This page is a collection of methods that a resolver MAY implement.
---
import { EmbedLink } from '../../components/EmbedLink'
import { Properties } from '../../components/Properties'
import { Repository } from '../../components/Repository'
import { Card } from '../../components/ui/Card'
# Offchain / L2 Resolvers
The source of truth for a name and its subdomains does not always have to be onchain or on Ethereum L1 at all. By leveraging [EIP-3668](https://eips.ethereum.org/EIPS/eip-3668) (CCIP Read) in a [Resolver](/resolvers/quickstart), developers can effectively defer resolution to an L2 or offchain API.
Name
jesse.cb.id
➡️
Resolver
0x1934...BE56
➡️
Gateway
api.coinbase.com
➡️
Address
0x8491...8bf1
## How does CCIP Read work?
CCIP Read (Cross Chain Interoperability Protocol) is a specification that defines a standard error smart contracts can throw if they want to trigger an offchain HTTP request.
```solidity
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
)
```
When a contract reverts with the `OffchainLookup` error, it is up to clients (wagmi, viem, ethers, etc.) to handle it appropriatrely. Thankfully, many clients support CCIP Read out of the box so it's transparent to application developers in most cases.
### How does ENS use CCIP Read?
CCIP Read can be used to make ENS name resolution more flexible. Instead of storing all data on Ethereum L1, developers can [implement CCIP Read in a Resolver](/resolvers/writing#offchain-resolvers) to store data (subnames, records, etc.) on L2 networks or even an offchain database.

To resolve an offchain/L2 name using CCIP Read, the steps are as follows:
1. A user types "example.eth" into their wallet.
2. The wallet's client calls `resolve()` on example.eth's Resolver.
3. The Resolver reverts with an `OffchainLookup` error.
4. The client makes a request to the gateway URL specified in the error with the calldata from the error.
5. The gateway processes the request and returns data to the client. This is where the data is fetched from L2 or an offchain database.
6. The client calls the callback function specified in the error with the data returned from the gateway, which usually performs some sort of validation.
7. If the callback function validates the data, the client returns the result to the user.
While this might sound complex, it all happens under the hood and is completely transparent to application developers.
## Offchain Subname Example
An example of offchain ENS names powered by CCIP Read can be found at [offchain.ens.gregskril.com](https://offchain.ens.gregskril.com/).
The name [offchaindemo.eth](https://ens.app/offchaindemo.eth) with Resolver [0x35b9...E237](https://etherscan.io/address/0x35b920d4329C5797727Af8b15358b43509e5E237#code), reverts with `OffchainLookup` and directs the client to a Gateway URL.
The Gateway returns the relevant information from an offchain database, signed by a trusted private key which the smart contract can verify. This prevents a compromised Gateway from returning false information.
## Offchain vs L2 Resolvers
From the perspective of the L1 Resolver contract, the process of resolving an L2 name is exactly the same as resolving on offchain name. The differences come from the Gateway implementation and the Resolver's callback function.
For names that are stored offchain like the example above, the Gateway would read from a normal web2 database and the Resolver's callback function would simply verify the Gateway operator's signature.
For names that are stored on L2, the Gateway would make an RPC call to the relevant L2 and the Resolver's callback function would ideally verify the response by using the L2's state roon on L1 (this assumes knowledge of how L2's work).
To implement trustless L2 resolution, developers should use a solution like [Unruggable Gateways](https://gateway-docs.unruggable.com/).
## Writing a CCIP Read Gateway
A gateway is an offchain API endpoint that implements the [Gateway Interface](https://eips.ethereum.org/EIPS/eip-3668#gateway-interface) specified in EIP-3668. It is responsible for decoding the calldata from an `OffchainLookup` error and returning a relevant response.
### Implementing the Endpoint
Your gateway must implement either a `GET` or `POST` endpoint with `{sender}` and `{data}` parameters, and be stored in the implementing smart contract. The `OffchainLookup` error will include this URL, which is how the client knows where to send the request.
:::code-group
```yaml [POST]
// POST if URL does not include '{data}' parameter
URL: https://example.com/gateway
Method: POST
Body:
sender: "0x..."
data: "0x..."
```
```yaml [GET]
// GET if URL includes '{data}' parameter
URL: https://example.com/gateway/{sender}/{data}.json
Method: GET
```
:::
Lowercased address of the contract reverting with the `OffchainLookup`
error.
0x prefixed bytes of the data passed to the `OffchainLookup` error.
### Example Gateway Implementation
The most basic gateway implementation is to return a static value without doing any signing. We even have a library [@ensdomains/ccip-read-router](http://github.com/ensdomains/ccip-read-router) to abstract decoding the calldata.
### Trust Assumptions
As explained in [Offchain vs L2 Resolvers](#offchain-vs-l2-resolvers), trust assumptions are up to the implementing developer and can range from fully trusted to full trustless.
The worst case scenario of a trusted implementation is that a malicious actor gains control of the gateway and can return false information.
The worst case scenario of a trustless implementation is that a malicious actor can take a gateway offline, but it can never return false data.
## Writing an Offchain/L2 Resolver
See [Writing a Resolver](/resolvers/writing#offchain-resolvers) for more information on how to implement a Resolver with CCIP Read.
## Testing your offchain names
To test your implementation, search the relevant name in the [ENS Manager App](https://app.ens.domains). Make sure that you've configured your test name to return a result for common data like an ETH address or common text records like `avatar` or `description`. If you set an arbitrary text record key like `test`, the manager app has no way of knowing that it exists.
---
# Interacting with a Resolver [Set Addresses, Text Records, and more]
Some apps may want to allow for users to edit, update, or modify their name and its behaviour at a more advanced level.
This is possible by interacting with the resolver contract of a name directly.
## Checking Interface Support
Before you start sending transactions to users resolvers, you should check if they support the interface you want to use. This is done by calling the `supportsInterface` (see [EIP-165](https://eips.ethereum.org/EIPS/eip-165)) function on the resolver contract.
```solidity
function supportsInterface(bytes4 interfaceID) external pure returns (bool)
```
In order to ensure that resolvers we interact with are compatible with specific standards you can call the above function on contracts with an interfaceID and then check the boolean it returns.
Interface IDs are calculated according to solidity ABI and stored in a four-byte value.
## Updating a user's Record
If you want to help a user set their avatar, specify a preferred color scheme, or set any other record on their ENS name you can do so in specific cases.
First we need to check if the user's resolver supports the interface we want to use (see [setText](/resolvers/interfaces#0x10f13a8c)).
Afterwhich you can call the `setText()` function on the user's resolver contract.
:::code-group
```solidity [Solidity]
interface Resolver {
function setText(bytes32 node, string calldata key, string calldata value) external;
}
```
```typescript [ENSjs]
// https://github.com/ensdomains/ensjs/blob/main/docs/wallet/function.setRecords.md
import { addEnsContracts } from '@ensdomains/ensjs'
import { setRecords } from '@ensdomains/ensjs/wallet'
import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'
const wallet = createWalletClient({
chain: addEnsContracts(mainnet),
transport: custom(window.ethereum),
})
const hash = await setRecords(wallet, {
name: 'ens.eth',
coins: [
{
coin: 'ETH',
value: '0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7',
},
],
texts: [{ key: 'foo', value: 'bar' }],
resolverAddress: '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41',
})
// 0x...
```
:::
Please make it clear to the user what you are doing and why. If possible please also showcase the record you are setting to the user before you do so.
## Update a user's Resolver
Overwriting a user's resolver involves overwriting the behaviour of their ENS name.
In order to overwrite the resolver for a user you need to call the `setResolver` function on the `ENSRegistry` contract.
:::code-group
```solidity [Solidity]
interface ENS {
function setResolver(bytes32 node, address resolver) external;
}
```
```typescript [ENSjs]
// https://github.com/ensdomains/ensjs/blob/main/docs/wallet/function.setResolver.md
import { addEnsContracts } from '@ensdomains/ensjs'
import { setResolver } from '@ensdomains/ensjs/wallet'
import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'
const wallet = createWalletClient({
chain: addEnsContracts(mainnet),
transport: custom(window.ethereum),
})
const hash = await setResolver(wallet, {
name: 'ens.eth',
contract: 'registry',
resolverAddress: '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41',
})
// 0x...
```
:::
Please do not change the resolver for a user without their permission. Overwriting the resolver is a destructive action and will overwrite any existing resolution logic.
## Layer 2 & Offchain Resolvers
At the time of writing this the ecosystem around multichain and "writing" to layer 2 & offchain resolvers has yet to be standardized and is still under active development.
Please check back at a later date.
---
import { HomePage } from '../components/HomePage'
---
# Supported TLD List [Any DNS TLD that supports DNSSEC can be used with ENS]
Alongside the `.eth` Top Level Domain, the ENS Protocol also supports most of your favourite DNS Top Level Domains (such as `.com`, `.cash` or `.domains`).
All DNS TLDs are owned by the [DNS Registrar](/registry/dns) by default, but TLD operators can claim ownership of their TLD in ENS to implement custom logic.
Below is a list of all known custom TLD implementations:
{/* TODO: Generate this at build time */}
| TLD | Registrar |
| ------- | --------------------------------------------------------------------------------------------------------------------- |
| .art | [0x828D6e836e586B53f1da3403FEda923AEd431019](https://etherscan.io/address/0x828D6e836e586B53f1da3403FEda923AEd431019) |
| .box | [0x0b9BB06Ebf35A755998B60353546ae8A055554d2](https://etherscan.io/address/0x0b9BB06Ebf35A755998B60353546ae8A055554d2) |
| .hiphop | [0x04ebA57401184A97C919b0B6b4e8dDE263BCb920](https://etherscan.io/address/0x04ebA57401184A97C919b0B6b4e8dDE263BCb920) |
| .club | [0x1eb4b8506fca65e6B229E346dfBfd349956A66e3](https://etherscan.io/address/0x1eb4b8506fca65e6B229E346dfBfd349956A66e3) |
| .kred | [0x56ca9514363F68d622931dce1566070f86Ce5550](https://etherscan.io/address/0x56ca9514363F68d622931dce1566070f86Ce5550) |
| .luxe | [0xA86ba3b6d83139a49B649C05DBb69E0726DB69cf](https://etherscan.io/address/0xA86ba3b6d83139a49B649C05DBb69E0726DB69cf) |
---
# [EP1.9] [Executable] Fund the Protocol Guild pilot with 200,000 $ENS
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/12877) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/8759198094868535520038506706231539487662297008587733129541987545743856603253) |
_Note: This was previously numbered EP13._
## Abstract
We propose that 200k ENS (\~3.7% of the [unclaimed airdrop](https://twitter.com/ensdomains/status/1522066354387718144)) be allocated in recognition of past and ongoing work of these core contributors.
ENS is one of the most successful projects built on Ethereum, and yet core protocol contributors do not benefit in any way from the success of projects being built on Ethereum. The Protocol Guild provides a vehicle for the ENS community to distribute governance tokens to those individuals who build and maintain the foundational infrastructure that ENS relies on.
ENS sponsorship of the Guild allows members to engage with ENS in a way that is values- and incentive-aligned. Simultaneously, it will allow them to continue the important work of scaling our shared infrastructure and making it as resilient as possible for the applications on top of it.
## Specification
### Useful links
- [Protocol Guild Docs](https://protocol-guild.readthedocs.io/en/latest/index.html)
- [1 Year Vesting Contract](https://app.0xsplits.xyz/accounts/0xF29Ff96aaEa6C9A1fBa851f74737f3c069d4f1a9/)
- [Split Contract](https://app.0xsplits.xyz/accounts/0x84af3D5824F0390b9510440B6ABB5CC02BB68ea1/)
- [Initial Announcement - Dec 2021](https://stateful.mirror.xyz/mEDvFXGCKdDhR-N320KRtsq60Y2OPk8rHcHBCFVryXY)
- [List of Members](https://protocol-guild.readthedocs.io/en/latest/9-membership.html)
- [Protocol Guild twitter](https://twitter.com/ProtocolGuild)
### Context
1. As a credibly neutral, maximally uncapturable infrastructure with no block reward, the Ethereum base protocol doesn't offer the same token incentives to contributors as applications or L2s can. However, the protocol still needs to attract and retain talent to continue to evolve. As the broader ecosystem continues to grow, competition for talented individuals will only increase. This isn't to fault individuals for rationally weighting financial incentives, or protocols for leveraging the power of tokens - this is just the reality of the current context. We also acknowledge that financial motivations aren't the only or best motivator for people, it's just one tool in our toolset that is currently underleveraged.
2. Existing public goods funding solutions tend to be either too narrow or broad in scope, fail to exclusively target core protocol contributors, or depend on an intermediating institution, which often leads to organizations, and not individuals, being recipients of funds.
3. The Protocol benefits from contributor continuity. Transferring institutional knowledge between cohorts is more likely to happen successfully the more overlap there is.
Here's a longer exploration of the [project rationale](https://protocol-guild.readthedocs.io/en/latest/1-proposal-rationale.html).
If we believe what we are building is important, then we should structure the incentives to attract more smart people to work on it. After all, "Ethereum is an unprecedented arena for playing cooperative games"; we should try to manifest the novel possibilities made possible by this arena. ([Griffith, 2019](https://medium.com/@virgilgr/ethereum-is-game-changing-technology-literally-d67e01a01cf8))
### What is the Protocol Guild?
The Protocol Guild aims to address the challenges mentioned above with a simple tool: a weighted split contract that includes vesting. Members will solicit sponsorships in the form of tokens from applications & protocols that build on Ethereum, which gives core contributors exposure to success at the application layer:
- current contributors are rewarded for past work through time-based weighting
- current contributors contribute for longer periods, resulting in less contributor churn
- new contributors are incentivized to join core protocol work, protocol evolution and maintenance is more robust
To date, the membership includes over 110 Ethereum protocol contributors, including researchers, client maintainers, upgrade coordinators, and more, all self-curated (member list [here](https://protocol-guild.readthedocs.io/en/latest/9-membership.html)). This is a broad-based ecosystem effort: members come from 22 different teams and 9 organizations. Only 30% of members are directly employed by the EF. The membership is continuously curated through quarterly updates to the split contract - we expect the membership to grow to 150 over the course of the Pilot.
The Guild contracts will act as an autonomous value routing mechanism, operated independently from any existing institution, purpose-built for incentivizing long-term core protocol work. At no point does PG take custody of funds on behalf of members, it is all handled trustlessly. The diagram below and the [docs](https://protocol-guild.readthedocs.io/en/latest/3-smart-contract.html) have more information.

### 2022 Pilot
Since starting the project in Nov 2021, we've built norms around member onboarding , refined the splitting and vesting mechanisms, and created extensive documentation on how PG operates.
At this point, we're ready to test the mechanism's efficacy with a 1 year / $10-20mm Pilot. We want to make sure the mechanism operates smoothly before graduating to a full-scale fundraising round for longer vesting periods. The funds for the Pilot would be vested directly to Guild members over one year: see the [Pilot vesting contract here](https://app.0xsplits.xyz/accounts/0xF29Ff96aaEa6C9A1fBa851f74737f3c069d4f1a9/).
### Proposal
We are proposing that 200k ENS (\~3.7% of the [unclaimed airdrop](https://twitter.com/ensdomains/status/1522066354387718144)) be sent to the [Pilot vesting contract](https://app.0xsplits.xyz/accounts/0xF29Ff96aaEa6C9A1fBa851f74737f3c069d4f1a9/) deployed at 0xF29…f1a9 in recognition of the past and ongoing work of these core contributors.
The tokens would not be liquidated, but would vest for one year to each beneficiary listed on the underlying split. Each recipient would be making an independent decision about how to use their tokens once vested.
The USD value of the 200k ENS is $2.49mm as of the time of this post on May 25 2022. This is roughly in line with what we have already proposed to similarly prominent Ethereum-based protocols. [Lido's 2mm LDO contribution](https://research.lido.fi/t/proposal-to-fund-the-protocol-guild-pilot-via-a-lido-grant/2016) was worth $2.6mm; the [active Uniswap proposal](https://gov.uniswap.org/t/governance-proposal-should-the-uniswap-community-participate-in-the-protocol-guild-pilot/16824) requesting 500k UNI would be worth $2.75mm.
There are a few reasons why supporting the Protocol Guild benefits the ENS community:
- ENS's long-term success is tightly coupled with the continued evolution and maintenance of the Ethereum protocol. These are projects that often have multi-year timelines. Contributing to the Pilot meaningfully increases the incentive to contribute to the core protocol, including:
- [The Merge](https://github.com/ethereum/pm/blob/master/Merge/mainnet-readiness.md): moving from PoW to PoS, increasing security and sustainability
- EVM improvements: new functionality for developers like [EOF](https://notes.ethereum.org/@ipsilon/evm-object-format-overview)
- [Statelessness](https://notes.ethereum.org/@gballet/Sy-a6T5St): sustainable management for state growth
- Supporting L2 scaling: [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844), [EIP-4488](https://eips.ethereum.org/EIPS/eip-4488)
- [Proposer Builder Separation](https://notes.ethereum.org/@vbuterin/pbs_censorship_resistance): reducing centralizing incentives for consensus participants
- Continuous client maintenance: improving sync, exploring new database types, researching modular clients
- Coordinating network upgrades: making sure the community helps to shape and is aware of network upgrades
- Having exposure to ENS allows protocol contributors to engage more with ENS governance. Members will be encouraged (but not obligated) to delegate them within the ENS governance framework.
- ENS should be among the protocols maximally aligned with the Public Goods of the largest ecosystem it operates in. Pilot participation maintains and expands the ENS community's existing reputation for funding Public Goods.
- Diverse funding sources from the community further decentralizes protocol governance and prevents influence from pooling with any single entity.
We hope that a successful Pilot will pave the way for future funding collaborations between the ENS community and the Protocol Guild as we scale up the project after the Pilot. To that end, we think it's important to demonstrate impact: learn more about how we intend to evaluate [Pilot outcomes here](https://protocol-guild.readthedocs.io/en/latest/5-initial-pilot.html). We have also adopted an active stance of continuous adjustments to improve PG while we operate the Pilot: improving documentation, resources for members, better transparency, etc.
## Transactions
| Address | Value | Function | Argument | Value |
| ---------------- | ------------------------ | -------- | --------- | -------------------- |
| token.ensdao.eth | 0 | transfer | recipient | theprotocolguild.eth |
| amount | 200000000000000000000000 | | | |
---
# [EP1.3.4] [Executable] Q1 & Q2 2022 Public Goods WG Budget
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/11022) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/115615865324623814833258987703837575663427750121726187103053182962864855260310) |
_Note: This was previously numbered EP7.4._
## Summary
The Ecosystem WG is requesting funding to start the Q1/Q2 2022 term. The initial request is made up of three components:
1. Elected steward compensation: $12,000 in cDAI
2. Bounties for ENS Public Goods Request for Proposals program: $50,000 in cDAI
3. Operational Budget $38,000 in cDAI
**Total for Q1 & Q2: $150,000.00 in cDAI**
The group will be using the price of cDAI in uniswap to calculate the exact amounts requested. We are using cDAI as a accounting unit as a manner to provide a stable value that will still accrue value while not in utilization and resist dollar depreciation. The group can convert them in DAI when sending out payments and grants. The current price of cDAI orbits around 0.021-0.022 per dai and the total budget would be, in today's value, about 6.85M cDAI.
**Elected steward compensation**
The group has 5 elected stewards: Alex (avsa.eth), makoto.eth, sumedha.eth, Scott and Richard Moore (Ricmoo.eth). Two of these steward are appointed by True Names and will not be receiving compensation, and one of the other stewards has elected to forego compensation and therefore we will asking only for a total of 12k USD for the total Steward Compensation. Pay for past months will be sent immediately and the remaining will be streamed until june 30th.
**Bounties for ENS Public Goods Request for Proposals program:**
The bounties will be distributed in prizes of $1,000, $5,000, and $10,000 for projects that execute on basic implementation of goals elected by the stewards. These are set to be given directly to projects and not spent on operations. Any funds remaining at the end of the term will be either given back to the DAO or rolled in the Public Goods stewards budget for the next term.
**Operational Budget**
These will encompass tasks like reimbursements for expenses executed by stewards, payment for projects like a website, social media, or compensation for work done in partner integrations. All payments must be approved by at least 3 other stewards and will be executed in the most transparent manner the stewards can find.
---
# [EP4.6] [Executable] October 2023 Working Group Funding
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/18064) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/97815480450036429026998617227498389389237567897899533433380564585906006968770) |
This proposal executes all three Working Group funding requests for the October 2023 funding window as passed in EP 4.4.1, EP 4.4.2, and EP 4.4.3. For more detail, view the ENS Governance docs at [https://basics.ensdao.org](https://basics.ensdao.org) 1 [Draft Discourse link](https://discuss.ens.domains/t/ep-4-6-executable-october-2023-working-group-funding/18064)
## Abstract
## [EP 4.4.1 — ENS Ecosystem Working Group](https://discuss.ens.domains/t/4-4-1-social-funding-request-ens-ecosystem-working-group/17995)
The ENS Ecosystem Working Group requests funding of **409,000 USDC** from the ENS DAO treasury.
The ENS Ecosystem Working Group is responsible for growing and improving the ENS Ecosystem by funding people and projects that are ENS-specific or ENS-centric. In line with Article III of the ENS DAO Constitution, the requested funds will be used to support projects and builders contributing to the development and improvement of the ENS protocol and the ENS ecosystem.
## [EP 4.4.2 — MetaGovernance Working Group](https://discuss.ens.domains/t/4-4-2-social-funding-request-ens-meta-goverance-working-group/17994)
The Meta-Governance Working Group requests funding of **376,000 USDC, 40 ETH, and 52,300 $ENS** from the ENS DAO treasury.
This MetaGovernance Working Group will use this funding to support the governance processes of the ENS DAO as well as manage and build infrastructure to support the ENS DAO and Working Groups.
## [EP 4.4.3 — Public Goods Working Group](https://discuss.ens.domains/t/4-4-3-social-funding-request-ens-public-goods-working-group/17996?u=5pence.eth)
The ENS Public Goods Working Group requests funding of **218,204 USDC, and 35 ETH** from the ENS DAO treasury.
The Public Goods Working Group will be use this funding to support projects and builders as provisioned by Article III of the ENS DAO Constitution, which provides for the funding of public goods in web3.
## Specification
The following transfers are to be made:
- Transfer 409,000 USDC to ens-ecosystem.pod.xyz
- Transfer 376,000 USDC, 40 ETH, and 52,300 $ENS to ens-metagov.pod.xyz
- Transfer 218,204 USDC and 35 ETH to ens-publicgoods.pod.xyz
Addresses for confirmation:
- 0x2686A8919Df194aA7673244549E68D42C1685d03 for ens-ecosystem.pod.xyz
- 0x91c32893216dE3eA0a55ABb9851f581d4503d39b for ens-metagov.pod.xyz
- 0xcD42b4c4D102cc22864e3A1341Bb0529c17fD87d for ens-publicgoods.pod.xyz
---
# [EP3.3] [Executable] Sell ETH to USDC
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/15906) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/45461903078948131870051132081249892009497709518413744958551889217805827301425) |
## Abstract
This proposal executes a swap of 10,000 ETH into USDC, to ensure ENS DAO has enough to cover operating expenses for 18 - 24 months.
## Motivation
The DAO currently keeps almost 100% of its spendable treasury in ETH. While ENS generates protocol revenue in ETH, having so much exposure to a single volatile asset places the DAO in a vulnerable position.
This is a proposal to convert 10,000 ETH into USDC through a Cowswap swap.
10,000 ETH is approximately 25% of the total amount of ETH held by the ENS DAO (wallet.ensdao.eth) and register controller (controller.ens.eth) as of January 18, 2023.
It is hoped that this sale will generate in excess of $13m in USDC. The goal is to ensure that the DAO has enough USDC to cover operations for the next 18 - 24 months.
## Specification
1. Call `withdraw()` on controller.ens.eth (0x283af0b28c62c092c9727f1ee09c02ca627eb7f5)
2. Call `deposit()` on WETH9 (0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2), sending 10,000 ETH
3. Call `approve(, 10000 ETH)` on WETH9 (0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2)
4. Call `requestSwapExactTokensForTokens(10000 ETH, , , wallet.ensdao.eth, , )` on milkman (0x11C76AD590ABDFFCD980afEC9ad951B160F02797)
Price checker data is configured to check the swap against the Uniswap v3 pool and limit to 2% slippage. It is generated using the below code:
```javascript
import { ethers } from 'https://cdn.ethers.io/lib/ethers-5.2.esm.min.js'
function getCheckerData(slippage, path, fees) {
return ethers.utils.defaultAbiCoder.encode(
['uint256', 'bytes'],
[
slippage,
ethers.utils.defaultAbiCoder.encode(
['address[]', 'uint24[]'],
[path, fees]
),
]
)
}
const checkerData = getCheckerData(
200,
[
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
],
[5]
)
console.log('checkerData:', checkerData)
```
**Addresses:**
- 0xfe89cc7abb2c4183683ab71653c4cdc9b02d44b7 - wallet.ensdao.eth
- 0x283af0b28c62c092c9727f1ee09c02ca627eb7f5 - controller.ens.eth
- 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - WETH9
- 0x11C76AD590ABDFFCD980afEC9ad951B160F02797 - milkman
- 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 - USDC
- 0x2F965935f93718bB66d53a37a97080785657f0AC - Uniswap v3 slippage checker
## Transactions
| Address | Value | Function | Argument | Value |
| ------------------------------------------ | --------- | ------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5 | - | withdraw | | |
| 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 | 10000 ETH | deposit | | |
| 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 | - | approve | guy | 0x11C76AD590ABDFFCD980afEC9ad951B160F02797 |
| | | | wad | 10000000000000000000000 |
| 0x11C76AD590ABDFFCD980afEC9ad951B160F02797 | - | requestSwapExactTokensForTokens | amountIn | 10000000000000000000000 |
| | | | fromToken | 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 |
| | | | toToken | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| | | | to | 0xfe89cc7abb2c4183683ab71653c4cdc9b02d44b7 |
| | | | priceChecker | 0x2F965935f93718bB66d53a37a97080785657f0AC |
| | | | priceCheckerData | 0x00000000000000000000000000000000000000000000000000000000000000c8000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000005 |
---
# [EP3.5] [Executable] Activate new .eth Controller and Reverse Registrar
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/16776) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/42973781582803845389836855775840822719678533376883030929209752909248937768242) |
## Abstract
With the new Name Wrapper, we will add a new .eth controller that allows registering wrapped names directly as well as registering with multiple records and adding a reverse record in 1 transaction. This will reduce the transactions required from 4 to 2 (for adding records + reverse). This will be added as a controller to the NameWrapper, and the NameWrapper will be added as the new controller of the existing .eth Base Registrar.
We will also replace the current reverse registrar with a new reverse registrar which allows the new controller to set the reverse on registration, as well as adds support for the owner of contract to retrospectively claim their reverse node.
## Specification
New instances of the Name Wrapper, Reverse Registrar, and .eth registrar controller have been deployed to mainnet at these addresses:
- NameWrapper: `0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401`
- ReverseRegistrar: `0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb`
- ETHRegistrarController: `0x253553366Da8546fC250F225fe3d25d0C782303b`
- PublicResolver: `0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63`
They have been configured, and ownership has been transferred to the DAO.The new Public Resolver is set as the default resolver on the reverse registrar.
This executable proposal will execute the following calls to complete the contract upgrades:
1. Call `registrar.addController(newNameWrapperAddress)`
2. Call `ens.setSubnodeOwner(namehash('reverse'), labelhash('addr'), newReverseRegistrarAddress)`
3. Call `setInterface` on the resolver for .eth with the interface IDs and contract addresses of the new .eth registrar controller and namewrapper. This is used as part of the discovery mechanism by the ENS manager app and others in order to locate the new contracts.
## Transactions
| Address | Value | Function | Argument | Value |
| ------------------------------------------ | ----- | --------------- | ----------- | ------------------------------------------------------------------ |
| 0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85 | | addController | controller | 0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401 |
| 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e | | setSubnodeOwner | node | 0xa097f6721ce401e757d1223a763fef49b8b5f90bb18567ddb86fd205dff71d34 |
| | | | labelhash | 0xe5e14487b78f85faa6e1808e89246cf57dd34831548ff2e6097380d98db2504a |
| | | | owner | 0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb |
| 0x30200e0cb040f38e474e53ef437c95a1be723b2b | | setInterface | node | 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae |
| | | | interfaceId | 0x019a38fe |
| | | | implementer | 0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401 |
| 0x30200e0cb040f38e474e53ef437c95a1be723b2b | | setInterface | node | 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae |
| | | | interfaceId | 0x612e8c09 |
| | | | implementer | 0x253553366Da8546fC250F225fe3d25d0C782303b |
---
# [EP0.1] [Social] Proposal: Transfer ENS Treasury and Contract Ownership
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/6307) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0xfe73d1b06675d6bc1cc074f440c347274d13c55b513ea02ec950efe639adbbb0) |
_Note: This was previously numbered EP1._
## Summary
Transfer ENS treasury and contract ownership from the ENS Multisig to ENS DAO.
## Abstract
With the recent launch of the ENS DAO and $ENS token, it is now time for key governance powers of ENS to be transferred from the ENS root multisig to the ENS DAO.
🔥_🔥 ([Fire Eyes DAO](https://fireeyes.xyz)) has taken a [central role in the planning and execution](https://mirror.xyz/james.eth/XkOIGh8xu_bF2cQV4QZkKxoN0o1OuNoEb3o-Nzjdo3I) of ENS's decentralization journey and is excited to present this first proposal to the community.
This proposal will take a different form than normal as it proposes making a request to the ENS multisig root keyholders, rather than executing an action on the DAO.
**This proposal looks to execute a transfer of ownership of several aspects of the protocol which are currently controlled by the ENS root multisig:**
1. We propose that control over the existing ENS treasury is transferred to the DAO.
2. We propose that ownership of the ENS Registrar Controller and the pricing oracle is transferred to the DAO.
Ownership of the price oracle gives the DAO the ability to:
1. Set the contract used to get the USD-ETH price.\*
2. Change the price per year of each length of domain name.
Ownership of the registrar controller gives the DAO the ability to:
1. Replace the price oracle contract.
2. Withdraw accumulated funds to the DAO treasury.
3. Set the minimum and maximum period between the first and second transactions of the registration process.
3. We propose that ownership of the ENS Registrar is transferred to the DAO. Ownership of the .eth registrar gives the DAO the ability to:
1. Add and remove controller contracts.
2. Set the resolver contract for .eth
4. We propose that ownership of the 'reverse' namespace, which governs assignment of primary ENS records, be transferred to the DAO. Ownership of the .reverse TLD gives the DAO the ability to:
1. Replace the reverse registrar for Ethereum addresses.
2. Create new types of reverse resolution (For example, bitcoin addresses) and update them.
Importantly, control over the ENS root will remain with the ENS root multisig for now. Control over the root allows for the creation and replacement of ENS top-level domains (TLDs) other than .eth (.eth is locked and cannot be changed by the root). This proposal also does not transfer ownership of the DNSSEC registrar contract, necessary for administering the DNSSEC integration used to allow owners of DNS domains to claim them on ENS.
Powers over the ENS root are not being requested immediately, as they are both powers that can be abused to revoke or reassign non-.eth ENS names, and as such the risk to name owners is significantly higher than the other powers the DAO will exercise. Once the DAO has demonstrated its ability to successfully govern ENS, a future proposal will transfer these powers to the DAO.
Snapshot voting on the above proposals will be conducted individually via approval voting; any proposal above that gets at least 50% approval and a quorum of 1% becomes the official policy of the DAO.
## Specification
The ENS DAO formally petitions the ENS root keyholders (being the owners of the multisig located at multisig.ens.eth) to execute a transaction or transactions taking the following actions:
1. If proposal 1 passes, transfer all ETH and USDC held by multisig.ens.eth to the DAO's timelock contract at wallet.ensdao.eth.
2. If proposal 2 passes, call `transferOwnership` on the contracts at controller.ens.eth and 0xb9d374d0fe3d8341155663fae31b7beae0ae233a (the price oracle), passing in the address of wallet.ensdao.eth.
3. If proposal 3 passes, call `transferOwnership` on the contract at registrar.ens.eth, passing in the address of wallet.ensdao.eth.
4. If proposal 4 passes, call `setSubnodeOwner` on the root, passing in `keccak256('reverse')` and the address of wallet.ensdao.eth.
The ENS DAO agrees to use these funds and powers in accordance with the ENS Constitution.
## Conclusion & Next Steps
This proposal looks to set precedent as the first action taken by ENS delegates & token holders. Upon successful execution, the ENS DAO will hold control over two significant aspects of the protocol, and will be able to dispense funds effectively towards community initiatives.
- Discuss this proposal on the ENS forum and discord.
- Implement a vote on this proposal on Snapshot.
- If passed:
- Transfer existing assets from the ENS Community multisig to the ENS DAO.
- Transfer control over the Registrar Controller to the ENS DAO.
This vote is being conducted via approval voting; select each option you wish to see pass. Those options that have at least 50% support and 1% quorum when voting ends will pass
---
# [EP 5.16] [Executable] Reimbursement of ENS Labs’ legal fees in eth.link litigation
::authors
| **Status** | Passed |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-16-executable-reimbursement-of-ens-labs-legal-fees-in-eth-link-litigation/19613) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/33657496545373741813637578444724485346468737982573562831880427564621945332995) |
## Summary
This executable proposal seeks to implement the reimbursement payment to ENS Labs for the legal fees incurred while pursuing litigation to protect the eth.link domain. The reimbursement was approved in the previously passed social proposal [EP 5.3](./5.3).
## Background
The lawsuit that ENS filed in federal district court in Arizona to maintain ownership and control over eth.link has been resolved, and on 26 August 2024, the Court officially closed this case.
ENS Labs has maintained full ownership and control over the eth.link domain and, therefore, ENS Labs has achieved the initial objective they had when first filing the complaint and obtaining injunctive relief. To reach this outcome, ENS Labs has spent in total 1,218,669.76 USD. This legal action was necessary to defend the ENS ecosystem and maintain control of the eth.link domain, a critical infrastructure component since 2017.
### Links
- [EP 5.3 Snapshot Vote](https://snapshot.org/#/ens.eth/proposal/0x456ccb438eed5d189cbe51e5e36a88d2bb4dc0c61f12f6d9e310a7ba4798d5fc)
- [Forum Discussion on EP 5.3](https://discuss.ens.domains/t/ep5-2-social-determine-ens-labs-next-steps-in-eth-link-litigation/18756)
## Specification
This executable proposal will initiate a transfer of 1,218,669.76 USDC from the ENS DAO treasury to ENS Labs. This amount represents the final total of all legal expenses related to the eth.link litigation.
### Transaction Details
- **From:** ENS DAO Treasury (0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7)
- **To:** USDC Token Contract (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)
- **Recipient:** ENS Labs (0x690F0581eCecCf8389c223170778cD9D029606F2)
- **Amount:** 1,218,669.76 USDC (1218669760000 considering USDC's 6 decimal places)
- **Purpose:** Reimbursement for legal fees in eth.link litigation
This transaction calls the `transfer` function of the USDC contract, transferring 1,218,669.76 USDC to ENS Labs' address.
## Rationale
The ENS community, through the passage of EP 5.3, has demonstrated its support for reimbursing ENS Labs for the legal expenses incurred in protecting the eth.link domain. This reimbursement acknowledges the efforts made by ENS Labs to safeguard a critical asset of the ENS ecosystem. It ensures that the financial burden of this legal action does not fall solely on ENS Labs, particularly given that their actions were taken to benefit the entire ENS community.
---
_Note - When the original snapshot for the social vote was posted it was numbered as 5.2, but it should have been 5.3. It has been renumbered in the official ENS documentation. Some links point to forum discussions and Snapshots that show the original duplicitive label of 5.2_
---
# [EP 6.2] [Executable] Endowment expansion (3rd tranche)
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/temp-check-endowment-expansion-3rd-tranche/19851) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/31309365093913580207991288430108338667724061355449265288906484597789511363394) |
## Summary
This proposal aims to expand the Endowment by funding a third tranche, comprising 5,000 ETH, from the ENS DAO to the ENS Endowment.
Alongside the third tranche transfer, an update to the Allowance Module on the ENS Endowment is proposed; _resetTime_ for ETH allowance (for fee payment) is to be changed from the current parameter of 43,200 seconds (30 days) to 36,000 seconds (25 days). The motivation for this is that there has been an accumulation of payment delays, such that current fee payments are delayed.
## Motivation
Since the establishment of the Endowment on March 7th, 2023, ENS has been setting the gold standard for DAO treasury management:
- **Asset allocation:** ENS has the 6th-largest stablecoin holding and 5th-largest majors (BTC, ETH) holdings of any Protocols ([source](https://defillama.com/treasuries)).
- **Income generation:** Since its inception, the Endowment has generated $2.92M in DeFi results (net of fees) for ENS. In 2024, DeFi results represented 12% of total revenue for ENS according to [ENS Revenue Report: Q3 2024](https://discuss.ens.domains/t/ens-revenue-report-q3-2024/19718). This is on track towards the core goal of self-sufficiency.
- **Asset location:** As of October 31, 2024, 71% of the funds were held in the Endowment, while the remaining 25% were held idle across DAO wallet and Registrar Controllers ([source](https://dune.com/karpatkey/ens-dao-fundflow)).
The original Request for Proposal in [[EP2.2.4]](https://docs.ens.domains/dao/proposals/2.2.4) sets out that the Endowment should be established gradually over time, in response to changing conditions and needs, and to achieve the eventual goal of self-sufficiency. To deliver on this vision for self-sufficiency, a further increase in the Endowment’s returns is needed. As the Endowment is fully utilised within the bounds of the existing mandate, it is clear that those additional returns must come from additional funds.. We think **now** is the right time to implement these increases, given the following developments:
- **Proven track record**: the Endowment has been operating for 1 year and 7 months, with _no funds lost._ During that time, the ENS community has gained familiarity with the Endowment in terms of both operations and infrastructure.
- **Investment Policy Statement as guardrails**: the [newly-adopted IPS](https://snapshot.org/#/ens.eth/proposal/0x085a1e40c264ffd44567b6dce889f5943e72cfa8442eaeb81819261a38f0bd0a) provides an additional layer of checks and balances, so that the DAO and its Metagov Working Group can hold the Endowment Manager accountable.
- **Vulnerability fix by @Blockful**: risk of governance takeover by “risk-free value raiders”, who could have taken over the control of the DAO and the Endowment, has been mitigated. The vulnerability has been fixed by the introduction of the [Security Council](https://www.tally.xyz/gov/ens/proposal/42329103797433777309488042029679811802172320979541414683300183273376839219133) and veto.ensdao.eth.
The proposed third tranche would be sized at 5,000 ETH, representing 42.4% of assets held in the Controller and the DAO Wallets (_per_ the pie chart above) It would raise the capital utilisation rate by 10.5 percentage points from the current 70.8% to 81.3%.
## Endowment Updates
### Updates
Karpatkey’s updates for the Endowment can be seen here: [2023 Review](https://discuss.ens.domains/t/karpatkey-2023-review-for-the-ens-endowment/18656), [1H 2024 Review](https://discuss.ens.domains/t/karpatkey-h1-2024-review-for-the-ens-endowment/19394).
Monthly reports on the Endowment can be found on karpatkey’s website ([here](https://reports.karpatkey.com/ens?month=9&year=2024¤cy=USD)).
### What’s been done
- **Investment Policy Statement**: karpatkey has created and [formally introduced](https://snapshot.org/#/ens.eth/proposal/0x085a1e40c264ffd44567b6dce889f5943e72cfa8442eaeb81819261a38f0bd0a) an [Investment Policy Statement](https://drive.google.com/file/d/1zsV0k3J7s2QAJUoWxPnBpF6XAGW1HuD6/view) which defines the key roles, responsibilities and limitations of the Endowment and its Manager.
- **Risk Management Development**: karpatkey has designed and implemented a robust risk management stack, leveraging on dedicated risk data service providers, including [Hypernative](https://www.hypernative.io/) and [Redefine](https://redefine.net/). Through weekly meetings with Hypernative, karpatkey is continuously fine-tuning risk alerts and management, especially to cover protocol risks. Automatic, real-time risk alerts help mitigate potential risks. Detailed plans for the Endowment’s emergency protocol and war room are in place to assess and react to urgent situations.
- **Infrastructure Development**: karpatkey has worked very closely with Gnosis Guild to develop [and implement](https://discuss.ens.domains/t/ep-5-12-roles-modifier-v2-migration-updates-to-endowment-permissions/19173), Zodiac Roles Modifier v2. As a design partner, karpatkey provided feedback allowing Zodiac Roles Modifier to become more user-friendly and flexible. One notable improvement is permissions updates user interface, enhancing transparency and simplifying audits for the ENS community. A code review was also conducted for permissions update.
- **Permissions Updates**: the Endowment has been undergoing continuous permissions updates, allowing it to stay up-to-date with changing market landscape and protocols ([[EP 4.1](https://discuss.ens.domains/t/ep-4-1-executable-approve-further-actions-and-strategies-for-the-endowment/17406)], [[EP 4.2](https://discuss.ens.domains/t/4-2-executable-fund-the-endowment-second-tranche/17743)], [[EP 4.5](https://discuss.ens.domains/t/ep-4-5-executable-endowment-permissions-to-karpatkey-update-3/18036)], [[EP 5.12](https://discuss.ens.domains/t/ep-5-12-roles-modifier-v2-migration-updates-to-endowment-permissions/19173)], [[EP 5.14](https://discuss.ens.domains/t/ep-5-14-executable-endowment-permissions-to-karpatkey-update-4/19503)]). Independent, thirdparty security review of our Permissions Updates has also been conducted by [Third Guard](https://thirdguard.com/), with the cost covered by karpatkey.
- **Dune Dashboards Development:** a variety of different Dune dashboards have been created to give public visibility over the DAO and the Endowment, and their respective operations. These include: the [DAO governance dashboard](https://dune.com/karpatkey/ens-dao-governance) (by karpatkey), the [fund flow dashboard](https://dune.com/karpatkey/ens-dao-fundflow) (by karpatkey) and the [financial reporting dashboard](https://dune.com/steakhouse/ens-steakhouse) (by Steakhouse)
- **Reporting:** weekly Endowment updates and monthly financial updates are provided during the DAO’s Metagov meetings. Monthly Endowment updates are also available on the karpatkey [website](https://reports.karpatkey.com/ens?month=9&year=2024¤cy=USD). Biannual Endowment updates are provided on ENS forum.
- **Partnerships:** karpatkey has negotiated and put in place a protocol fees rebate agreement with Stader Labs.
### Future plans
- **Risk Management**: to further protect the Endowment against potential hacks and exploits in protocols that the Endowment deploys funds in, karpatkey has been developing Guardians and ‘Agile Execution App’, which automatically detect potential exploit events and exit at-risk positions.
- **Permissions Updates**: The Endowment will undergo continuous permissions updates. Immediate priorities include introducing other stablecoins and RWAs, onboarding new liquid staking protocols and keeping the permissions updated with protocol/contract upgrades.
- **Partnerships**: reinforce yields without taking additional risk by formulating partnership deals leveraging karpatkey’s DeFi network.
- **Analysis on Governance Attack:** karpatkey, together with the ENS community, will conduct further research into potential governance attack vectors that could put the Endowment at risk, and present potential solutions to the DAO.
## Specification
1. Fund Transfer
Executed by the ENS DAO Multisig (payload available [here](https://gist.github.com/JeronimoHoulin/8195478708e88830de9aabfb67bc8710)).
Transfer 5,000 ETH to the Endowment (0x4F2083f5fBede34C2714aFfb3105539775f7FE64)
Value: 5000000000000000000000
2. Update the [Allowance Module](https://etherscan.io/address/0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134)
Executed by the ENS Endowment.
Currently, the parameters of Allowance Module are as follows: allowance = 30 ETH, resetTimeMin = 43200 seconds (30 days). Due to payment delays in the past, we would like to propose a change in [resetTimeMin](https://etherscan.io/address/0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134#writeContract#F6) parameter, i.e. Change the [resetTimeMin](https://etherscan.io/address/0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134#writeContract#F6) for [main.mg.wg.ens.eth](https://etherscan.io/address/0x91c32893216dE3eA0a55ABb9851f581d4503d39b) in the Allowance Module from 30 days to 25 days (i.e. 43200 minutes to 36000).
```
"delegate": "0x91c32893216dE3eA0a55ABb9851f581d4503d39b",
"token": "0x0000000000000000000000000000000000000000",
"allowanceAmount": "30000000000000000000",
"resetTimeMin": "43200" → "36000",
"resetBaseMin": "28825613"
```
Payload available [here](https://gist.github.com/JeronimoHoulin/3e10411ac6d40b6e4087fc59df2719d5).
(Can be downloaded, unzipped, and dropped into [Safe’s transaction builder](https://app.safe.global/apps/open?safe=eth:0x4F2083f5fBede34C2714aFfb3105539775f7FE64&appUrl=https%3A%2F%2Fapps-portal.safe.global%2Ftx-builder)).
Tenderly simulation available [here](https://dashboard.tenderly.co/public/safe/safe-apps/simulator/02add7c9-0ffc-49dc-b372-30fce90ccf9d).
---
# [EP0.3] [Social] Amend airdrop proposal to include accidentally returned funds
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/6975) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0x9ab53c76cee40d58cb27b244dfa5f9f2763bd8b97b1b4be1dd0f0bf706818fb4) |
_Note: This was previously numbered EP3._
## Summary
Amend EP2 to include funds accidentally sent back to the $ENS token contract.
## Abstract
A number of users have [accidentally sent](https://discuss.ens.domains/t/proposal-to-correct-ens-transfer-errors-back-to-contract/5989) $ENS tokens back to the token contract. As of December 6, this amounted to some 6,246 tokens in 49 separate transactions.
These funds are held in the same account as is used for new airdrop claims, and on May 4, 2022 when the airdrop expires, the DAO will be able to claim them back to its own account.
[EP2](https://discuss.ens.domains/t/executable-retrospective-airdrop-for-accounts-that-owned-another-accounts-primary-ens-name/6755) sends approximately 200k tokens to users who owned ENS names that were used as primary names but did not already benefit from the 2x multiplier.
This proposal suggests that [EP2](https://discuss.ens.domains/t/executable-retrospective-airdrop-for-accounts-that-owned-another-accounts-primary-ens-name/6755) be amended to include returning the mistakenly sent funds as part of the same airdrop. This minimises overhead, as the DAO will not incur transaction fees or have to set a separate system up, and enables users to get their mistakenly sent funds back promptly.
## Specification
Amend [EP2](https://discuss.ens.domains/t/executable-retrospective-airdrop-for-accounts-that-owned-another-accounts-primary-ens-name/6755) as follows:
> `-This logic is implemented by [this series of BigQuery queries](https://gist.github.com/Arachnid/667178e854945abaecb6dfd3b6c0c279/1182eea3145394181affe4bb799d6b7858f9eb58), and shows that 1,969 accounts meet these criteria but did not qualify for the multiplier under the original criteria. The sum of the tokens these accounts would be entitled to comes to ~213,049 ENS tokens.`
>
> `+This logic is implemented by [this series of BigQuery queries](https://gist.github.com/Arachnid/667178e854945abaecb6dfd3b6c0c279/106d9bc156988cf96786c71f6448f13fb11599fc), and shows that 1,969 accounts meet these criteria but did not qualify for the multiplier under the original criteria. The sum of the tokens these accounts would be entitled to comes to ~213,049 ENS tokens.`
> `-A list of affected accounts and balances is [here](https://gist.github.com/Arachnid/d6495f57ac6a5b17cf28e01b646e99a8).`
>
> `+A list of affected accounts and balances is [here](https://gist.github.com/Arachnid/e8b1a18fc19818fb00f51fbb8d90e429).`
> `-This proposal, if executed, will transfer 213,049 ENS tokens to [a new merkle airdrop contract ](https://github.com/ensdomains/governance/pull/9) allowing affected users to claim them.`
>
> `+Further, a number of users have accidentally transferred their ENS tokens to the token contract, totalling 6,246 contracts across 49 transfers. These tokens should be returned to their previous owners.`
>
> `+This proposal, if executed, will transfer 219,295 ENS tokens to [a new merkle airdrop contract ](https://github.com/ensdomains/governance/pull/9) allowing affected users to claim them.`
> `-3. Authorise the contract deployed in (1) to spend 213049736662531485206636 base ENS tokens from the ENS DAO account.`
>
> `+3. Authorise the contract deployed in (1) to spend 219295650978169915391391 base ENS tokens from the ENS DAO account.`
> `-const tx = await token.populateTransaction.approve(airdropAddress, '213049736662531485206636');`
>
> `+const tx = await token.populateTransaction.approve(airdropAddress, '219295650978169915391391');`
---
# [EP1.6] [Executable] A DAO-Governed Identity Server
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/11125) |
| **Social Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x104eb11d42813fadc2b408856e8fa2c10e34dbb4a87abaa2f089ece124263f16) |
| **Onchain Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/82659277767818009782194204088226418907972756681918239480374274857360772298879) |
_Note: This was previously numbered EP10._
## Abstract
In our research, we found that many services wanted to integrate the Sign-In with Ethereum workflow but did not have the ability to add new passwordless authentication methods to their installations.
We also learned that most services already support OpenID Connect, and were open to adding a new Identity Provider that supported the SIWE workflow. By meeting those services where they are today, we can provide a pragmatic stepping stone towards true decentralization, with an upgrade path to direct authentication.
To ensure adherence to the vision, it's critical that we collaborate with the ENS DAO on hosting and maintenance of this identity server, ensuring the identity server's governance ultimately resides with the community, whom we believe will always put users first. This would be the world's first DAO-hosted, transparent identity server.
## Rationale
The ENS service and community would benefit from increased adoption of Sign-In with Ethereum due to the enablement of organizations to use ENS as a core touchpoint for a user's basic identity and information (via ENS profiles).
Additionally, we believe that a community server could be governed by a credibly neutral party that Ethereum users accept as an intermediary. We think a non-profit or DAO are the right structures to help govern such a server, which is why we would like to collaborate with the ENS DAO on hosting and maintenance.
## Specification and Proposal Request
- **Establish a Subgroup in the Ecosystem Working Group: Community Managed Identity Server**
- **$250,000** allocated from the DAO to the Ecosystem WG to fund this Subgroup.
- **$75,000** from the allocated budget will be in place for community contributions related to the Subgroup, including grants for development, evangelism, and retroactive rewards for SIWE-related efforts.
- **$175,000** from the allocated budget will go towards Spruce's maintenance contract (see below). Paid 25% upon execution, and then an additional 25% every 3 months.
- This Subgroup will support the administration and maintenance of a DAO-run Identity Server for Sign-In with Ethereum. This subgroup will also serve as a support system to help onboard organizations, and evangelize Sign-In with Ethereum to allow users to control their identifiers and use ENS profiles as a base identity.
- An important part of duties this group will include creating training, onboarding, and maintenance materials for managing the server on a specified cloud account.
- Additionally, the group will be responsible for providing updates to the broader community on the health of the server.
- Initial lead: Rocco from Spruce, while continuing to add interested parties to the group for good governance.
- **A 12-month maintenance contract awarded to Spruce for the continuous monitoring, maintaining, and improving of the deployed Identity Server.**
- Spruce will help host a [`siwe-oidc`](https://github.com/spruceid/siwe-oidc) implementation in a lightweight fashion, using a well-known infrastructure provider ultimately administered by the Subgroup.
- Spruce will also be responsible for the deployment, and continuous monitoring, maintenance, and improvements on the server throughout the duration of the contract.
- If the DAO votes to end the contract funding will be returned against the remaining days of the year and we will provide sufficient training for administrators to transfer those duties to a new organization.
- The server is expected to be live within 60 days of this proposal being accepted, assuming that access to the necessary systems and people is provided on a timely basis.
- The 1-year contract begins when this proposal is accepted, and there will not be additional setup fees even if there are increased coordination costs to get the service running.
---
# [EP1.8] [Social] Working Group Rules
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/12953) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xc7186cf8bebe47600f8d847e76f7971ea97b48bc04eda1e07780aff91fb6410d) |
_Note: This was previously numbered EP12._
## Abstract
This is a proposal to repeal the working group rules passed in [EP4](https://docs.ens.domains/v/governance/governance-proposals/ep4-social-proposal-creation-of-foundational-working-groups-and-working-group-rules) and replace those rules with working group rules specified in this proposal for the Second Term of 2022 and all Terms thereafter.
The working group rules specified in this proposal add more details about Steward responsibilities and the management of working group funds, as well as introducing the requirement of each working group to appoint a lead steward.
The specification below also introduces a rule to appoint a new DAO Secretary, responsible for managing working relationships and communications across working groups as well as performing administrative duties for the DAO.
## Specification
### Working Groups Rules
1. Formation of Working Groups
1. To create a new working group, a social proposal, as defined by the ENS governance documentation (‘Social Proposal’), must be put forward and passed by the DAO.
2. A Social Proposal to create a new working group must demonstrate that the new working group is needed and the work cannot be undertaken within an existing working group.
2. Dissolution of Working Groups
1. A working group can be dissolved by passing a Social Proposal requesting the dissolution of a working group or working groups.
2. If an active proposal is put forward to dissolve a working group, all working group funds, including outgoing payments, within that working group, are to be frozen with immediate effect, pending the outcome of that vote.
3. Upon the dissolution of a working group, any and all unspent working group funds from that working group, at the time of dissolution, must be immediately returned to the DAO treasury, without delay.
3. Working Group Stewards
1. Each working group shall be managed by three stewards (hereafter a 'Steward' or 'Stewards').
2. Stewards will be elected to serve within working groups for a set period of time (hereafter known as a 'Term' or 'Terms').
3. There are two Terms each calendar year:
1. The first Term commences at 9am UTC on January 1 each year and ends immediately prior to the commencement of the second Term ('First Term'); and
2. The second Term commences at 9am UTC on July 1 each year and ends immediately prior to the commencement of the First Term of the following year ('Second Term').
4. Stewards are responsible for overseeing the operation of working groups in accordance with these rules and the ENS DAO constitution.
5. The responsibilities of Stewards include, but are not limited to:
1. Requesting working group funds from the DAO in accordance with these rules;
2. Approving the creation of sub-groups or workstreams within a working group to undertake work and/or carry out specific projects or tasks;
3. Dissolving sub-groups or workstreams within a working group;
4. Using discretion to make working group funds available to sub-groups, workstreams, or contributors within a working group;
5. Using discretion to disburse working group funds to people and/or projects in accordance with the ENS DAO constitution; and
6. Acting as keyholders of working group multi-sigs.
4. Steward Eligibility and Nominations
1. Any individual is eligible to nominate themselves to be a Steward of a working group within the DAO ('Eligible Person' or 'Eligible Persons').
2. To be eligible to be included in the ballot for First Term elections of a given year, Eligible Persons must nominate themselves between 9am UTC on December 6 and 9am UTC on December 9 ('First Term Nomination Window').
3. To be eligible to be included in the ballot for Second Term elections of a given year, Eligible Persons must nominate themselves between 9am UTC on June 6 and 9am UTC on June 9 ('Second Term Nomination Window').
4. An Eligible Person may nominate themselves to become a Steward of a working group or working groups during the First Term Nomination Window or the Second Term Nomination Window (each a 'Nomination Window'), by meeting the requirements set out in a call for nominations posted in the relevant working group category of the ENS governance forum.
5. An Eligible Person who completes the steps outlined in rule 4.4 above during a Nomination Window and receives 10,000 signed votes to support their nomination will be included in the ballot as a nominee in the election for Stewards that takes place following that Nomination Window ('Nominee').
5. Steward Elections
1. Elections for working group Stewards for the First Term of a given year will take place by a vote of governance token holders using signed messages and will be open for 120 hours, commencing at 9am UTC on December 10 each year ('First Term Election Window').
2. Elections for working group Stewards for the Second Term of a given year will take place by a vote of governance token holders using signed messages and will be open for 120 hours, commencing at 9am UTC on June 10 each year ('Second Term Election Window').
3. The top-ranked Nominees from each working group vote held during a First Term Election Window or a Second Term Election Window (each an 'Election Window'), will fill any available positions for the role of Steward for those working groups for the Term immediately following an Election Window, based on the order in which they are ranked in each working group vote.
4. A Nominee elected to serve as a Steward may not take up the role of Steward for more than two working groups during a single Term.
6. Delay of Nominations or Elections
1. In the event that nominations or elections for Stewards take place after a Nomination Window or after an Election Window, the nomination process and/or elections shall take place, as otherwise prescribed in rules 4 and 5 above, as soon as is practicable after the missed Nomination Window or missed Election Window.
2. In the event that an election takes place outside of an Election Window and after the commencement date of a new Term, outgoing Stewards from the previous Term shall stay in their positions as working group Stewards until immediately prior to 9am UTC the day following the end of the election, which, for the avoidance of doubt, is 120 hours after voting in those elections commenced.
3. In the event that an election takes place outside of an Election Window and after the commencement date of a new Term, newly elected Stewards will assume the responsibilities of stewardship within working groups at 9am UTC the day following the end of the election, as defined in rule 6.2 above, for the remainder of that Term.
7. Removal and Replacement of Stewards
1. Stewards may be removed at any time by:
1. a Social Proposal passed by the DAO; or
2. a simple indicative majority vote among Stewards of all working groups, with the outcome of that vote communicated in the relevant working group category of the ENS governance forum.
2. Stewards may step down from their position at any time by communicating their intention to step down in the ENS governance forum.
3. In the event that a Steward is removed, steps down, or is unable to continue as a Steward, for whatever reason, prior to the end of a Term, a new election must be held to fill any vacant Steward positions, in accordance with rule 6 above.
8. Lead Stewards
1. Each working group must appoint a lead Steward within the first five days of a Term (hereafter a 'Lead Steward' or 'Lead Stewards').
2. Only current elected Stewards of a working group are eligible to serve as Lead Stewards within a given working group.
3. Lead Stewards may be appointed or removed from that role at any time by a simple indicative majority vote among the Stewards of a working group, with the outcome of that vote communicated in the relevant working group category of the ENS governance forum.
4. In the event that a Lead Steward steps down from the position or is removed as a Lead Steward before the end of a Term in accordance with rule 8.3 above, a new Lead Steward must be appointed within five calendar days.
5. A Steward who is appointed to serve as a Lead Steward of a working group will remain in that position, as Lead Steward, from the date of appointment until the end of their elected Term as a Steward or until they are removed as a Lead Steward in accordance with rule 8.3 above or until they are removed as a Steward in accordance with rule 7 above.
6. Lead Stewards are responsible for the operational management and administration of working groups and are expected to provide regular updates to the DAO in the ENS governance forum related to working group progress, achievements, and challenges.
7. The responsibilities of Lead Stewards include, but are not limited to:
1. Acting as a representative of a working group;
2. Managing resource requests from sub-groups, workstreams, and contributors within a working group;
3. Initiating the disbursement of working group funds on an as-needed basis;
4. Providing reports of working group spending in the ENS governance forum; and
5. Maintaining open communications with DAO participants in the ENS governance forum.
9. DAO Secretary
1. At the start of each Term, the current Stewards of each working group shall collaborate to appoint an individual who will serve as the secretary of the DAO (hereafter 'Secretary' or 'Secretaries').
2. The Secretary may be appointed or removed from that role at any time by a majority vote of all elected Stewards in a given Term with the outcome of that vote communicated in the ENS governance forum.
3. The Secretary will remain in that position, as Secretary of the DAO, from the date of appointment until the end of a given Term or until the date at which they are removed from that position in accordance with rule 9.2 above.
4. Secretaries are eligible to receive fair compensation for their work as Secretary of the DAO.
5. Compensation for the Secretary of the DAO is to be paid by the Meta-Governance Working Group using funds requested in accordance with rule 10 below.
6. Any individual is eligible to be appointed as the Secretary of the DAO, including past and present working group Stewards.
7. The Secretary is responsible for managing working relationships and communications across working groups as well as performing administrative duties for the DAO.
8. The responsibilities of the Secretary include, but are not limited to:
1. Managing a DAO-wide calendar;
2. Coordinating and attending working group meetings where possible and ensuring meeting summaries are posted in the ENS governance forum;
3. Assisting Stewards with coordination challenges within working groups; and
4. Acting as a multi-sig keyholder for each working group.
10. Working Group Funds
1. To request working group funds, Stewards of all working groups will collaborate to submit an active executable proposal, as defined by the ENS governance documentation ('Collective Proposal'), to the DAO during the months of January, April, July, and October each calendar year (each a 'Funding Window').
1. In order for a working group to have a funding request included in a Collective Proposal submitted to the DAO during a Funding Window, the funding request must have passed as a Social Proposal in the same Funding Window.
2. In the case of an emergency, where working group funds are needed by a working group outside of a Funding Window, an executable proposal, as defined by the ENS governance documentation, may be submitted at any time by a Steward of a working group to request funds from the DAO.
2. Working group funds requested and approved in accordance with rule 10.1 above are to be paid out into separate working group multi-sigs controlled by the DAO.
3. Each working group multi-sig must have four keyholders, made up of three current elected Stewards for that working group and the Secretary of the DAO for that Term, with no other keyholders permitted.
4. Working group funds may be disbursed from working group multi-sigs with three-of-four keyholder signing.
5. Stewards of a given working group shall have the discretion to reallocate funds approved in a Collective Proposal where appropriate and where it is not in conflict with any rules of the DAO, DAO bylaws, or the ENS DAO constitution.
11. Compensation for Stewards and Lead Stewards
1. Stewards are eligible to receive fair compensation for their work as a Steward or Lead Steward in the DAO.
2. All requests for Steward or Lead Steward compensation must be detailed in a Collective Proposal for working group funds submitted to the DAO in accordance with rule 10.1 above.
3. Stewards may not receive compensation for their role as a Steward or Lead Steward outside of that compensation expressly provided for in a Collective Proposal submitted to the DAO in accordance with rule 10.1 above.
12. Amendments
1. These rules may be amended at any time by passing a Social Proposal.
### Next Steps
This vote will be a single choice vote. You may vote 'for' or 'against' the proposal, or choose to abstain from the vote.
By voting 'for' this proposal, you are voting in favor of repealing the working group rules passed in EP4 and replacing them with the working group rules provided in the specification of this proposal.
---
# [EP 6.1] [Executable] Convert 6,000 ETH to USDC for DAO Operating Expenses
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-6-1-executable-convert-6-000-eth-to-usdc-for-dao-operating-expenses/20138) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/33808096277765934415068056906696425640427679116963285327644834080891069190379) |
## Abstract
We propose to convert 6,000 ETH into USDC to replenish the USDC reserves in the [DAO's treasury](https://etherscan.io/address/0xfe89cc7abb2c4183683ab71653c4cdc9b02d44b7).
## Motivation
In February 2023, the DAO executed a swap of 10,000 ETH into USDC via [EP 3.3](https://www.tally.xyz/gov/ens/proposal/45461903078948131870051132081249892009497709518413744958551889217805827301425), generating approximately $16.2M in USDC. The rationale at the time was to secure 18–24 months of operational runway.
Over the past 21 months, the USDC reserves have been effectively utilised to fund ENS DAO's operations. However, these reserves have now been fully depleted. To ensure financial stability and effective liquidity management, it is prudent to secure a 12-month runway in the DAO's wallet to cover operating expenses.
We propose ENS convert 6,000 ETH (~$20.4M at $3,200/ETH) to replenish the USDC reserves. These funds will be used to meet ongoing commitments, such as payments to [ENS Labs](https://discuss.ens.domains/t/ep-5-22-ensv2-development-funding-request/19762), and [Service Provider streams](https://docs.ens.domains/dao/proposals/5.2) and DAO Working Groups.
## Specification
The executable proposal will transfer 6,000 ETH from the ENS DAO Wallet to a [new Safe](https://etherscan.io/address/0x02D61347e5c6EA5604f3f814C5b5498421cEBdEB) created solely for the swap of ETH to USDC. The proposal will also sweep the ETH balance in the [Old Registrar Controller ](https://debank.com/profile/0x283af0b28c62c092c9727f1ee09c02ca627eb7f5)to the DAO wallet.
The payload to transfer 6k ETH from the DAO Multisig is available [here](https://gist.github.com/JeronimoHoulin/5e4728d36bf5d2b7ee08b9382f61bf78). Transaction details:
- From: ENS DAO Wallet ([wallet.ensdao.eth](https://etherscan.io/address/0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7#multichain-portfolio))
- Token: ETH
- Amount: 6,000
- Recipient: TWAP Safe ([0x02D6...BdEB](https://app.safe.global/home?safe=eth:0x02D61347e5c6EA5604f3f814C5b5498421cEBdEB))
karpatkey has deployed a [new Safe](https://app.safe.global/home?safe=eth:0x02D61347e5c6EA5604f3f814C5b5498421cEBdEB) dedicated solely for TWAPs, with karpatkey and ENS representatives as signers. The Safe has a threshold of 3 signatures and the following signers:
1. DAO Wallet ([wallet.ensdao.eth](https://etherscan.io/address/0xfe89cc7abb2c4183683ab71653c4cdc9b02d44b7))
2. karpatkey's Endowment manager Safe ([0xb423e0f6E7430fa29500c5cC9bd83D28c8BD8978](https://etherscan.io/address/0xb423e0f6E7430fa29500c5cC9bd83D28c8BD8978))
3. ENS Labs' cold wallet ([coldwallet.ens.eth](https://etherscan.io/address/0x690F0581eCecCf8389c223170778cD9D029606F2))
4. Metagov Working Group Safe ([main.mg.wg.ens.eth](https://etherscan.io/address/0x91c32893216dE3eA0a55ABb9851f581d4503d39b))
This allows for the segregation of funds between other ENS-related wallets (e.g. the Endowment Safe). This proposal sends 6,000 ETH to the newly created Safe.
Signers will swap ETH to USDC via Cow's TWAP mechanism.
The rationale behind this framework is that [Cow's TWAP function](https://docs.cow.fi/cow-protocol/reference/contracts/programmatic/twap) is currently not supported by Tally or the Zodiac Roles Modifier (ZRM) permissions. Once TWAP permissions are integrated, ZRM can be onboarded to this Safe, allowing for the removal of the ENS signers.
Safe keyholders will execute the following swaps:
1. 1,000 ETH swap to meet immediate funding needs by the ENS DAO, executed as soon as funds become available.
2. 5,000 ETH swap via a 3-month TWAP, conducted in 90 parts (~55.6 ETH sold per part), with wallet.ensdao.eth as recipient.
The payload to call withdraw() on the Old Registrar Controller, and send the funds to the DAO Multisig, is available [here](https://gist.github.com/JeronimoHoulin/f08e7a5704829df606602e53447569e3).
Transaction details:
- From: Old Registrar Controller (0x283af0b28c62c092c9727f1ee09c02ca627eb7f5)
- Token: ETH
- Amount: 4,241.966 (total balance)
- Recipient: ENS DAO Wallet (wallet.ensdao.eth)
---
# [EP 6.5] [Social] Service Provider Season 2 Vote Amendment Proposal II
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/20526/42) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x60c95ab69a427ce263f4c3c950df8da1134e96a3e76d139c8dac366271009530) |
## Abstract
Season 2 of the Service Provider Program was approved with a $4.5M budget on February 25th of this year. During the Delegate All Hands meeting on April 1st, delegates expressed a desire for more granular voting — not only to approve candidates, but also to vote on individual budget allocations.
In the weeks that followed, extensive discussions took place on how to address this through an amendment. Two main proposals emerged from these discussions, now formally designated as EP6.4 and EP6.5.
## Vote Results
There are now two active proposals to amend the Service Provider Program. Here's how the outcome of this vote should be interpreted:
- If this vote passes but receives fewer votes than EP6.4, then this amendment is considered null.
- If this vote passes and receives more votes than EP6.4, then EP6.4 is considered superseded.
- If this vote fails to reach either a majority or consensus, then only the results of EP6.4 should be considered valid.
Absolutely — here's a clearer and more polished version of your section on the main differences between the proposed amendments, with improved grammar, flow, and formatting, while staying faithful to your original content:
## Main Differences Between Proposed Amendments
- **Ballot Reordering and Expressivity**:
Both amendments enhance the expressiveness of the SPP voting system, allowing voters to choose both teams and their budgets (as opposed to the current model, where budget size is inferred from ranking). However, **EP6.5** goes further by enabling voters to place an _extended_ budget request **below multiple basic budgets**. This allows voters to express preferences such as: “This team should receive its extra funding, but only after these other teams get their basic funding.”
- **Ballot Interpretation**:
Both amendments involve reordering ballots based on how voters rank options.
- **EP6.4** always bundles a candidate’s basic and extended budget requests together, preserving their relative order in the original ballot.
- **EP6.5**, on the other hand, only reorders ballots when an _extended_ budget is ranked **above** its corresponding basic budget — to avoid accidental inconsistencies in voter intent.
- **Basic vs. Extended Budget Competition**:
- In **EP6.4**, a candidate’s basic and extended budgets compete only with each other. Whichever is ranked higher wins.
- In **EP6.5**, a candidate’s _extended_ budget competes with **all** other allocations for the same budget amount, not just the candidate’s own basic option. This allows for a more nuanced prioritization across teams.
- **Complexity of Rules**:
- **EP6.4** introduces a custom ballot preprocessing step, a distinct counting method, and special downgrade rules for extended budgets.
- **EP6.5** has a simplified preprocessing, but achieves similar outcomes through emergent behavior from its ruleset, which may lead to rankings on Snapshot that are more closely aligned with what is displayed in custom UIs.
---
## Amended rules for the Service Provider Program
### 1. Proposals
Teams can propose a **basic budget**, and optionally an **extended budget**, which is listed as the **extra amount** they’d like on top of the basic. The ballot would include all budget options as independent entries to be ranked independently.
Candidates will have a chance to edit their proposal, but as it stands, these are the current asks:
| Company | Basic Scope | Extra Ask |
| -------------------------------- | ----------- | --------- |
| **AlphaGrowth** | $400,000 | +$400,000 |
| **ZK.Email** | $400,000 | +$400,000 |
| **Blockful** | $400,000 | +$300,000 |
| **Unruggable** | $400,000 | +$300,000 |
| **3DNS** | $500,000 | +$200,000 |
| **Ethereum.Identity.Foundation** | $500,000 | +$200,000 |
| **JustaName** | $400,000 | +$200,000 |
| **NameHash.Labs** | $1,100,000 | +$200,000 |
| **Namespace** | $400,000 | +$200,000 |
| **Agora** | $300,000 | +$100,000 |
| **dWeb.host** | $300,000 | +$100,000 |
| **EthLimo** | $700,000 | +$100,000 |
| **Wildcard.Labs** | $300,000 | +$100,000 |
| **Curia.Lab** | $300,000 | – |
| **Decent** | $300,000 | – |
| **Enscribe** | $400,000 | – |
| **GovPal** | $300,000 | – |
| **Lighthouse_Labs** | $400,000 | – |
| **Namestone** | $800,000 | – |
| **PYOR** | $300,000 | – |
| **Tally** | $300,000 | – |
| **Unicorn.eth** | $300,000 | – |
| **Web3bio** | $500,000 | – |
| **WebHash** | $300,000 | – |
| **x23.ai** | $300,000 | – |
### 2. Preprocessing Ballots
Before counting, each ballot is checked: if a voter ranks a team’s extra budget above its basic, the basic entry is moved directly above the extra. No changes are made otherwise.
### 3. Creating the Rank
Each entry is treated as a separate candidate and ranked using the **Copeland method**. If two entries have the same number of match victories, **average support** is used as a tiebreaker (in a sports comparison, this would be equivalent to \"total points/goals scored\" being used as a tiebreaker between teams with equal number of victories).
### 4. Budget Allocation
Once ranking is complete, entries are evaluated in order, using a **total budget of $4.5 million**:
1. Assign an entry to the **2-year stream** if it is a **current service provider**, ranked in the **top 10**, _and_ assigning it would **not cause the total allocated to 2-year grants to exceed $1.5 million**.
2. If those conditions aren’t met, assign the entry to the **1-year stream** if its budget fits within the **remaining total budget** (regardless of the 2-year cap).
3. Stop the evaluation if the **$4.5M total budget** has been fully allocated, if there are **no more valid candidates**, _or_ if **“None Below”** is reached.
---
# [5.17.1] [Social] Funding Request: ENS Meta-Governance Working Group Term 5 (Oct. Window)
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/5-17-1-social-funding-request-ens-meta-governance-working-group-term-5-oct-window/19677) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x0f906ef744da4aace051305dff824fe7b000814f23af36f037f0dd23a1a94f98) |
## Abstract
The Meta-Governance Working Group is responsible for providing governance oversight and supporting the management and operation of working groups through DAO tooling and governance initiatives as well as treasury management for the DAO.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](/dao/proposals/1.8)) and further required by [this snapshot proposal in Nov. 2023 modifying steward rules](https://snapshot.org/#/ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266). If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
This specification is the amount requested from the DAO treasury to the Metagov Multisig to fulfill anticipated budgetary needs through the next formal funding window in April 2025.
| | USDC | ETH | $ENS |
| -------------------------- | ---- | --- | ---- |
| ENS Meta-Gov Main Multisig | 254k | 0 | 0 |
This amount will cover all expected expenses outlined below while leaving a 100k prudent reserve to ensure continuity if future funding is delayed.
## Description
**Current Metagov Wallet Balances**
| | USDC | ETH | $ENS |
| -------------------------- | ---- | ---- | ---- |
| ENS Meta-Gov Main Multisig | 286k | 83.7 | 51 |
\*Up to date balance information can be found at https://enswallets.xyz
## Expenditures
Meta-Gov sets aside funds to ensure coverage for mission-critical initiatives. While we strive to estimate term expenditures accurately, the final spending depends on pending initiatives.
### Expected Expenses through April 2025
| | USDC | ETH | $ENS |
| ---------------------------------- | ---- | --- | ---- |
| Steward + Secretary Compensation ¹ | 294k | - | - |
| Governance | - | - | - |
| Contract Audits | 155k | - | - |
| DAO Tooling | 150k | - | - |
| Discretionary | 40k | - | - |
| Total Balance | 639k | - | - |
### Governance Distributions
The governance distribution strategy for Term 6 will be announced and posted as a temp check in the forum. This strategy will then be codified by a DAO vote.
### Description of Initiatives/Pods
**Steward + Secretary Compensation**: Working Group Steward and Secretary compensation as required by [revised steward working group rules](https://snapshot.org/#/ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266) and [totaling $294,000 USDC to cover the costs for all 9 stewards and supporting roles for a 6 month period](https://discuss.ens.domains/t/ens-dao-steward-compensation/18063).
¹This is the expected compensation amount based on existing compensation. The Meta-governance working group is required by [EP 4.8](https://docs.ens.domains/dao/proposals/4.8) to announce compensation ahead of the next term's steward nominations. **The current Term 5 stewards will also be proposing Term 6 compensation numbers to the DAO to ensure consensus. This proposal will take place before the Term 6 nominations to comply with [EP 4.8].**
**Governance**: Fee reimbursements and initiatives related to reducing friction in the governance process. This can also include $ENS distributed in order to lower barriers to the governance proposal process. The $ENS distributions to stewards and service providers falls into this category.
**Contract Audits**: Meta-governance maintains a balance to be used for contract audits. These audits are performed independently on contracts that are to be included in executable proposals if those contracts impact or affect any ENS protocol or ENS DAO contracts or processes. Examples would be any proposed changes to the ENS DAO governor contract or protocol related contracts such as the registrar.
This line item is larger than normal because Meta-governance is anticipating potential audits for both governor upgrades and contracts from Service Providers that are contributing to the ENS v2 work.
**DAO Tooling**: Developing interfaces and dashboards to improve the governance process and increase transparency across the DAO. An example of DAO tooling spend is our current engagement with Agora as they help build out an enhanced DAO proposal flow to streamline the proposal process.
**Discretionary**: Funds distributed at the discretion of stewards towards new initiatives + governance experiments. In this cycle, we've consolidated the former DAO Sponsorship category into this discretionary category.
## Conclusion
This funding request will allow the ENS Meta-Governance Working Group to continue its essential work in providing governance oversight, supporting the management and operation of working groups, and ensuring effective treasury management for the DAO. The requested funds will enable us to maintain our ongoing initiatives and develop new tools to enhance the governance process. We are grateful for the community's ongoing support and engagement, which is crucial to the success of the ENS DAO. The Meta-Governance Working Group remains committed to serving the ENS community and driving the long-term growth and sustainability of the ecosystem.
---
# [5.4.2] [Social] Funding Request: ENS Public Goods Working Group Term 5 (Q1/Q2)
::authors
| **Status** | Passed |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-5-executable-funding-request-ens-public-goods-working-group-term-5-q1-q2/18885) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xd3437f1c9ece8a309da116be5bffaf31fa40df5361e04e43f9c913970a8746ee) |
## Abstract
The ENS Public Goods Working Group requests funding of the below to **support operations until the September 2024 funding window**.
The Public Goods working group funds projects and builders improving the Web3 ecosystems. This funding stream is authorized in [Article III](https://docs.ens.domains/dao/constitution#iii-income-funds-ens-and-other-public-goods) of the ENS DAO Constitution. This funding supports initiatives related to open-source software, tooling, research and any practical implementations that broadly benefit a wide range of users of Ethereum and Web3.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)) and further required by [this snapshot proposal in Nov. 2023 modifying steward rules.](https://snapshot.org/#/ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266) If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
The current balance of the Public Goods multisig contains 147.2k USDC and 33.5 ETH.
The working group requests an additional 450.3k USDC and 21.5 ETH from the DAO to accommodate this proposed budget.
## Current Balances (March 2024)
| USDC | ETH | ENS |
| ------ | ---- | --- |
| 147.2k | 33.5 | 200 |
\*Balance information can be found at [https://enswallets.xyz](https://enswallets.xyz/)
## Expected Spend (Through September 2024)
| Initiative | USDC | ETH |
| ------------------- | ------ | --- |
| Large Grants | 387.5k | 0 |
| Small Grants | 0 | 50 |
| Bounties | 45k | 0 |
| Events + Hackathons | 115k | 0 |
| Discretionary | 50k | 5 |
| Total | 597.5k | 55 |
\*Mult-sig contains 200 ENS that has no planned use. This may be transferred back to the DAO wallet during the term.
## Description of Initiatives
### [Large Grants](https://discuss.ens.domains/t/public-goods-group-budget-h1-2024/18725#large-grants-3)
Grants up to 50k USDC with applications accepted on a rolling basis throughout the year-long term. Large Grants will resume in Q2. With five grantees completing milestones from last term, the budget includes remaining payouts yet to be
disbursed. We plan to add at least two more grantees during Q2 while piloting new grants management software. In the second half of the year, we will run another full-size round supporting up to 10 grantees at a time with a 200k USD total prize
pool.
### [Small Grants](https://discuss.ens.domains/t/public-goods-group-budget-h1-2024/18725#small-grants-4)
Multiple micro-grantsvoted on by the community. Small Grants will resume during the first half of the year shortly after ETHDenver. We have added the amount expected to spend through the end of the year with no increase from last term. This is
approximately 12.5 ETH per quarter. With market fluctuations, stewards may right-size and lower the amounts distributed during round
### [Events and Hackathons](https://discuss.ens.domains/t/public-goods-group-budget-h1-2024/18725#events-and-hackathons-5)
The working group will support Public Goods events and hackathons. Funds have included expenses related to the funding of hackathons, events, and related participation in events (judging, panels, speaking) where necessary.
The current earmarked events are:
- [ETHGuatemala](https://ethereum.gt/)
- [EthLatam](https://ethlatam.org/)
- [ETHGlobal London](https://ethglobal.com/events/london2024)
- [ETHCanal](https://www.ethcanal.xyz/)
- [ETHCC](https://ethcc.io/)
- [ETHGlobal Brussels](https://ethglobal.com/events/brussels)
- [DAO Tokyo](https://dao-tokyo.xyz/)
- [ETHGlobal San Francisco](https://ethglobal.com/events/sanfrancisco2024)
- [ETHGlobal Bangkok](https://ethglobal.com/events/bangkok)
- [Devcon](https://devcon.org/en/)
This list is not guaranteed as several events are still in the planning stages.
The PG stewards will continuously assess opportunities to expand the public goods conversation and collaborations.
### [Discretionary](https://discuss.ens.domains/t/public-goods-group-budget-h1-2024/18725#discretionary-6)
The funds in this initiative are reserved for additional grant opportunities and expenses that arise during the term. Spending on this initiative is at the discretion of the working group stewards.
---
# [EP5.3] [Social] Determine ENS Labs' next steps in eth.link litigation
::authors
| **Status** | Approved $300k settlement |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep5-2-social-determine-ens-labs-next-steps-in-eth-link-litigation/18756) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x456ccb438eed5d189cbe51e5e36a88d2bb4dc0c61f12f6d9e310a7ba4798d5fc) |
## Abstract
Since 2017, ENS Labs has been operating eth.link as a public gateway for the Ethereum community, providing access to ENS+IPFS sites. Eth.link has been used by some of the largest projects in the space, including Uniswap.
Eth.link was kindly delegated to ENS Labs for this purpose by its owner, Virgil Griffith. In August 2022, a dispute arose between ENS Labs, Dynadot, GoDaddy, and Manifold Finance about ownership of eth.link after the transfer and auction of the domain without ENS' consent. The highest bidder for eth.link at the auction was Manifold Finance, who expressed their intention to [operate the service themselves](https://twitter.com/foldfinance/status/1566200320375476225). A full recitation of the facts surrounding the dispute can be found within [the complaint that ENS Labs filed](https://drive.google.com/file/d/1951CeiKQZ4jQvcpjKU53BnugVedIf8go/view?usp=sharing) in September 2022.
Before the domain could be transferred, ENS Labs and Virgil successfully obtained a preliminary injunction from a federal district court judge in Phoenix, Arizona. The Court ordered that Dynadot had to immediately return eth.link to ENS Labs. Since then, this litigation has been proceeding slowly through the courts. ENS Labs had to enforce the preliminary injunction as to Dynadot and Manifold Finance exited the lawsuit on jurisdictional grounds. Settlement discussions between the parties indicated that Manifold Finance should be included in any global settlement because of its potential claims to eth.link and against Dynadot.
Recently, Manifold Finance has extended a settlement offer. They are demanding $300,000, along with confidentiality and non-disparagement clauses. In return, they are offering an allparties settlement, which would result in the dismissal of the case and ENS Labs retaining the eth.link domain name.
ENS Labs believes that this is the appropriate juncture to solicit the DAO's input on how to proceed with the litigation. We are thus asking the DAO to instruct ENS Labs on the next steps to take in this case, whether that be agreeing to the settlement, offering a compromise amount, continuing the lawsuit, or dismissing the matter altogether, which would relinquish ENS Labs' possession of eth.link. Thus far, ENS Labs has been funding this litigation from its own finances. Our expenses so far amount to approximately $750,000. Continuing the case is likely to incur substantial additional costs.
Further, given the criticality of eth.link to the ENS ecosystem, we would like to request reimbursement from the DAO for the legal fees incurred in protecting possession of eth.link.
## Specification
This will be an approval vote, with four independent items:
1. Do you approve the proposed settlement?
2. Do you approve of offering a compromise amount?
3. Do you approve of continuing the litigation?
4. Do you approve reimbursing ENS labs for its legal expenses involved in pursuing this case?
You should vote on each outcome you would be happy with. For example, if you would be happy with either continuation or a compromise settlement, but not the full amount, you would vote for options 2, 3 and 4. If you prefer that the case be dismissed, vote only for option 4, or for no options.
If the reimbursement is not approved, or none of the options receive a majority (50%) of approving votes, ENS Labs will:
- Promptly post an interstitial on eth.link, warning users of the potential change in ownership and functionality of the domain, and offering alternatives including eth.limo.
- Dismiss the litigation without delay and without unnecessary further expenditure of funds.
Based on the outcome of the vote, ENS Labs will proceed with the case based on whichever of the courses of action (settlement or continuation) receives the higher proportion of approving votes. Subsequently, ENS Labs will post an executable proposal seeking reimbursement from the DAO for its legal expenses pursuing the case.
---
# [EP2.2.5] [Social] Selection of an ENS endowment fund manager
::authors
| **Status** | Passed, Karpatkey selected |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/15188) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0x1ab7ef84f6e904582d5b5b921944b5b1a8e36dbff1f1248fde87fef02b046816) |
## Abstract
Following the RFP process approved in EP2.2.4, the Meta-Governance Working Group stewards have selected a short list of potential fund managers for the DAO to elect to manage the ENS Endowment.
## Specification
EP2.2.4 set out a process for fund managers to submit proposals to the Meta-Governance Working Group for consideration as fund managers over an ENS Endowment. The Endowment's goal is to ensure the long-term viability of ENS by ensuring it can meet its ongoing financial obligations even in the face of reduced ETH price and registration/renewal revenue.
Eight submissions were made as a result of the RFP process, and the stewards have narrowed this down to three finalists. All three finalists are established actors with track records of fund management, and their proposals reflect an understanding of the low-risk, long-term sustainability approach that an endowment should take.
Rather than select a winner themselves, the Meta-Governance Working Group Stewards believe that the selection of a fund manager should be put to the DAO. This EP implements a Snapshot vote using Ranked Choice voting with the following options:
1. Avantgarde
2. Karpatkey
3. Llama
4. None of the above
Submissions for the vote are listed in alphabetical order.
Following the conclusion of the vote, the Meta-Governance Stewards will work with the winning team to develop an initial treasury allocation strategy, resulting in a separate executable proposal to enact the initial setup of the endowment.
### Submission Summary
The table below summarizes the final submissions made by each candidate.
| Submission | Initial Size | Target Size | Performance Fee | Admin Fee | Breakeven Return |
| -------------------------------------------------------------------------------------------------- | --------------- | ---------------- | --------------- | ----------------- | ---------------- |
| [Avantgarde](https://discuss.ens.domains/t/endaoment-proposal-avantgarde/14800?u=nick.eth) | 25,000 ETH | $100,000,000 USD | 12.5% | 0.5% | 0.57% |
| [Karpatkey](https://discuss.ens.domains/t/endaoment-proposal-karpatkey-steakhouse-financial/14799) | $52,000,000 USD | $69,000,000 USD | 10% | 0.5% | 0.56% |
| [Llama](https://discuss.ens.domains/t/updated-endaoment-proposal-llama-x-alastor/15183) | 30,000 ETH | $80,000,000 USD | 0% | 1% min. $500k USD | 1% - 1.28% |
Breakeven return is calculated as `admin_fee / (1 - performance_fee)`.
---
# [EP 5.20] [Social] ENS Endowment Investment Policy Statement
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/temp-check-proposal-for-introduction-of-ens-investment-policy-statement/19568) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x085a1e40c264ffd44567b6dce889f5943e72cfa8442eaeb81819261a38f0bd0a) |
## Abstract
This proposal suggests introducing and implementing an [Investment Policy Statement (IPS)](https://drive.google.com/file/d/1NtaKb-9HJf9wJmVOq4sQbhsYQ4yZtTF2) for the ENS Endowment.
The Investment Policy Statement (“IPS”) outlines the principles and guidelines for the effective management of the ENS Endowment Fund (the “Endowment”). And provides additional clarity to the Endowment Manager in their duties of achieving the mandate of the ENS Endowment established in [EP 2.2.4](https://discuss.ens.domains/t/ep2-2-4-social-rfp-ens-endowment/14069).
## Specification
The creation and implementation of the [IPS](https://drive.google.com/file/d/1NtaKb-9HJf9wJmVOq4sQbhsYQ4yZtTF2), will serve the following purposes:
1. Define and assign the responsibilities of all parties involved in the Endowment.
2. Establish clear investment goals and objectives for the Endowment assets.
3. Offer guidance and limitations to the Endowment Manager regarding the investment of the Endowment assets.
4. Establish a basis for evaluating the performance of the Endowment Manager.
5. Express the prudent standards that are expected in the management of the Endowment assets.
The proposed ENS Investment Policy Statement to be ratified can be read: [here](https://drive.google.com/file/d/1NtaKb-9HJf9wJmVOq4sQbhsYQ4yZtTF2).
As specified in the IPS, this document will be continually reviewed annually.
### Updates from Feedback
Following discussions, the community provided feedback on the draft IPS posted in the [September temp check post](https://discuss.ens.domains/t/temp-check-proposal-for-introduction-of-ens-investment-policy-statement/19568) and the weekly MetaGov calls.
Based on those discussions, the following changes have been incorporated into the final version of the IPS:
- **More conservative asset allocation:** Increase stablecoin allocation in the Endowment to 40% (up from 20%).
- **Regular transfers:** Introduce regular monthly transfers of income from the Controller to the Endowment to support continuous growth and reduce future funding requests.
## Proposal Success Criteria
For this social proposal to pass, the following quorum and voting requirements must be met:
- **Quorum:** The proposal must receive a minimum of 1% of the total supply of $ENS (1 million votes) in the form of "For" and "Abstain" votes combined. "Against" votes do not count towards quorum.
- **Approval:** Once the quorum is reached, the proposal requires a simple majority (>50%) of "For" votes among the "For" and "Against" votes to pass. "Abstain" votes do not count towards the approval calculation.
If approved, the ENS DAO should adopt the final Investment Policy Statement prepared by karpatkey for the ENS Endowment established under [EP 2.2.4](https://discuss.ens.domains/t/ep2-2-4-social-rfp-ens-endowment/14069). If not approved, karpatkey will reassess and propose an alternative structure based on the feedback received.
---
# [EP4.1] [Executable] Approve further actions and strategies for the Endowment
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/17406) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/55447465396737793905646186593156244424717001140618132725073945884287085787959) |
## Abstract
This proposal introduces additional actions and strategies to the [ENS Endowment](https://discuss.ens.domains/t/ep3-4-executable-fund-the-endowment-first-tranche/16277?u=alisha.eth), which enhance the Endowment's performance, adaptability, and diversification.
## Motivation
Permissions granted to Karpatkey to manage the Endowment are not static, and require regular updates to effectively respond to evolving market conditions and to capitalize on emerging opportunities.
Any changes to the permissions related to Endowment funds ([endowment.ensdao.eth](https://etherscan.io/address/0x4F2083f5fBede34C2714aFfb3105539775f7FE64)) must be executed by the DAO. To streamline the process for delegates, Karpatkey has bundled all non-urgent changes in this proposal.
### Endowment Update
The latest weekly report shows that the Endowment's ncAUM (non-custodial Assets Under Management) amount is $30.1M, with a capital utilisation of 99.95% and an APR (Annual Percentage Rate) of 2.78%.
Weekly updates on the Endowment can be found in the ENS governance forum [here](https://discuss.ens.domains/t/endowment-weekly-reports/16665?u=alisha.eth). Monthly updates can be found in the ENS governance forum [here](https://discuss.ens.domains/t/ens-financial-reporting-by-steakhouse/16601?u=alisha.eth).
## Specification
Permissions that will be added in this proposal are:
1. Deposit USDC on Compound v3;
2. Deposit DAI on AAVE v3;
3. Deposit USDC on AAVE v3;
4. Deposit DAI on Maker's DSR module;
5. Deposit wstETH - ETH on Balancer;
6. Stake ETH - stETH on Convex;
7. Deposit and stake rETH - WETH on Balancer;
8. Stake rETH - WETH on Aura;
9. Deposit and stake Compound USDC DAI on Curve;
10. Stake Compound USDC DAI on Convex;
11. Deposit and stake Balancer Boosted Aave V3 USD on Balancer;
12. Stake Balancer Boosted Aave V3 USD on Aura;
13. Add swap route CVX - WETH on Curve;
14. Migrate wstETH-WETH position to a new gauge on Aura,
15. Add swap route processor for SushiSwap's concentrated liquidity (v3) pools, and
16. Add swapping options on Cow Swap
The majority of the actions mentioned above were initially presented in the proposal that resulted in the selection of Karpatkey as the Endowment Manager. However, it is important to note that there is one exception, namely action #4 - Deposit DAI on Maker's DSR module. This particular opportunity has emerged as an appealing addition subsequent to the original proposal, presenting a new avenue that was not available at the time of its submission. More details about the new actions are posted below as a comment to this post.
Moving forward, this initial list of actions will be subject to updates based on future market conditions, ensuring the Endowment remains responsive and adaptable.
### Additional information
We propose an updated version of the "Preset permissions - ENS Endowment" [document](https://docs.google.com/document/d/1ccSDe25I9ojvlbPGT5acqD71LOY9U8xkjBYWJgjJtgI/edit?usp=sharing) that shows all the permissions granted to Karpatkey, including the ones that are being requested now.
The payload to update the preset that needs to be executed by the ENS DAO upon a successful on-chain vote is posted below as a comment to this post along with some additional considerations.
---
# [EP 5.26] [Executable] Implementation of EP 5.19’s ENS Governance Distribution Pilot Program
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-26-executable-implementation-of-ep-5-19-s-ens-governance-distribution-pilot-program/19878) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/50152158826647742094695349340830523178083147237337111134725087674188893435887) |
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-26-executable-implementation-of-ep-5-19-s-ens-governance-distribution-pilot-program/19878) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/50152158826647742094695349340830523178083147237337111134725087674188893435887) |
## Abstract
This executable proposal implements the Governance Distribution Pilot Program approved in [EP5.19](https://snapshot.org/#/ens.eth/proposal/0xfa54ff2b55f0495c96ec2d8645241bcff48ca6afe1f4925fb51f29c4667252df). Following the community's selection of the 30k ENS distribution option, this proposal authorizes the transfer of ENS tokens from the treasury to implement the program.
## Specification
This proposal authorizes:
1. The transfer of 30,000 ENS tokens from the DAO treasury to the Meta-governance working group's main wallet (main.mg.wg.ens.eth) for distribution to eligible recipients
2. The Meta-governance working group will then distribute these tokens through 2-year linear vesting contracts according to the allocation table below
This distribution follows the quadratic funding method, which is a progressive ratio where recipients receive ENS tokens proportional to the square root of their USDC/ETH contributions during January-September 2024. The complete distribution table is as follows:
### Recipients
| From | To | SUM of Value | Squared Weight | % of total | ENS | % of value received |
| --------------------- | ---------------------------------- | -------------- | -------------- | ---------- | --------- | ------------------- |
| Ecosystem | ETHGlobal | $ 190,000.00 | 435.9 | 5.2% | 1,569.85 | 11.6% |
| | wslyvh.eth | $ 50,004.00 | 223.6 | 2.7% | 805.35 | 22.5% |
| | Onthis | $ 25,000.00 | 158.1 | 1.9% | 569.44 | 31.9% |
| | ipns.eth | $ 25,000.00 | 158.1 | 1.9% | 569.44 | 31.9% |
| | Fluidkey | $ 25,000.00 | 158.1 | 1.9% | 569.44 | 31.9% |
| | Blockscout | $ 25,000.00 | 158.1 | 1.9% | 569.44 | 31.9% |
| | beaconchain.eth | $ 25,000.00 | 158.1 | 1.9% | 569.44 | 31.9% |
| | 1w3.eth | $ 25,000.00 | 158.1 | 1.9% | 569.44 | 31.9% |
| | Urbe Campus | $ 14,680.00 | 121.2 | 1.5% | 436.36 | 41.6% |
| | Discord Support | $ 13,000.00 | 114.0 | 1.4% | 410.63 | 44.2% |
| | generalmagic.eth | $ 11,563.43 | 107.5 | 1.3% | 387.28 | 46.9% |
| | Pugson | $ 10,000.00 | 100.0 | 1.2% | 360.15 | 50.4% |
| | Juicebox | $ 10,000.00 | 100.0 | 1.2% | 360.15 | 50.4% |
| | frolic.eth | $ 10,000.00 | 100.0 | 1.2% | 360.15 | 50.4% |
| | Drips | $ 10,000.00 | 100.0 | 1.2% | 360.15 | 50.4% |
| | Socket | $ 5,000.00 | 70.7 | 0.8% | 254.66 | 71.3% |
| | ENS Fairy | $ 4,781.34 | 69.1 | 0.8% | 249.03 | 72.9% |
| | weird3.eth | $ 3,000.00 | 54.8 | 0.7% | 197.26 | 92.1% |
| | stephancill.eth | $ 2,367.89 | 48.7 | 0.6% | 175.25 | 103.6% |
| | Scope.sh | $ 2,367.89 | 48.7 | 0.6% | 175.25 | 103.6% |
| | Kiwi News | $ 2,367.89 | 48.7 | 0.6% | 175.25 | 103.6% |
| | aexek.eth | $ 1,750.00 | 41.8 | 0.5% | 150.66 | 120.5% |
| Ecosystem Total | | $ 490,882.44 | | | | |
| Public Goods | Rotki | $ 53,973.38 | 232.3 | 2.8% | 836.70 | 21.7% |
| | gashawk.eth | $ 40,000.00 | 200.0 | 2.4% | 720.29 | 25.2% |
| | buidlguidl.eth | $ 35,000.00 | 187.1 | 2.2% | 673.77 | 27.0% |
| | borderlessafrica.eth | $ 30,000.00 | 173.2 | 2.1% | 623.79 | 29.1% |
| | Revoke.Cash | $ 20,000.00 | 141.4 | 1.7% | 509.33 | 35.7% |
| | Firefly | $ 20,000.00 | 141.4 | 1.7% | 509.33 | 35.7% |
| | EIP-7212 | $ 20,000.00 | 141.4 | 1.7% | 509.33 | 35.7% |
| | ethdaily.eth | $ 14,797.60 | 121.6 | 1.5% | 438.10 | 41.4% |
| | Dappnode | $ 12,500.00 | 111.8 | 1.3% | 402.66 | 45.1% |
| | ETHDenver | $ 10,000.00 | 100.0 | 1.2% | 360.15 | 50.4% |
| | pairwise.eth | $ 8,402.18 | 91.7 | 1.1% | 330.12 | 55.0% |
| | apoorv.eth | $ 7,021.88 | 83.8 | 1.0% | 301.79 | 60.2% |
| | leticiaferraz.eth | $ 6,949.32 | 83.4 | 1.0% | 300.23 | 60.5% |
| | Urbe Campus | $ 5,000.00 | 70.7 | 0.8% | 254.66 | 71.3% |
| | Latin Hackathon | $ 5,000.00 | 70.7 | 0.8% | 254.66 | 71.3% |
| | eth-mexico.eth | $ 5,000.00 | 70.7 | 0.8% | 254.66 | 71.3% |
| | DAOstar | $ 5,000.00 | 70.7 | 0.8% | 254.66 | 71.3% |
| | aynieducativo.eth | $ 5,000.00 | 70.7 | 0.8% | 254.66 | 71.3% |
| | glodollar.eth | $ 3,762.22 | 61.3 | 0.7% | 220.90 | 82.2% |
| | modularcrypto.eth | $ 1,818.29 | 42.6 | 0.5% | 153.57 | 118.2% |
| | daveytea.eth | $ 1,818.29 | 42.6 | 0.5% | 153.57 | 118.2% |
| | UPE | $ 940.56 | 30.7 | 0.4% | 110.45 | 164.4% |
| | illuminated.eth | $ 940.56 | 30.7 | 0.4% | 110.45 | 164.4% |
| | dhive.eth | $ 940.56 | 30.7 | 0.4% | 110.45 | 164.4% |
| | bloomnetwork.eth | $ 940.56 | 30.7 | 0.4% | 110.45 | 164.4% |
| | pabl0cks.eth | $ 877.74 | 29.6 | 0.4% | 106.70 | 170.2% |
| | iviangita.eth | $ 877.74 | 29.6 | 0.4% | 106.70 | 170.2% |
| | easlabs.eth | $ 877.74 | 29.6 | 0.4% | 106.70 | 170.2% |
| | 2118.eth | $ 877.74 | 29.6 | 0.4% | 106.70 | 170.2% |
| Public Goods Total | | $ 318,316.31 | | | | |
| Metagov | Karpatkey | $ 187,149.13 | 432.6 | 5.2% | 1,558.02 | 11.7% |
| | daemon.eth | $ 27,000.00 | 164.3 | 2.0% | 591.78 | 30.7% |
| | Lemma | $ 9,998.67 | 100.0 | 1.2% | 360.12 | 50.4% |
| | Tally | $ 8,999.54 | 94.9 | 1.1% | 341.66 | 53.1% |
| | Event Support | $ 3,000.00 | 54.8 | 0.7% | 197.26 | 92.1% |
| | daveytea.eth | $ 1,000.00 | 31.6 | 0.4% | 113.89 | 159.4% |
| | andrewpage.eth | $ 780.00 | 27.9 | 0.3% | 100.58 | 180.5% |
| Metagov Total | | $ 237,927.35 | | | | |
| Immunefi Bounty | Immunefi | $ 102,000.00 | 319.4 | 3.8% | 1,150.22 | 15.8% |
| | @UGWST_COM | $ 75,000.00 | 273.9 | 3.3% | 986.30 | 18.4% |
| | @navad | $ 7,500.00 | 86.6 | 1.0% | 311.90 | 58.2% |
| | @Sagamore | $ 6,000.00 | 77.5 | 0.9% | 278.97 | 65.1% |
| | @adhd | $ 5,000.00 | 70.7 | 0.8% | 254.66 | 71.3% |
| | @solidityhaxor | $ 2,500.00 | 50.0 | 0.6% | 180.07 | 100.8% |
| | @haoce505 | $ 2,500.00 | 50.0 | 0.6% | 180.07 | 100.8% |
| | @h4nt3rx | $ 2,500.00 | 50.0 | 0.6% | 180.07 | 100.8% |
| | @austinoa012 | $ 1,000.00 | 31.6 | 0.4% | 113.89 | 159.4% |
| Immunefi Bounty Total | | $ 204,000.00 | | | | |
| ETH Global | TF Guo | $ 3,000.00 | 54.8 | 0.7% | 197.26 | 92.1% |
| | Nikolas Lionis | $ 2,000.00 | 44.7 | 0.5% | 161.06 | 112.7% |
| | Arthur Sabirzyanov | $ 2,000.00 | 44.7 | 0.5% | 161.06 | 112.7% |
| | Stephan Wittig | $ 1,500.00 | 38.7 | 0.5% | 139.48 | 130.2% |
| | Rashmi Abbigeri | $ 1,500.00 | 38.7 | 0.5% | 139.48 | 130.2% |
| | Mehran Saliminia | $ 1,500.00 | 38.7 | 0.5% | 139.48 | 130.2% |
| | Mattis Deisen | $ 1,500.00 | 38.7 | 0.5% | 139.48 | 130.2% |
| | Ben Levy | $ 1,375.00 | 37.1 | 0.4% | 133.55 | 136.0% |
| | Aryeh Greenberg | $ 1,375.00 | 37.1 | 0.4% | 133.55 | 136.0% |
| | Shoma Shiga | $ 1,250.00 | 35.4 | 0.4% | 127.33 | 142.6% |
| | Rao Araki | $ 1,250.00 | 35.4 | 0.4% | 127.33 | 142.6% |
| | sudolabel | $ 1,000.00 | 31.6 | 0.4% | 113.89 | 159.4% |
| | Simone Staffa | $ 1,000.00 | 31.6 | 0.4% | 113.89 | 159.4% |
| | Paolo Rollo | $ 1,000.00 | 31.6 | 0.4% | 113.89 | 159.4% |
| | Francesco Cirulli | $ 1,000.00 | 31.6 | 0.4% | 113.89 | 159.4% |
| | Yong Feng Ng | $ 875.00 | 29.6 | 0.4% | 106.53 | 170.5% |
| | Yik Kai Ng | $ 875.00 | 29.6 | 0.4% | 106.53 | 170.5% |
| | Junyao Chan | $ 875.00 | 29.6 | 0.4% | 106.53 | 170.5% |
| | Jing Jie Ng | $ 875.00 | 29.6 | 0.4% | 106.53 | 170.5% |
| | Tom Chauveau | $ 750.00 | 27.4 | 0.3% | 98.63 | 184.1% |
| | Roman GASCOIN | $ 750.00 | 27.4 | 0.3% | 98.63 | 184.1% |
| | Coline SEGURET | $ 750.00 | 27.4 | 0.3% | 98.63 | 184.1% |
| | Wojciech Staniszewski | $ 500.00 | 22.4 | 0.3% | 80.53 | 225.5% |
| | Patrick Fuchs | $ 500.00 | 22.4 | 0.3% | 80.53 | 225.5% |
| | Pascal Rüger | $ 500.00 | 22.4 | 0.3% | 80.53 | 225.5% |
| | Luca Orbke | $ 500.00 | 22.4 | 0.3% | 80.53 | 225.5% |
| | Joanna Daniluk | $ 500.00 | 22.4 | 0.3% | 80.53 | 225.5% |
| | Eason Chai | $ 500.00 | 22.4 | 0.3% | 80.53 | 225.5% |
| | Bartłomiej Tarczyński | $ 500.00 | 22.4 | 0.3% | 80.53 | 225.5% |
| | Antoni Koszowski | $ 500.00 | 22.4 | 0.3% | 80.53 | 225.5% |
| | Andrzej Daniel | $ 500.00 | 22.4 | 0.3% | 80.53 | 225.5% |
| | Yoshiki Takabayashi | $ 250.00 | 15.8 | 0.2% | 56.94 | 318.9% |
| | Shritesh Jamulkar | $ 250.00 | 15.8 | 0.2% | 56.94 | 318.9% |
| | Rohit Ramesh | $ 250.00 | 15.8 | 0.2% | 56.94 | 318.9% |
| | Nishen Kaushika Menerapitiyage Don | $ 250.00 | 15.8 | 0.2% | 56.94 | 318.9% |
| | Helen Femi Williams | $ 250.00 | 15.8 | 0.2% | 56.94 | 318.9% |
| | Francisco Cordero | $ 250.00 | 15.8 | 0.2% | 56.94 | 318.9% |
| | Danya Carolina Gómez Cantú | $ 250.00 | 15.8 | 0.2% | 56.94 | 318.9% |
| | Bianca Trovò | $ 250.00 | 15.8 | 0.2% | 56.94 | 318.9% |
| | Bernardo Vieira | $ 250.00 | 15.8 | 0.2% | 56.94 | 318.9% |
| | Adaku Agwunobi | $ 250.00 | 15.8 | 0.2% | 56.94 | 318.9% |
| ETH Global Total | | $ 35,000.00 | | | | |
| Grand Total | | $ 1,286,126.10 | 8329.9 | 100% | 30,000.00 | |
1. Value represents the total received of all transactions
2. Squared Weight is simply the square root of that value, which is used as a weight
3. % of total divided that by the total weight
4. ENS multiplies that percentage to the total budget
5. % of value received is a comparison of the value received in USDC with the value received in ENS, with ENS at $14
## Transaction
```json
{
"target": "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72",
"value": 0,
"calldata": "0xa9059cbb00000000000000000000000091c32893216de3ea0a55abb9851f581d4503d39b00000000000000000000000000000000000000000000065a4da25d3016c00000"
}
```
## Verification
- Target: ENS token contract (0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72)
- Recipient: main.mg.wg.ens.eth (0x91c32893216dE3eA0a55ABb9851f581d4503d39b)
- Amount: 30,000 ENS (30000000000000000000000 in wei)
- Function: transfer(address,uint256)
---
The Meta-governance working group will handle subsequent distributions to individual recipients through Hedgey vesting contracts following the approved allocation table.
The transaction will move the 30k $ENS to the main Meta-governance wallet. After the transaction completes, the Meta-governance working group will craft the Hedgey vesting contracts following the above allocation table.
---
# [EP 5.12] [Executable] Roles Modifier V2 Migration & Updates to Endowment Permissions
::authors
| **Status** | Passed |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-12-roles-modifier-v2-migration-updates-to-endowment-permissions/19173) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/107992041043258996427224563090014372885335179099580585497266204203463156791290) |
## Abstract
This proposal aims to roll out an updated version of the Zodiac Roles Modifier module. The new version improves usability and transparency of treasury management operations. Upon approval, the Roles Modifier v2 module will be activated.
Furthermore, this proposal requests authorization from the DAO to revise the permissions policy. A notable change includes enabling swapping actions on CoW Swap while the other permissions primarily focus on eliminating obsolete actions and protocols, and refining parameters within the existing permissions.
# Roles v2 Migration
## Motivation
As [previously stated](https://discuss.ens.domains/t/endowment-initiation/15952#a-list-of-zodiac-roles-modifier-permissions-for-the-manager-role-9), the Zodiac Roles Modifier facilitates karpatkey’s proxy management of the Endowment by ensuring that only pre-approved transactions, defined by the permissions policy voted on by the DAO, can be executed. In collaboration with karpatkey, the Gnosis Guild team has significantly upgraded the Zodiac Roles Modifier module and the Zodiac Roles app. These enhancements have resulted in a more powerful and robust on-chain permissions infrastructure with the following improvements:
- **Introduction of Allowances**: Implementation of spending limits within permissions.
- **Enhanced Call Data Scoping Toolset**: This toolset considerably broadens the range of functions that can have permissions set, increasing flexibility.
- **Advanced Logical Conditions**: Allows for the creation of complex permissions structures to accommodate sophisticated operational needs.
- **Compatibility with** [**DeFi Kit**](https://kit.karpatkey.com/): This feature integrates with karpatkey’s permissions policy building module, facilitating the straightforward crafting of protocol actions.
- **Improved Visualisation and User Interface**: the new [Zodiac Roles app UI 1](https://roles.gnosisguild.org/) not only displays permissions clearly but also provides a user-friendly interface to compare changes in each policy update, enhancing transparency and simplifying audits.
For more detailed information, please refer to the following resources:
- **Documentation**: [The Zodiac Roles Modifier](https://docs.roles.gnosisguild.org/)\
- **Articles**: [Evolving Smart Accounts with Onchain Permissions](https://gnosisguild.mirror.xyz/oQcy_c62huwNkFS0cMIxXwQzrfG0ESQax8EBc_tWwwk), [Permissions as code](https://engineering.gnosisguild.org/posts/permissions-as-code)
## Contract audits
The Zodiac Roles Modifier v2 contract has been rigorously audited by G0 Group, the internal auditing team of Gnosis DAO, and by [Omniscia](https://omniscia.io/). The detailed audit reports are available for review [here 1](https://github.com/gnosisguild/zodiac-modifier-roles/tree/main/packages/evm/docs).
# Changes to Permissions policy
This proposal outlines the following modifications to the permissions policy:
1. **Token Arrays for Swapping**:
- Considering the tokens involved in the existing permissions, we have updated the token arrays to ensure they can be seamlessly swapped across the various whitelisted protocols and aggregators.
- **Token IN Allowlist**: \\[CRV, DAI, USDT, BAL, AURA, CVX, ETHx, COMP, rETH, SWISE, wstETH, LDO, WETH, ankrETH, USDC, stETH].
- **Token OUT Allowlist**: \\[DAI, USDT, USDC, rETH, wstETH, WETH, stETH].
- 4. The above arrays are to be utilised for swapping on CoW Swap, with equivalent lists replicated for Uniswap v3 and Balancer.
2. **Introduction of CoW Swap** ([diff 4](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#right-0x23da9ade38e4477b23770ded512fd37b12381fab)):
- Addition of a **CoW Swap Order signer** to enable gas-minimised and MEV-protected swaps. This includes an extensive set of aggregated exchange routes, improving the efficiency and effectiveness of required swaps.
- Tokens will be swapped on CoW Swap according to the token IN/OUT allowlists mentioned above.
3. **Deprecations and Removals**:
- **Uniswap v2 Swaps** ([diff 1](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#left-0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45-0x472b43f3)): Removed due to insufficient liquidity in V2 pools.
- **Stakewise v2**: Deprecated functions related to deposit ([diff 1](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#left-0xc874b064f465bdd6411d45734b56fac750cda29a)) and claim ([diff 1](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#left-0xa3f21010e8b9a3930996c8849df38f9ca3647c20)) functions in light of the recent launch of Stakewise v3. Consequently, permitted actions related to Stakewise v2’s sETH2-ETH Uniswap pool are also removed ([diff](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#left-0xc36442b4a4522e871399cd717abdd847ab11fe88)).
- **Compound v2** ([diff](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#left-0x39aa39c021dfbae8fac545936693ac917d5e7563)): Discontinued all actions targeting v2 contracts and v2 cTokens (cUSDC and cDAI) due to the ongoing transition of the protocol to its v3.
- **Revocation of Existing/Obsolete Allowances:** All existing and outdated allowances previously set by the Endowment are revoked (set to zero). The ability to call the corresponding approve functions is included in the newly proposed policy. Accordingly, the payload contains a bundle of transactions to revoke these allowances.
4. **Updates**:
- **Uniswap v3** ([diff 1](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#right-0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45-0x04e45aaf)) **and Balancer** ([diff](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#right-0xba12222222228d8ba445958a75a0704d566bf2c8)): Adjusted to allow the mentioned token IN/OUT allowlists.
- **Curve Pools**: Addition of stETH-ng (factory) pool ([diff](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/rMNui3Yp9LlBScVLFjY9cWkk4QGYoIVkkYxURs16ys?annotations=false#right-0x21e27a5e5513d6e65c4f830167390997aa84843a)) and removal of cUSDC + cDAI pool ([diff 1](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/rMNui3Yp9LlBScVLFjY9cWkk4QGYoIVkkYxURs16ys?annotations=false#left-0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56); Compound v2 tokens).
- **Curve ZAP Deposit Contract** ([diff](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/rMNui3Yp9LlBScVLFjY9cWkk4QGYoIVkkYxURs16ys?annotations=false#right-0x56c526b0159a258887e0d79ec3a80dfb940d0cd7)): Introduced to allow depositing and staking of LP tokens in a single step.
- **Convex Staking** ([diff](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#right-0x6b27d7bc63f1999d14ff9ba900069ee516669ee8)): Added the CVX/stETH Rewards contract.
- **Lido Withdrawals** ([diff](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#right-0x889edc2edab5f40e902b864ad4d7ade8e412f9b1)): Enhanced to include new withdrawal methods using permits for both wstETH and stETH; methods include `requestWithdrawalsWstETHWithPermit` and `requestWithdrawalsWithPermit`.
- **Spark Rewards Claim** ([diff 1](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false#right-0x4370d3b6c9588e02ce9d22e684387859c7ff5b34)): Added functionality to claim wstETH rewards in Spark.
# Audit Considerations
We highly value the community’s involvement in reviewing and providing feedback on this proposal. We encourage members with the necessary technical expertise to examine the content carefully (including this [payload 1](https://gist.github.com/santinomics/773117b729152f31a121045f4ba26b5b)) and share their insights with us. For effective testing of the permissions policy configuration, we have utilised a [Testing Avatar Safe 1](https://app.safe.global/balances?safe=eth:0xC01318baB7ee1f5ba734172bF7718b5DC6Ec90E1). This safe mirrors the current state of the [permissions policy v4 1](https://docs.google.com/document/d/1Ker_TkBJV0xmQ9Li9HB-vtdlpx1vEeVEQwpIH6WoK0o) granted by the Endowment to the Manager SAFE operated by karpatkey. The enhancements in the Zodiac Roles app interface now allow for a clear visualisation of all existing permissions, accessible [here 2](https://roles.gnosisguild.org/eth:0xbd1099dfd3c11b65fb4bb19a350da2f5b61efb0d/roles/ENS-MANAGER-V1). The updated interface also simplifies the process of identifying [changes](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/C0zUV4NtNErufEpbpNc2S55F2oiAGw8aKE2jYwWjLg?annotations=false) by displaying the current permissions policy on the left side and the newly proposed policy on the right side. To further aid in the adoption and understanding of these tools for audit purposes, we have detailed the proposed changes in [version 5 documentation](https://docs.google.com/document/d/1KU4a7s-AxAAAPJxd8vexn7kCl8hsr3-c7VIDfEPHbKc/edit), following our standard Policy Update Request (PUR) format.
# Additional Considerations
## Roles Modifier v1 and Enabling of v2
The existing Roles Modifier v1 module will remain active to ensure a smooth transition and prevent any unexpected disruptions in operational execution. Ownership of the deployed [Roles Modifier v2 module 1](https://etherscan.io/address/0x703806E61847984346d2D7DDd853049627e50A40), equipped with the new proposed permissions policy, has been [transferred 1](https://etherscan.io/tx/0x78c69c7ad0c5f430d97ec5c3bac5cb649d831a756b3d4c5b09b45152427ae8f4) to the Endowment’s Avatar Safe. These permissions are displayed [here 4](https://roles.gnosisguild.org/eth:0x703806e61847984346d2d7ddd853049627e50a40/roles/MANAGER) and match those shown on the comparison interface [here 1](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/rMNui3Yp9LlBScVLFjY9cWkk4QGYoIVkkYxURs16ys?annotations=false). The payload will only activate this module, marking the first phase of the migration process. A subsequent policy update proposal will seek to disable the v1 module.
## Policy Visualisation in Terms of DeFi Kit Actions
The “show annotations” button, located at the top-right [here 1](https://roles.gnosisguild.org/eth:0xBd1099dFD3c11b65FB4BB19A350da2f5B61Efb0d/roles/ENS-MANAGER-V1/diff/rMNui3Yp9LlBScVLFjY9cWkk4QGYoIVkkYxURs16ys?annotations=false), provides a visualisation of the proposed permissions policy expressed through the DeFi Kit Protocol Actions. This feature offers a more abstract and simplified description of the policy, enhancing understanding and accessibility.
---
# [EP5.1] [Executable] Upgrade DNSSEC support
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep5-1-executable-upgrade-dnssec-support/18535) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/4208408830555077285685632645423534041634535116286721240943655761928631543220) |
## Abstract
The ENS labs team has been working on a new version of the DNSSEC oracle and the DNS registrar that, combined with wildcard resolution (ENSIP 10) and CCIP-Read, allow for 'gasless DNSSEC' - enabling the use of DNS names inside ENS with no onchain transactions required. This proposal replaces the existing DNSSEC registrar with the new one.
Existing DNS names will continue to function as before, and names can still be imported using the 'legacy' method. The new registrar also allows configuring a name by setting a TXT record on \_ens.name.tld, containing the address of a special resolver contract to use, followed by any resolver-specific data. Resolvers designed for the purpose can be configured to parse this extra data, making configuration entirely offchain a possibility.
Alongside the new registrar and oracle contracts, a simple resolver is provided that reads the Ethereum address to resolve a name to from the extra data.
Post-execution, ENS Labs will run a process to upgrade all current DNS TLDs to use the new registrar. TLDs will only function with the new registrar once this (permissionless) transaction is sent for the TLD.
## Specification
Call `setController` on the Root contract at `root.ens.eth`, passing in the address of the new DNS registrar (`0xb32cb5677a7c971689228ec835800432b339ba2b`).
## Transactions
| Address | Value | Function | Argument | Value |
| ------------------------------------------ | ----- | ------------- | -------- | ------------------------------------------ |
| 0xaB528d626EC275E3faD363fF1393A41F581c5897 | 0 ETH | setController | | 0xB32cB5677a7C971689228EC835800432B339bA2B |
| | | | | true |
---
# [5.8] [Social] ENS Steward Vesting Proposal
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/5-8-social-ens-steward-vesting-proposal/19059) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x1f328fd1fda5f3cabfdace3e521403def7ad41b0b0582e27334c135cd23c511d) |
## Abstract
Following discussion in the Metagov funding request thread and feedback on the Temp Check Proposal, we have put together this amendment proposal which looks to add the requirement of vesting to ENS distributions to stewards for the current term.
Under the current proposal, the MetaGov Working Group will be distributed liquid ENS tokens which will then be distributed individually to stewards. As discussed on the forum and on Meta-Gov calls, these tokens should be vested in order to ensure long-term alignment of stewards with the DAO, whilst ensuring that they are able to use the tokens to particiapte in governance.
This proposal is only in reference to adding vesting to current term steward allocations. With more discussion and input needed for any other changes to steward compensation going forward.
## Specification
All ENS disbursements to stewards will be vested on a linear 24 month schedule from the time of their appointment. In line with the 12 month term that each steward serves plus an additional 12 months of vesting to encourage longterm DAO alignment.
This vesting will be implemented by using Hedgey which allows stewards to access the full voting power of their allocated tokens up front, whilst ensuring that the monetary value of these tokens can only be accessed based on the vesting schedule.
All ENS marked for steward compensation will be transferred via executable proposal to the metagov WG multisig. Term 5 stewards were appointed at the beginning of Q1 2024, and as such are already 4 and a half months into their term.
For the current term's stewards, ENS vesting will be scaled retroactively based on the time since their appointment. ~37.5% of ENS will be distributed to stewards directly from the multisig, whilst the remaining ENS will then be deposited into each steward's vesting contract. These contracts will be set up by Hedgey.
## Vote
- For - Apply vesting to the current term
- Against - Do not apply vesting to the current term in line with the 2023/2024 guidance
- Abstain
This vote adjusts the structure of ENS token distributions to stewards, and if passed, the Meta-gov working group will implement this vesting schedule (and associated tooling) for all ENS token distributions to stewards.
---
# [EP 5.25] [Executable] Collective Working Group Funding Request (Oct 2024) - Resubmission
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-25-executable-collective-working-group-funding-request-oct-2024-resubmission/19847) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/14573906698181916980991958251655570598275649222336388998984945658874299918898) |
## Description
This is a resubmission of [EP 5.24], with no changes to the funding amounts or technical specifications.
## Abstract
This proposal executes all three Working Group funding requests for the October 2024 funding window as passed in [EP 5.17.1](https://discuss.ens.domains/t/5-17-1-social-funding-request-ens-meta-governance-working-group-term-5-oct-window/19677), [EP 5.17.2](https://discuss.ens.domains/t/5-17-2-social-funding-request-ens-ecosystem-working-group/19678), and [EP 5.17.3](https://discuss.ens.domains/t/5-17-3-social-funding-request-ens-public-goods-working-group/19679). For more detail, view the ENS Governance docs at https://basics.ensdao.org/funding-requests
## Proposal Components
---
### 1) [Meta-governance Funding Request [EP 5.17.1]](https://discuss.ens.domains/t/5-17-1-social-funding-request-ens-meta-governance-working-group-term-5-oct-window/19677)
The Meta-governance Working Group requests funding to fulfill anticipated budgetary needs through the next formal funding window in April 2025.
| Destination | USDC | ETH | $ENS |
| :---------------------------------------------------------------------------------------------------- | :-----: | :-: | :--: |
| [ENS Meta-Gov Main Multisig](https://etherscan.io/address/0x91c32893216dE3eA0a55ABb9851f581d4503d39b) | 254,000 | 0 | 0 |
This amount will cover all expected expenses while maintaining a 100,000 USDC prudent reserve to ensure continuity if future funding is delayed.
---
### 2) [Ecosystem Funding Request [EP 5.17.2]](https://discuss.ens.domains/t/5-17-2-social-funding-request-ens-ecosystem-working-group/19678)
The ENS Ecosystem Working Group requests funding to support operations through April 2025. This is the only funding request of Term 5. The working group is responsible for growing and improving the ENS Ecosystem by funding builders and projects that are ENS-specific or ENS-centric.
| Destination | USDC | ETH | $ENS |
| :----------------------------------------------------------------------------------------------------- | :-----: | :-: | :--: |
| [ENS Ecosystem Main Multisig](https://etherscan.io/address/0x2686a8919df194aa7673244549e68d42c1685d03) | 836,000 | 0 | 0 |
---
### 3) [Public Goods Funding Request [EP 5.17.3]](https://discuss.ens.domains/t/5-17-3-social-funding-request-ens-public-goods-working-group/19679)
The ENS Public Goods Working Group requests funding to support operations through the next funding window in April 2025. The funds requested extend current needs through to next term to ensure that next season's stewards have available funding before the next funding window.
| Destination | USDC | ETH | $ENS |
| :---------------------------------------------------------------------------------------------------- | :-----: | :-: | :--: |
| [Public Goods Main Multisig](https://etherscan.io/address/0xcD42b4c4D102cc22864e3A1341Bb0529c17fD87d) | 226,000 | 0 | 0 |
---
## Specification
The following transfers are to be made from the DAO treasury:
1. Transfer 254,000 USDC to the Meta-governance safe:
- Address: `0x91c32893216dE3eA0a55ABb9851f581d4503d39b`
2. Transfer 836,000 USDC to the Ecosystem safe:
- Address: `0x2686A8919Df194aA7673244549E68D42C1685d03`
3. Transfer 226,000 USDC to the Public Goods safe:
- Address: `0xcD42b4c4D102cc22864e3A1341Bb0529c17fD87d`
#### Total transfer amount: 1,316,000 USDC
---
### Calldata:
**5.17.1 Tx to Metagov**
```
{
"target": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"value": 0,
"calldata": "0xa9059cbb00000000000000000000000091c32893216de3ea0a55abb9851f581d4503d39b0000000000000000000000000000000000000000000000000000003b23946c00"
}
```
**5.17.2 Tx to Ecosystem**
```
{
"target": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"value": 0,
"calldata": "0xa9059cbb0000000000000000000000002686a8919df194aa7673244549e68d42c1685d03000000000000000000000000000000000000000000000000000000c2a57ba800"
}
```
**5.17.3 Tx to Public Goods**
```
{
"target": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"value": 0,
"calldata": "0xa9059cbb000000000000000000000000cd42b4c4d102cc22864e3a1341bb0529c17fd87d000000000000000000000000000000000000000000000000000000349ea65400"
}
```
---
# [EP 5.23] [Executable] blockful's governance security bounty
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-23-executable-governance-security-bounty/19803) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/46071186312489687574960948336391811341595411932836110873328798657006776570015) |
## Summary
This proposal aims to compensate the blockful team for their work in identifying, analyzing, reporting and mitigating a severe vulnerability in ENS DAO's governance structure.
## Background
In March 2024, blockful uncovered a critical vulnerability that could have led to a [~$150M](https://dune.com/steakhouse/ens-steakhouse) theft and protocol capture. Their subsequent work led to the implementation of the Security Council,
significantly enhancing ENS DAO's resilience against attacks.
## Contribution Details
The team involved is a [different](https://discuss.ens.domains/t/blockful-service-provider-reports/19553#p-54163-other-contributions-not-related-to-service-provider-scope-14) squad than the one working on the scope of the [ENS service provider](https://discuss.ens.domains/t/blockful-service-provider-reports/19553). It was developed by 2 researchers,
1 smart contract engineer and 4 different auditors the team has worked with previously. Summing up to ~600 hours,
the scope includes:
- Comprehensive vulnerability assessment and risk analysis: **[Here](https://mirror.xyz/research.blockful.eth/-PfMduhpxdypPrutofr6099T4ROpsAmX0fPNbvDgR_k)** is our detailed security report.
- Data analysis of ENS governance metrics and study of past DAO attacker's behaviors.
- Design, development and deployment of the Security Council contract and multisig.
- The Security Council was thought with several key features to balance security and decentralization.
- Smart contract implementation and testing ([GitHub](https://github.com/blockful-io/security-council-ens))
- Governance proposal drafting and support [[1](https://snapshot.org/#/ens.eth/proposal/0xf3a4673fe04a3ecfed4a2f066f6ced1539a5466d61630428333360b843653c54), [2](https://snapshot.org/#/ens.eth/proposal/0xa0b1bfadf6853b5b0d59d3c4d73c434fc6389339887d05de805361372eb17c3a), [3](https://www.tally.xyz/gov/ens/proposal/42329103797433777309488042029679811802172320979541414683300183273376839219133)]
More details can be found on the links above for past proposals and the [report](https://mirror.xyz/research.blockful.eth/-PfMduhpxdypPrutofr6099T4ROpsAmX0fPNbvDgR_k).
## Compensation Rationale
As a team that is totally bootstrapped and never received any investment, this support us to keep it sustainable with the resources invested towards this initiative. The requested amount represents fair compensation for:
- The potential loss prevention of ~$150M, capture of the DAO and protocol. The attack is anything but theoretical and there are actually many groups of investors who specialize in "risk free value raiders". They have exerted the attack on other DAOs before. Currently there are [unknown whales](https://etherscan.io/address/0x245445940b317e509002eb682e03f4429184059d#tokentxns) buying ENS for +450 days and have ~2M ENS, showing how feasible the scenario is, more than the average quorum, in one wallet.
- A critical code bug bounty in [ENS is $250k USDC](https://immunefi.com/bug-bounty/ens/scope/#assets). Our work was much beyond identifying and disclosing.
- Significantly lower cost compared to standard rates charged by other security service providers in the DAO space,
which typically demand liquid compensation. An example is that Open Zeppelin (one of the most reputable players in security) [charges $4M/year at Compound](https://compound.finance/governance/proposals/76),
which recently [suffered](https://mirror.xyz/research.blockful.eth/v0GEP49oXP1gzMDlyP91-S4XIa8PIOd0vKq-6R8f54I) this type of attack.
- Months of dedicated work by the team involved (researchers, devs and auditors).
- The long-term value added to ENS through enhanced security.
- Our commitment to ENS's long-term success and continued contribution, as evidenced by the 2-year vesting schedule.
## Compensation Structure
- Total amount: 100k USDC + 15k vested ENS tokens
- Vesting period: 2 years
- Vesting start date: April 8 2024 (date of initial research disclosure)
- Vesting schedule: Linear vesting
- Will be sent to the meta-governance multisig transferred and vested to blockful.
## Benefits to ENS DAO
- Sets a positive precedent that **responsible vulnerability disclosure and correction are rewarded**,
encouraging future security contributions
- Preserves DAO treasury liquidity by using part of the bounty in ENS tokens instead of USDC or ETH
- Enhances governance security by increasing the number of engaged security-focused token holders
## Conclusion
By approving this compensation, ENS DAO acknowledges the critical importance of security research and proactive governance improvements. The vesting structure ensures ongoing commitment and aligns incentives for continued contribution to ENS's security and stability.
---
# [EP 5.15] [Social] Adding ProposalBond to ENS Governor to make proposing more accessible
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/temp-check-social-adding-proposalbond-to-ens-governor-to-make-proposing-more-accessible/19539?u=estmcmxci) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xca27ce450e2a404b51d7ec7979639561dc5d8302949b3a29e4dfef2a8993f018) |
## Abstract
The proposal threshold for propose new executable ENS proposals is high, and rightly so. ENS is one of the most popular DAOs and community in the Web3 community and keeping the quality bar of proposals to the highest standard is very important. However, ENS also has the treasury and the desire to expand the community and make proposing easier and more accessible to enable more builders to come and build in ENS.
[Agora](https://agora.xyz/) proposes adding the functionality of the ProposalBond to the ENS DAO Governor that would allow a proposer to propose with a lower threshold, and then the community could vote [For, Against, Against with penalty, Abstain]. If the weight of `Against with penalty + Against > For`, then the proposer does not get their bond back and the proposal does not pass.
To clarify further: a bond is withheld only if a proposal is rejected (the sum of rejections are bigger than the approvals) AND if the "Against with penalty" is bigger than "Against".
This way we align incentives to create good proposals.
## Specification
A discussion in the DAO Meta-Gov working group titled: [Seeking Feedback: ENS Governor Upgrade to make proposing more accessible](https://discuss.ens.domains/t/seeking-feedback-ens-governor-upgrade-to-make-proposing-more-accessible/19296), Agora proposed the following PR on the ENS Governor: [Proposal Bond Pull Request](https://github.com/voteagora/ens-governance-contracts/pull/1) which outlines the code needed to make this change happen.
Since the time of that PR and during the follow up discussions, the community has asked for the following additions:
- Ensure that the ProposalBond work proposed by Agora works with the new Veto rules and security council. This covers the case of a proposal being vetoed from within the timelock therefore making sure we have the code to handle that case. The default case here being that the bond would not be returned.
- Work with OpenZeppelin to see if we can bring this functionality into OZ Governance Core
- Collaborate with ScopeLift to bring in a few sensible defaults to the voting delay period which is currently set to only one block. This leaves the DAO open to attack and the MetaGov working group is agreed on a 24 hour delay to ensure the DAO time to protect itself if needed.
Agora is committed to building public goods and is already working closely with OpenZeppelin to bring innovations of Agora's Governor into OZ Governance Core.
Given that the proposal threshold of this new functionality will be the most important piece, there is a general consensus in the discussion group that `1,000 ENS` is the right initial value. This parameter can later be set governance and moved up and down as we see fit.
## Voting
We are putting this to a simple, for/again/abstain vote
## Next Steps
Should the vote pass, Agora will be responsible for:
- closing out the implementation
- working with ScopeLift to review the code and add in the Voting Delay logic
- securing and organizing the audits
- making any changes raised by the auditors
- getting the code ready for a governor upgrade
### Auditing
Given the potential impact of the change, we are going to work with the ENS MetaGov stewards to do 2 audits on this code: one chosen by Agora, and the other chosen by the stewards.
Agora recommends using [OpenZeppelin](https://www.openzeppelin.com/security-audits), a reputable and top quality auditor in the governance space.
Agora recommends that the MetaGov stewards pick from: [CodeArena](https://code4rena.com/), [Trail of Bits](https://www.trailofbits.com/services/software-assurance/blockchain/), [Spearbit](https://spearbit.com/) or [Trust Security](https://www.trust-security.xyz/services). Each of these are quality auditing firms with a proven track record of working with governance contracts.
Both of these audits will be funded by ENS, and code changes will be implemented by Agora as part of their service contract with ENS, at no additional charge.
Results and changes will be posted for everyone to see
## Success Criteria
For this social proposal to pass, the following quorum and voting requirements must be met:
**Quorum**: The proposal must receive a minimum of 1% of the total supply of $ENS (1 million votes) in the form of "Yes" and "Abstain" votes combined. "No" votes do not count towards quorum.
**Approval**: Once the quorum is reached, the proposal requires a simple majority (>50%) of "Yes" votes among the "Yes" and "No" votes to pass. "Abstain" votes do not count towards the approval calculation.
---
# [EP2.2.1] [Executable] Q3 & Q4 2022 Meta-Governance WG Budget
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/13756) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0x46c7294aca8d70ae8213e8e8c6915697c7be1aab731fbb7e534276f7eb0ef2b9) |
_Note: This was previously numbered EP16.1._
## Summary
The Meta-Governance Working Group is requesting funding of 632k USD equivalent for the Q3/Q4 of 2022. The budget has three categories. All figures are presented in USD equivalent.

## Multisigs / Subgroups: $371k USD Value
Funding in this category is related to the two Meta-Governance subgroup multi-sigs. These have been reviewed and approved by the Meta-Gov Stewards.
#### Budget

#### Description

## Compensation: $190k USD Value
Funding in this category relates to compensation for DAO stewards and secretary.
#### Budget

#### Description
The Working Group Rules passed in [EP12](https://discuss.ens.domains/t/ep12-social-working-group-rules/12953#specification-3) state compensation is permitted for stewards and secretary as set out in Rules 11.1 and 9.4 respectively.
## Unallocated: $71k USD Value
The funds in this category are reserved for unforeseen grants and unexpected expenses for the term. Stewards will distribute unallocated funds on a discretionary basis.

---
# [6.6.1] [Social] April Funding Request - ENS Meta-Governance Working Group Term 6
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/20536) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x2c56d8776c8be3a824d010f94f00108716acaa31ede92cd03692e779af8a051d) |
## Abstract
The Meta-Governance Working Group is responsible for providing governance oversight and supporting the management and operation of working groups through DAO tooling and governance initiatives as well as treasury management for the DAO.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)). If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
This specification is the amount requested from the DAO treasury to the Metagov Multisig to fulfill anticipated budgetary needs through the next formal funding window in October 2025.
| | USDC | ETH | $ENS |
| -------------------------- | ---- | --- | ---- |
| ENS Meta-Gov Main Multisig | 589k | 0 | 100k |
This amount will cover all expected expenses outlined below while leaving a prudent reserve to ensure continuity if future funding is delayed.
## Description
### Current Metagov Wallet Balances
| | USDC | ETH | $ENS |
| ----------------------------------------------------------------------------------------- | ----- | ---- | ---- |
| ENS Meta-Gov Main Multisig | 84.3k | 83.6 | 4.5k |
| \*Up to date balance information can be found at [enswallets.xyz](https://enswallets.xyz) | | | |
## Expenditures
Meta-Gov sets aside funds to ensure coverage for mission-critical initiatives. While we strive to estimate term expenditures accurately, the final spending depends on pending initiatives.
### Expected Expenses through October 2025
| | USDC | ETH | $ENS |
| -------------------------------- | -------- | ----- | -------- |
| Steward + Secretary Compensation | 294k | - | - |
| Governance | - | - | 100k |
| Contract Audits | 150k | - | - |
| DAO Tooling | 150k | - | - |
| Discretionary | 45k | - | - |
| **Total Balance** | **639k** | **-** | **100k** |
### Description of Initiatives/Pods
**Steward + Secretary Compensation**: Working Group Steward, Scribe, and Secretary compensation [as required by the steward working group rules](https://snapshot.box/#/s:ens.eth/proposal/0x88de13f2f088390262d1d5e7db9ddff5a74d0b878fedf590a0448c32692078ba).
**Governance**: Fee reimbursements and future initiatives related to reducing friction in the governance process. This includes 70k $ENS to be distributed to DAO contributor, Stewards, and Service Provider roles. In Term 6 this amount was increased to allow for launch of programs to reward delegators and delegates.
**Contract Audits**: Meta-governance maintains a balance to be used for contract audits. These audits are performed independently on contracts that are to be included in executable proposals if those contracts impact or affect any ENS protocol or ENS DAO contracts or processes.
**DAO Tooling**: Funding research and/or develpoment interfaces and dashboards to improve the governance process and increase transparency across the DAO.
**Discretionary**: Funds distributed at the discretion of stewards towards new initiatives + governance experiments.
## Conclusion
This funding request will allow the ENS Meta-Governance Working Group to continue its essential work in providing governance oversight, supporting the management and operation of working groups, and ensuring effective treasury management for the DAO. The requested funds will enable us to maintain our ongoing initiatives and develop new tools to enhance the governance process. We are grateful for the community's ongoing support and engagement, which is crucial to the success of the ENS DAO. The Meta-Governance Working Group remains committed to serving the ENS community and driving the long-term growth and sustainability of the ecosystem.
---
# [EP0.2] [Executable] Retrospective airdrop for accounts that owned another account's primary ENS name
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/6755) |
| **Social Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0xcf77c74696cab1d939936ae8684c0007297bed641f60896ad186354f036d725f) |
| **Onchain Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/90476529665364161211265365238121921179703522228680648046371476645353679539653) |
_Note: This was previously numbered EP2._
## Summary
Send 213,049 ENS tokens to a new airdrop contract for users who did not receive the 2x multiplier despite owning a name that was used as a primary ENS name.
## Abstract
One of the criteria used for the ENS airdrop was whether the account had a primary ENS name set. Those accounts that did got a 2x multiplier on their airdrop amount. The intention was to use this as a measure of involvement in the ENS ecosystem; those accounts that set primary ENS names are typically using their names in supported applications, while those that don't are less likely to be doing so.
The way this was implemented was to award the multiplier to all accounts that had interacted with the reverse registrar. This had the unintended side effect that if a user owned an ENS name on account A, and configured it to resolve to account B, which used it as its primary name, account A would get tokens, and account B would get the multiplier. As B doesn't own any names, the multiplier has no effect and the end result is that some users got fewer tokens than they would have if they had used the same account for everything.
This proposal aims to correct this, by introducing a new criteria for determining who to award the multiplier to, and creating a new airdrop contract for all accounts that qualify for the new criteria but not the old one.
In plain english terms, the new criteria is that the account must have at some point in time owned a name that was used as the primary ENS name for an account.
More formally, for each account `a`, the account is assigned the multiplier if there exists a name `n` and a time before 2021-11-01 00:00:00 UTC, where all of the following are true:
1. `a` is the registrant of `n`.
2. `n` has a resolver, `r`, set on the ENS registry.
3. `r` has an `addr` record, `a'`, set for `n`, and has emitted an `AddrChanged` event to record this fact.
4. The reverse record for `a'` has a resolver, `r'`, set on the ENS registry.
5. `r'` has a `name` record, `n'`, set for the reverse record of `a`. `r'` is either the default reverse resolver, or has emitted a `NameChanged` event for `n'` and `a'`.
6. `n == n'`.
This logic is implemented by [this series of BigQuery queries](https://gist.github.com/Arachnid/667178e854945abaecb6dfd3b6c0c279/106d9bc156988cf96786c71f6448f13fb11599fc), and shows that 1,969 accounts meet these criteria but did not qualify for the multiplier under the original criteria. The sum of the tokens these accounts would be entitled to comes to \~213,049 ENS tokens. A list of affected accounts and balances is [here](https://gist.github.com/Arachnid/e8b1a18fc19818fb00f51fbb8d90e429).
Further, a number of users have accidentally transferred their ENS tokens to the token contract, totalling 6,246 contracts across 49 transfers. These tokens should be returned to their previous owners. This proposal, if executed, will transfer 219,295 ENS tokens to [a new merkle airdrop contract ](https://github.com/ensdomains/governance/pull/9)allowing affected users to claim them.
## Specification
1. Request that True Names Limited write and deploy a contract that allows claiming of tokens via Merkle Proofs using the same methodology as was used for the airdrop.
2. Request that True Names Limited make changes to the claim.ens.domains site to support claiming this additional airdrop for qualifying accounts.
3. Authorise the contract deployed in (1) to spend 219295650978169915391391 base ENS tokens from the ENS DAO account.
## Code
```javascript
const ethers = require('ethers')
const abi = [
'function approve(address _spender, uint256 _value) public returns (bool success)',
]
const token = new ethers.Contract(
'0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72',
abi
)
const airdropAddress = 'TBD'
const tx = await token.populateTransaction.approve(
airdropAddress,
'213049736662531485206636'
)
console.log([token.address])
console.log([0])
console.log([tx.data])
```
---
# [EP1.4] [Executable] Reimburse True Names for expenses and tax obligations incurred for the DAO
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/10053) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/82659277767818009782194204088226418907972756681918239480374274857360772298879) |
_Note: This was previously numbered EP8._
## Abstract
Since ENS started allowing registrations using the annual-fee model, revenue from this has accrued to the ENS root multisig, which is controlled by seven individuals drawn from the Ethereum community. In order to shield them from individual tax liability, True Names Limited, the development company responsible for ENS development, historically identified itself as the beneficial owner of these funds, which obliged True Names to pay tax on any income to the multisig.
In past years True Names has covered this tax bill from its own reserves - primarily out of funds that were collected during the Short Name auction - but in 2021 revenue rose to a level that meant that was no longer sustainable. Accordingly, True Names requested funds from the multisig to cover the anticipated tax, and the multisig agreed.
The calculation used to determine the tax owing used the actual income to October 20th, plus a 1/12th buffer to cover the anticipated income between the launch of the DAO and its potential request for control of the funds from the keyholders. This total came to $2,163,921 USDC.
However, this failed to take into account the enormous uptick in interest that the announcement of the DAO produced, and so falls significantly short of True Names' actual tax obligations for FY 2021. This proposal requests that the DAO sends True Names the remainder of the funds required to cover the multisig's income during the period that True Names was the beneficial owner.
Further, True Names has incurred the following expenses on behalf of the DAO in January 2022:
:::note
This screenshot could not be found
:::
We additionally request the DAO reimburse True Names for these expenses in the total of $48,637.
### Revenue
Revenue to the multisig came exclusively from ENS name registrations and renewals, and can be calculated from onchain data using [this BigQuery query](https://gist.github.com/Arachnid/dfd374886a3e6b0a0eb17b26703d776a), producing the following results:
| Month | ETH | USD |
| --------- | ------------- | --------------- |
| Jan 2021 | 411.6875 | 498484.22 |
| Feb 2021 | 383.5613 | 643985.05 |
| Mar 2021 | 453.5619 | 776834.28 |
| Apr 2021 | 429.0654 | 955345.36 |
| May 2021 | 243.0624 | 740165.91 |
| Jun 2021 | 422.2419 | 993899.95 |
| Jul 2021 | 384.6863 | 811202.23 |
| Aug 2021 | 849.9890 | 2563121.38 |
| Sep 2021 | 728.7825 | 2490699.18 |
| Oct 2021 | 500.3753 | 1863125.83 |
| Nov 2021 | 1699.4660 | 7643673.03 |
| **Total** | **6506.4793** | **19980536.41** |
### Tax
Singapore's company tax rate is 17%, meaning that the tax owing on $19,980,536 comes to $3,396,691. After deducting the $2,163,921 USDC already sent by the multisig, this leaves a shortfall of $1,232,770.
## Specification
We request that the DAO send $1,281,407 USDC to coldwallet.ens.eth.
---
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ens-dao-steward-compensation-structure-term-6/19739) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x88de13f2f088390262d1d5e7db9ddff5a74d0b878fedf590a0448c32692078ba) |
# [EP 5.18] [Social] ENS DAO Steward Compensation Structure - Term 6
::authors
## Background
The ENS DAO Working Group Rules place the responsibility for steward compensation on the Metagov working group.
**Working Group Rules:**
[[EP0.4] [Social] Proposal: Creation of Foundational Working Groups and Working Group Rules](https://docs.ens.domains/v/governance/governance-proposals/term-0/ep4-social-proposal-creation-of-foundational-working-groups-and-working-group-rules)
[[EP12][Social] Working Group Rules ](https://discuss.ens.domains/t/ep12-social-working-group-rules/12953)
With the passing of [this social proposal](https://snapshot.org/#/ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266) in Term 4, these two rules were added:
> **10.4.** The Meta-Governance working group are responsible for defining standards for fair compensation ('Compensation Guidelines').
> **10.5.** The Compensation Guidelines shall be defined prior to the Nomination Window for each term and can only take effect for the following term.
## Summary
In accordance with rule 10.5, This post outlines the proposed compensation structure for ENS DAO stewards during Term 6. The structure builds upon previous compensation models but includes several important changes. Notably, this compensation proposal will be put to a DAO vote for approval.
## Proposed Compensation Structure
### USDC Compensation
The total USDC compensation is the same as Term 5. The base compensation structure remains similar to previous terms, with some adjustments:
| Role | Quantity | Per Steward Per Month | Per Steward Per Term | Total per Term |
| ------------ | -------- | --------------------- | -------------------- | -------------- |
| Steward | 6 | $4,000 | $24,000 | $288,000 |
| Lead Steward | 3 | $5,500 | $33,000 | $198,000 |
| Secretary | 1 | $5,500 | $33,000 | $66,000 |
| Scribe | 1 | $3,000 | $18,000 | $36,000 |
| **Total** | | | | **$588,000** |
([previous structure linked here for reference](https://discuss.ens.domains/t/ens-dao-steward-compensation/18063))
Key changes:
- The discretionary amount has been removed.
- The $3,000 previously allocated to each WG for discretionary compensation has been split evenly among the stewards of that working group.
- Regular stewards will now receive $4,000 USDC per month (up from $3,000).
- Lead stewards will now receive $5,500 USDC per month (up from $4,500).
### $ENS Token Distribution
We are introducing a new structure for $ENS governance distribution to stewards:
1. Each steward will receive $ENS tokens equal in value to their total USDC compensation for the year.
2. The $ENS tokens will be distributed on July 1st, via 2-year linear vesting contracts.
3. The $ENS token price used to calculate the number of tokens each steward receives will be derived from the average daily price of the token between January 1st and July 1st of the term.
4. The vesting period will start from the beginning of the term (January 1st), meaning 6 months of vesting will have already occurred at the time of distribution.
Example calculation:
- A regular steward receiving $48,000 USDC for the year would also receive $48,000 worth of $ENS tokens.
- If the average $ENS price is $12 when calculated on July 1st, the steward would receive 4,000 $ENS tokens in a 2-year linear vesting contract.
- At the time of distribution (July 1st), 25% of the tokens (1,000 $ENS) would have already vested.
This structure ensures that stewards' token compensation aligns with their USDC compensation and incentivizes long-term commitment to the DAO.
### Success Criteria
For this social proposal to pass, the following quorum and voting requirements must be met:
1. **Quorum**: The proposal must receive a minimum of 1% of the total supply of $ENS (1 million votes) in the form of "Yes" and "Abstain" votes combined. "No" votes do not count towards quorum.
2. **Approval**: Once the quorum is reached, the proposal requires a simple majority (>50%) of "Yes" votes among the "Yes" and "No" votes to pass. "Abstain" votes do not count towards the approval calculation.
If approved, this structure will be implemented for Term 6. If not approved, the Metagovernance Working Group will reassess and propose an alternative structure based on feedback received.
---
# [6.6.2] [Social] April Funding Request - ENS Public Goods Working Group Term 6
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/20532) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x2c07add832383dc6900077406b4241a34dc4923ba209e2d07d1a4243a18fcdef) |
_This is an updated budget for this funding window to account for better treasury management at this time - a new funding request may be put forward in the October window which was not initially planned when scoping out this budget_
# Abstract
The Public Goods Working Group exists to fund initiatives that advance public goods funding within the wider ecosystem. We support builders, stewards, and community members working on public goods that are aligned with the values and goals of ENS.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules (EP 1.8). If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all Working Groups requesting funding.
# Specification
This specification is the amount requested from the DAO treasury to the Public Goods Multisig to fulfill anticipated budgetary needs through the next formal funding window in October 2025.
| | USDC | ETH | $ENS |
| -------------------- | ---- | --- | ---- |
| ENS PG Main Multisig | 356k | 0 | 0 |
This amount will cover all expected expenses outlined below while leaving a small reserve to ensure continuity if future funding is delayed.
# Description
Current Public Goods Wallet Balances as of April 2, 2025
| | USDC | ETH | $ENS |
| -------------------- | ---- | ---- | ---- |
| ENS PG Main Multisig | 139k | 24.6 | 200 |
Updated balance information can be found at enswallets.xyz
# Expenditures
The Public Goods Working Group allocates funds to support the public goods ecosystem through strategic grants, builder grants, and discretionary initiatives. While we aim to estimate expenditures accurately, actual spending may shift based on new opportunities or unforeseen needs.
Expected Expenses through October 2025
| | USDC | ETH | $ENS |
| -------------------------- | -------- | ----- | ----- |
| Strategic Grants | 300k | - | - |
| Builder Grants | 150k | - | - |
| Discretionary (10% buffer) | 45k | - | - |
| **Total Balance** | **495k** | **-** | **-** |
# Description of Initiatives
**1. Strategic Grants:** High-impact funding for initiatives aligned with the long-term vision of ENS and the broader public goods ecosystem. We are working on developing strategic initiatives focused on foundational infrastructure alongside our existing grants.
The pilot for Strategic Grants is our funding of the DRC. Since they are also receiving external matching funds for any contribution, our funding of 150,000 USDC equates to $300,000 in total funding dedicated to advancing decentralization advocacy and policy engagement.
Strategic Grants will be characterized by:
**Larger Funding Amounts:** Providing substantial support to initiatives that require more significant resources to succeed
**Internal Expertise Utilization:** Leveraging the expertise of our elected stewards rather than outsourcing key decision-making
**Focused Impact Areas:** Targeting underfunded yet critical areas such as developer tools, core dependencies, and infrastructure
**Measured Outcomes:** Establishing clear criteria for success and impact measurement from the outset
**2. Builder Grants:** Support for builders at all stages of their journey in building public goods. The platform will feature USDC payments as of next month so better accounting and higher amounts of funding can easily be distributed through the already tested and successful mechanism.
**3. Discretionary Buffer:** A 10% margin to provide flexibility for time-sensitive or emergent opportunities that align with the working group’s mandate. This may include hackathon sponsorship or event support.
**Conclusion**
This funding request will allow the Public Goods Working Group to continue its essential work in stewarding public goods funding. These resources will help us support a diverse set of builders, projects, and community-driven efforts, ensuring long-term sustainability and alignment with ENS values.
We are grateful for the ongoing support and engagement with our work.
---
# [EP 5.14] [Executable] Endowment permissions to karpatkey - Update #4
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-14-executable-endowment-permissions-to-karpatkey-update-4/19503) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/62537345451524095629071359388092434511638926463658570104856324869910407296726) |
# Abstract
This proposal aims to introduce new permissions for deploying Endowment funds, focusing on improved diversification and alignment with the evolving market landscape and liquidity. We are also introducing an independent audit report together with the Permissions Update; this will be the standard practice for Permissions Updates going forward.
# Motivation
Effective treasury management strategies must be adapted to market conditions and protocol updates; for existing Permissions, there might be migrations and introductions of new pools; for new Permissions, protocols and pools that were previously considered immature and unsuitable for the Endowment’s risk appetite may become viable options as they become more time- and battle-tested. This proposal seeks to request new permissions from the ENS DAO for karpatkey, enabling the introduction of new yield-generation strategies for the Endowment.
The new permissions have also been audited by [ThirdGuard](https://thirdguard.com/), an independent 3rd-party, to ensure the suggested changes have been thoroughly reviewed by a technically-competent, independent party.
# Specification
## New permissions implemented in this payload
1. Deposit osETH on Aave v3;
2. Stake (and unstake) ETH on Stakewise v3;
1. Through the [Genesis Vault](https://app.stakewise.io/vault/mainnet/0xac0f906e433d58fa868f936e8a43230473652885).
3. Mint (and burn) osETH on Stakewise v3;
1. Through the Genesis Vault
4. WETH/osETH pool on Balancer;
5. WETH/osETH pool on Aura Finance;
6. Swaps:
1. WETH <> osETH on Balancer
2. USDC <> osETH on Uniswap v3
3. USDC <> WETH <> osETH on CoW Swap
4. RPL <> WETH on Uniswap v3
5. RPL <> WETH on CoW Swap
7. Unsign order on Cow Protocol so that a pending order that has been submitted but not executed can be cancelled.
## Additional implementation details
1. The enableModule(address module) function is called to enable the modules, pointing it to the [Avatar address](https://app.safe.global/home?safe=eth:0x4F2083f5fBede34C2714aFfb3105539775f7FE64) (the Endowment).
2. The payload to be executed upon the successful approval of this proposal can be found [here](https://gist.github.com/JeronimoHoulin/55f50e86d1dc874e4e685d5e9b496a67). The proposed permissions policy can be visualised in the aforementioned [link](https://roles.gnosisguild.org/eth:0x703806E61847984346d2D7DDd853049627e50A40/roles/MANAGER/diff/C5Twf3khKv2Ny8PvzoARgHFKFFK8vIiNR7nDkrIM?annotations=false) for ease of review.
3. We have tested the payload to make sure all interactions mentioned on this proposal work as expected through our [Test Safe](https://app.safe.global/transactions/history?safe=eth:0xC01318baB7ee1f5ba734172bF7718b5DC6Ec90E1).
4. With the introduction of the [new Roles App Permissions Visualisation tool](https://roles.gnosisguild.org/eth:0x703806E61847984346d2D7DDd853049627e50A40/roles/MANAGER?annotations=false), manually updating the “Preset Permissions - ENS Endowment” [document](https://docs.google.com/document/d/1KU4a7s-AxAAAPJxd8vexn7kCl8hsr3-c7VIDfEPHbKc/edit?usp=sharing) is no longer necessary. The new tool provides an up-to-date and accurate method for exploring the current permissions granted to karpatkey by the ENS DAO.
# Auditing process
## Introduction of an independent audit report
We have received feedback in the previous proposal that independent, 3rd party code review would be helpful for the ENS community and delegates to make a more informed decision and to reduce delegate fatigue.
In our commitment to transparency and effort towards DAO efficiency, karpatkey decided to engage with independent, third-party firms / individuals for every contract upgrade starting with this proposal. [ThirdGuard](https://thirdguard.com/) has been engaged for this proposal's code review; ThirdGuard is a provider of on-chain risk monitoring solutions, and has been working with the Zodiac Roles Modifier since its inception (and its precursor, Scope Guard). Given their past experiences across Zodiac Roles Modifier, Solidity, and DeFi risk management, ThirdGuard was deemed to be a suitable candidate to fulfil the role of policy reviewer. Their approach to auditing the permissions can be found [here](https://www.loom.com/share/0b3cbcd6907a4455ab45ead4887c7f9a?sid=9bd0a2c7-d932-45c0-acad-8516201c56ea).
**The ThirdGuard audit for the permissions in this payload can be found [here](https://github.com/ThirdGuard/roles-policy-audits/blob/main/ENS/ens-policy-audit-v2-21st-Aug-2024.pdf).**
Audit report summary is as follows:
- No material findings were found.
- Policy changes requested were considered bona fide actions needed by the Manager to carry out their DeFi operations.
- 1 Informational Finding and 1 Warning were logged, and acknowledged by karpatkey. These findings do not post an immediate risk but are relevant to security best practices.
---
# [EP2.2.2] [Executable] Q3 & Q4 2022 Ecosystem WG Budget
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/10195) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/115615865324623814833258987703837575663427750121726187103053182962864855260310) |
_Note: This was previously numbered EP7.2._
## Summary
The Ecosystem WG is requesting funding to start the Q1/Q2 2022 term. The initial request is made up of three components:
1. Elected steward compensation: $27,500 in USDC/DAI
2. Bounties for the Bug Bounty program: $50,000 in USDC/DAI
3. Ecosystem WG operational budget: $50,000 in USDC/DAI, 6 ETH, and 6,500 $ENS
#### Bug Bounty
The ENS Bounty Program provides bounties for bugs. This program has been running since 2017 and historically has been funded by True Names Limited (TNL). With the creation of the DAO, the DAO is now able to manage and fund the bug bounty. Details of the program can be found [here](https://docs.ens.domains/bug-bounty-program). The $50,000 will be awarded to developers outside of TNL who find vulnerabilities in the ENS protocol. As well as funding for white hat coding in response to system vulnerabilities which arise.
#### Operational Budget
The operational budget supports ecosystem subgroups with the following allocations:
| Subgroup Name | Description | USDC/DAI | ETH | $ENS |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------- | ----- |
| Hackathons | Bounties, judging, and facilitating and managing ENS's hackathon participation. | $10,000 | | |
| Integrations | Bounties for ecosystem integration support | $10,000 | 500 | |
| 3rd Party Projects | Support projects that improve the ENS ecosystem, through proactive and retroactive grants. | $10,000 | 6 | 1,000 |
| Bug Bounty | Administer the bug bounty program | $5,000 | | |
| WG Unallocated Funds | Funds to be allocated to the above subgroups or facilitate the funding of new subgroups as the council of stewards deem necessary | $15,000 | 5,000 | |
| Total | $50,000 USDC/DAI | 6 ETH | 6,500 $ENS | |
#### Elected Steward Compensation
Provide compensation for the stewarding and the coordination efforts of active elected stewards.
| Description | Compensation | Months # | Stewards # | Total |
| ------------------------- | ------------ | ---------------- | ---------- | ------- |
| Base Compensation | $1,000/month | 5.5 | 3 | $16,500 |
| Supplement compensation\* | $2,000/month | 5.5 | N/A | $11,000 |
| Total | | $27,500 USDC/DAI | | |
\*Supplement compensation is allocated to the steward who supports coordination or who has greater involvement in DAO activities above what is expected of a steward. The steward council determines how the supplemental compensation is split between the stewards based on contributions of each steward.
It should be noted that the ecosystem working group has three elected stewards (@slobo.eth, @Ginge.eth, @bobjiang) and two appointed stewards (@nick.eth, @jefflau.eth). The appointed stewards are not compensated via this request.
---
# [EP 6.3] [Social] Renew Service Provider Budget
::authors
| **Status** | Passed, increase budget to $4.5M/year |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/20272) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x0cca1cf36731203e235b0e2de9041be3a16d9cdeadff6e15e1f1215c611e12ef) |
The Service Provider's streams were initiated almost exactly a year ago, and while it's approved for 18 months, it is supposed to be a program reevaluated yearly. Here is the proposed format for this year. This vote intends to be a DAO check on whether to renew the program and if so, under what budget.
## Vote
The vote will be a Ranked Choice Vote on either to approve or reject and budget. Options will be:
- Renew and increase budget to $4.5 Million per year
- Renew and maintain budget at $3.6 Million per year
- Renew but reduce budget to $2.7 Million per year
- Do not renew the program.
The budget is set per year. However the actual required budget will need to cover 19 months, because as detailed below, 1/3 of the streams will be budgeted for 2 years and we will add another 3 months of runway to make sure the program is not interrupted by eventual delays in the 2026 vote.
## What is the Service Provider Program
The ENS constitution states that \"Any income generated to the ENS treasury is to be used first of all to ensure the long-term viability of ENS, and to fund continuing development and improvement of the ENS system.\". The goal of this program is to create a more diverse and decentralized base of developers and companies involved in the improvement of the ENS system, by creating a guaranteed income stream to support their continued work. Streams last at least one year.
## Who can apply?
Any company offering services they believe will add value to the ENS ecosystem is eligible to apply. While this process has traditionally focused on developers, delegates now have discretion to determine what services qualify as beneficial.
### Eligibility Requirements:
- The applicant must be an existing company, over one year old, with an established team and reputation.
- The company or its team members must have serious prior experience with ENS, blockchain, or internet domain related projects.
- The company must secure endorsement of at least 50k delegated ENS tokens, either through public backing by a delegate or by demonstrating support via Snapshot.
- Neither the company nor its team members may reside in OFAC-sanctioned countries.
## Submission Process
**Candidates will be required to submit a proposal demonstrating their eligibility and outlining their plans. The proposal must include the following:**
1. **Past Achievements:**
A list of their accomplishments in ENS, the blockchain space, or domain systems.
2. **Scope of Work and Budgets:**
- A **basic scope**, which outlines the minimum yearly budget they would accept to perform their work, along with the specific goals and deliverables they aim to accomplish within that budget.
- Optionally, they may also provide an **extended scope**, which includes an increased budget and details the additional projects or goals they would pursue if granted this higher budget.
3. **Proposal Format:**
The submission must be provided both as a written document and as a short video (no longer than 5 minutes) where the team discusses the above points.
4. **Quarterly KPIs:**
The proposal must include a set of quarterly Key Performance Indicators (KPIs) to define what \"success\" looks like for their basic and extended scope. These may include specific targets, such as product releases, user acquisition numbers, or other measurable metrics that can be used to evaluate their progress against their own promises. Althought we encourage developers to explore new areas of research outside these given metrics, considerable thought should be given to these metrics, and every quarter you will be required to post an update on them. Failing targets in two consecutive targets will trigger a Service Assessement (see below) by Metagov stewards.
5. **Budget Guidelines:**
- All budgets must be submitted as integer multiples of $100k per year.
- The minimum budget request is $300k per year, and the maximum is $1.3M per year.
## What will be selection proccess look like?
1. **Eligibility & Voting**
- The MetaGov Working Group reviews candidates for eligibility.
- Eligible candidates are included in a ranked-choice DAO vote with the option **\"None Below\"**.
- A quorum of **1M ENS** votes is required; otherwise, the process halts and a new vote is conducted.
2. **Budget Streams**
- **Two-Year Stream**: 1/3 of the yearly budget for a two-year duration.
- **One-Year Stream**: Remaining 2/3 of the yearly budget for one year.
3. **Evaluation Process**
Projects are assessed in ranked order:
- If **\"None Below\"** is reached, evaluation stops.
- If the candidate has been part of the Service provider program for at least a year AND if the **extended budget** fits within the remaining two-year stream budget, assign to the **two-year stream** . Subtract the extended budget from the two-year stream budget.
- Assign to the **one-year stream** if:
1. The **extended budget** fits the one-year budget. Subtract its extended budget from the one-year stream.
2. OR if the **basic budget** fits the one-year budget, subtract the its basic budget from the one-year stream.
- If none of these conditions are met, the project is eliminated.
4. **Completion**
- Evaluation ends when all projects are assessed, the remaining budget on all streams reaches zero or **\"None Below\"** is selected.
## Relevant Dates
The Submission process will start as soon as this vote ends, the deadline for submission of proposals will be \*March 31st\*\* and the vote for the selection will start soon after that. Those with the two year stream will be guaranteed a stream until at least february 2027, while the others will be at least february 2026, when they will need to submit again.
## Resposibilities for selected service providers
### Brand name and association with ENS DAO
Selected providers will be rewarded with the streams, but will also carry the responsibility to represent ENS to the world. They will be granted usage of the ENS brand name (within guidelines).
The stream will be managed by the Metagovernance Stewards, but they are to enact representing the DAO's intent. They will have access to pause or move streams due to security concerns or upon the request of the service provider. They can only terminate a stream if there is a sucessful DAO vote requesting them to do so (which the DAO is free to do at any point for any reason).
### Open Source
All work done directly funded by this program must be Open Source and Freely Licensed (MIT). Service Providers are free to also have proprietary codebases but the works establised in their proposals and quarterly reports must also be available on github or other public repositories.
### Quarterly reports
Service Providers must provide a detailed written report on their accomplishments every quarter. They are also required to present at working group meetings when requested by stewards and to give at least one presentation at a conference each year (remote attendance is acceptable if necessary). These reports must include any metrics or KPIs promised for the reporting quarter, as well as any new metrics or KPIs proposed for future quarters. Additionally, the report must state the total amount received during the quarter. While Service Providers are encouraged to include financial spending details, this is at their discretion and not a mandatory requirement.
### Increased funding
Selected service providers may request specific, non-recurring grants at any time, following the usual governance process on the forum. However, to request an increase in their ongoing stream budget, they must meet the following conditions:
- They must have been a service provider for at least one year.
- At least six months must have passed since their most recent stream-related request, whether it was part of a service provider submission or another stream adjustment process, regardless of either it was sucessful or not.
Service Providers can terminate their own stream at any time without notice, thus liberating them from any further obligations towards the DAO. (Note: Terminating their stream does not exempt them from potential liability for any misconduct or unresolved issues.)
## Responsabilities for Metagov Working Group
The ENS DAO can terminate streams for any reason following the proper governance procedure and after a DAO vote. Additionally, Metagov or Ecosystem Stewards can and should trigger a Service Assessement if any of these conditions are met:
- A service provider fails to publish their quaterly report twice
- A service provider falls short of the KPIs on their proposal twice _in a row_.
- A service provider is unresponsive to attempts to contact them
- A service provider is believed to be actively participating in a behavior or project that is considered harmful to the ENS system or brand.
The Metagov Working will then appoint a person that will conduct an investigation on the claims and publish recommendations on how to solve the issue moving forward. Solutions might include (but are not limited to) a reconsideration of new KPIs, a demand of internal reorganization for the service provider, a renegotiation of terms or even the termination from the program. Once the report is published, a DAO wide vote will executed to decide on the proper outcome for the provider.
---
# [EP 5.19] [Social] Governance Distribution Pilot Program
::authors
| **Status** | Passed, approved 30K $ENS distribution program |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-19-social-governance-distribution-pilot-program/19759) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xfa54ff2b55f0495c96ec2d8645241bcff48ca6afe1f4925fb51f29c4667252df) |
## Objective:
This proposal establishes a pilot program to distribute ENS governance to eligible DAO contributors who have been selected by the DAO to receive a grant, bounty, or other payment for services. The program would also serve as a pilot, and if successful, could become a long-term initiative, aiming to cover all DAO USDC and ETH recipients, while setting a policy for future governance distribution.
The intent is to engage contributors in governance. We believe governance distribution increases access to DAO governance, enhances the utility of the token, decreases the amount of tokens purely used in markets, increases delegation and the safety of the DAO, and helps align developers and other contributors who bring value to the DAO.
## Context:
- **Previous Distributions**:
- [90k ENS tokens to stewards](https://discuss.ens.domains/t/5-8-social-ens-steward-vesting-proposal/19059)
- [80k ENS tokens to service providers and security council members](https://discuss.ens.domains/t/distribution-of-80k-ens-tokens-to-service-providers-and-security-council-members/19541)
- **Proposed Distribution**:
- This vote is to determine if the program is approved and to select its budget within a range between 0 and 90k ENS.
## Matching Program:
The distributions are calculated using a progressive ratio, meaning that the more you receive in dollar value from the DAO, the more you will receive in absolute values in ENS, but less relatively to the amount, following a quadratic formula. The process is as follows:
1. **Calculate all eligible transfers**: First, we list all transfers from the various DAO wallets into external wallets from January to September 2024. Data is collected from the [ENS Ledger app](https://ens-ledger.app).
2. **Exclude ineligible transactions**: Transactions to Stewards, ENS Labs, Service Providers, and other transactions that are considered refunds or payments for goods are not counted.
3. **Calculate the square root of the total value received**: The total weights are calculated to determine the percentage of the budget each recipient will receive. That calculation and further details can be seen in [this spreadsheet](https://docs.google.com/spreadsheets/d/1hnrBSPy5efi2W0b4hd4y2BnAmRIfI0M5y4NgYGjEPms/edit?usp=sharing).
4. **Decide the program's total budget**: The total budget will be determined by this vote.
## Voting Method:
The vote will use a ranked choice voting system with options of 0, 30k, 60k, and 90k ENS.
An Instant Run-Off will be calculated, and if the budget option of 0 wins, or if the total votes are less than 1 million, then the proposal will be considered rejected.
## Distribution Table:
The table below is from the [ENS ledger](https://ens-ledger.app), excluding Stewards, Service Providers, ENS Labs, refunds, and internal wallets. ETHGlobal Hackers are bounties given by ETHGlobal to hackathon winners. The final number of hackers is still being calculated, but here it is presented as a maximum of 100. If the final number is less than this, then more ENS will be distributed to other recipients, and if it is greater, it will be capped at 100. This proposal covers transactions from January 2024 to September 2024, and the final table may be subject to change upon legal or technical review.
| | $ Received jan-sep 2024 | 30k ENS | % | 60k ENS | % | 90k ENS | % |
| ---------------------------------------- | ----------------------- | ------- | ---- | ------- | ---- | ------- | ---- |
| ETHGlobal | $ 190,000.00 | 1,441 | 13% | 2,883 | 26% | 4,324 | 39% |
| Karpatkey | $ 187,149.13 | 1,430 | 13% | 2,861 | 26% | 4,291 | 39% |
| @UGWST_COM | $ 75,000.00 | 906 | 21% | 1,811 | 41% | 2,717 | 62% |
| Rotki | $ 53,973.38 | 768 | 24% | 1,536 | 48% | 2,305 | 73% |
| wslyvh.eth | $ 50,004.00 | 739 | 25% | 1,479 | 50% | 2,218 | 75% |
| gashawk.eth | $ 40,000.00 | 661 | 28% | 1,323 | 56% | 1,984 | 84% |
| buidlguidl.eth | $ 35,000.00 | 619 | 30% | 1,237 | 60% | 1,856 | 90% |
| borderlessafrica.eth | $ 30,000.00 | 573 | 32% | 1,145 | 65% | 1,718 | 97% |
| daemon.eth | $ 27,000.00 | 543 | 34% | 1,087 | 68% | 1,630 | 103% |
| Revoke.Cash | $ 25,000.00 | 523 | 36% | 1,046 | 71% | 1,568 | 107% |
| Onthis | $ 25,000.00 | 523 | 36% | 1,046 | 71% | 1,568 | 107% |
| ipns.eth | $ 25,000.00 | 523 | 36% | 1,046 | 71% | 1,568 | 107% |
| Fluidkey | $ 25,000.00 | 523 | 36% | 1,046 | 71% | 1,568 | 107% |
| Blockscout | $ 25,000.00 | 523 | 36% | 1,046 | 71% | 1,568 | 107% |
| beaconchain.eth | $ 25,000.00 | 523 | 36% | 1,046 | 71% | 1,568 | 107% |
| 1w3.eth | $ 25,000.00 | 523 | 36% | 1,046 | 71% | 1,568 | 107% |
| Firefly | $ 20,000.00 | 468 | 40% | 935 | 79% | 1,403 | 119% |
| EIP-7212 | $ 20,000.00 | 468 | 40% | 935 | 79% | 1,403 | 119% |
| Urbe Campus | $ 19,680.00 | 464 | 40% | 928 | 80% | 1,392 | 120% |
| ethdaily.eth | $ 14,797.60 | 402 | 46% | 804 | 92% | 1,207 | 139% |
| Discord Support | $ 13,000.00 | 377 | 49% | 754 | 99% | 1,131 | 148% |
| Dappnode | $ 12,500.00 | 370 | 50% | 739 | 101% | 1,109 | 151% |
| generalmagic.eth | $ 11,563.43 | 356 | 52% | 711 | 105% | 1,067 | 157% |
| Immunefi | $ 10,200.00 | 334 | 56% | 668 | 111% | 1,002 | 167% |
| Pugson | $ 10,000.00 | 331 | 56% | 661 | 112% | 992 | 169% |
| Juicebox | $ 10,000.00 | 331 | 56% | 661 | 112% | 992 | 169% |
| frolic.eth | $ 10,000.00 | 331 | 56% | 661 | 112% | 992 | 169% |
| ETHDenver | $ 10,000.00 | 331 | 56% | 661 | 112% | 992 | 169% |
| Drips | $ 10,000.00 | 331 | 56% | 661 | 112% | 992 | 169% |
| Lemma | $ 9,998.67 | 331 | 56% | 661 | 112% | 992 | 169% |
| Tally | $ 8,999.54 | 314 | 59% | 627 | 119% | 941 | 178% |
| pairwise.eth | $ 8,402.18 | 303 | 61% | 606 | 123% | 909 | 184% |
| @navad | $ 7,500.00 | 286 | 65% | 573 | 130% | 859 | 195% |
| apoorv.eth | $ 7,021.88 | 277 | 67% | 554 | 134% | 831 | 201% |
| leticiaferraz.eth | $ 6,949.32 | 276 | 67% | 551 | 135% | 827 | 202% |
| @Sagamore | $ 6,000.00 | 256 | 73% | 512 | 145% | 768 | 218% |
| Socket | $ 5,000.00 | 234 | 79% | 468 | 159% | 701 | 238% |
| Latin Hackathon | $ 5,000.00 | 234 | 79% | 468 | 159% | 701 | 238% |
| eth-mexico.eth | $ 5,000.00 | 234 | 79% | 468 | 159% | 701 | 238% |
| aynieducativo.eth | $ 5,000.00 | 234 | 79% | 468 | 159% | 701 | 238% |
| @adhd | $ 5,000.00 | 234 | 79% | 468 | 159% | 701 | 238% |
| ENS Fairy | $ 4,781.34 | 229 | 81% | 457 | 163% | 686 | 244% |
| glodollar.eth | $ 3,762.22 | 203 | 92% | 406 | 183% | 608 | 275% |
| weird3.eth | $ 3,000.00 | 181 | 103% | 362 | 205% | 543 | 308% |
| Event Support | $ 3,000.00 | 181 | 103% | 362 | 205% | 543 | 308% |
| daveytea.eth | $ 2,818.29 | 176 | 106% | 351 | 212% | 527 | 318% |
| @solidityhaxor | $ 2,500.00 | 165 | 112% | 331 | 225% | 496 | 337% |
| @haoce505 | $ 2,500.00 | 165 | 112% | 331 | 225% | 496 | 337% |
| @h4nt3rx | $ 2,500.00 | 165 | 112% | 331 | 225% | 496 | 337% |
| stephancill.eth | $ 2,367.89 | 161 | 116% | 322 | 231% | 483 | 347% |
| Scope.sh | $ 2,367.89 | 161 | 116% | 322 | 231% | 483 | 347% |
| Kiwi News | $ 2,367.89 | 161 | 116% | 322 | 231% | 483 | 347% |
| bloomnetwork.eth | $ 1,881.11 | 143 | 130% | 287 | 259% | 430 | 389% |
| modularcrypto.eth | $ 1,818.29 | 141 | 132% | 282 | 264% | 423 | 395% |
| aexek.eth | $ 1,750.00 | 138 | 134% | 277 | 269% | 415 | 403% |
| @austinoa012 | $ 1,000.00 | 105 | 178% | 209 | 356% | 314 | 533% |
| illuminated.eth | $ 940.56 | 101 | 183% | 203 | 367% | 304 | 550% |
| dhive.eth | $ 940.56 | 101 | 183% | 203 | 367% | 304 | 550% |
| pabl0cks.eth | $ 877.74 | 98 | 190% | 196 | 379% | 294 | 569% |
| iviangita.eth | $ 877.74 | 98 | 190% | 196 | 379% | 294 | 569% |
| easlabs.eth | $ 877.74 | 98 | 190% | 196 | 379% | 294 | 569% |
| 2118.eth | $ 877.74 | 98 | 190% | 196 | 379% | 294 | 569% |
| andrewpage.eth | $ 780.00 | 92 | 201% | 185 | 403% | 277 | 604% |
| ETHGlobal Hackers (max 100 participants) | $ 500.00 | 72 | 245% | 144 | 491% | 216 | 736% |
---
# [4.4.1] [Social] Funding Request: ENS Ecosystem Working Group
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/4-4-1-social-funding-request-ens-ecosystem-working-group/17995) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x12a2abca291496c7e990d099240b4c995099dc0fb85767e04f22b9496e953799) |
## Abstract
The ENS Ecosystem Working Group requests funding of 409,000 USDC to support operations until the March 2024 funding window. This is the only funding request of this term.
The ENS Ecosystem Working Group is responsible for growing and improving the ENS Ecosystem by funding builders and projects that are ENS-specific or ENS-centric.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)). If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
| | USDC | ETH | $ENS |
| --------------------------- | :-----: | :-: | :--: |
| ENS Ecosystem Main Multisig | 409,000 | -- | -- |
## Description
### ENS Ecosystem Multisig Balances - As of October 18, 2023
The ENS Ecosystem Working Group multisigs:
| Multisigs|USDC|ETH|
|:----|----:|----:|
|[Main Multisig](https://etherscan.io/address/0x2686a8919df194aa7673244549e68d42c1685d03)|215k | 157|
|[Builder Grants](https://etherscan.io/address/0x6a016548310076285668e2378df70bd545396b5a)|157k | 38|
|[Grants](https://etherscan.io/address/0xba0c461b22d918fb1f52fef556310230d177d1f2)|141k| 3|
|[IRL](https://etherscan.io/address/0x536013c57daf01d78e8a70cad1b1abada9411819)|21k| 9|
|[Hackathon](https://etherscan.io/address/0x9b9c249be04dd433c7e8fbbf5e61e6741b89966d)|68k | 19|
|[Support](https://etherscan.io/address/0x69a79128462853833e22bba1a43bcdac4725761b)|74k| 19|
|[Bug Bounty](https://etherscan.io/address/0xb3a37c813d3d365a03dd1dd3e68cc11af019cdd6)|14k|0
|[Merch](https://etherscan.io/address/0x0d06a817584ac378849f03df6f11a9ad67dd786d)|39k| 4|
|[Newsletter](https://etherscan.io/address/0x13aEe52C1C688d3554a15556c5353cb0c3696ea2)|14k| 0|
|Total\*\* |741k|249|
\*\*Amounts do not foot due to rounding (739k vs. 741)
### Reserved for Initiatives
Ecosystem reserves amounts to make sure we can cover initiatives that are important. Reserving is not the same as spending it. For example, we are reserving 250k for the bug bounty program. The actual spend will depend on what, if any, bugs are discovered.
| Initiatives | USDC | ETH |
| :------------- | ---: | --: |
| Grants | 300k | 115 |
| Bug Bounty | 250k | 0 |
| Hackathon | 150k | 50 |
| Audit Support | 100k | 0 |
| IRL | 75k | 0 |
| Newsletter | 25k | 0 |
| Reserved Total | 900k | 165 |
**Reconciliation**
| Initiatives|USDC|ETH|
|:----|----:|----:|
|Current Balance|741k| 249|
|Reserved|(900k)| (165)|
|Buffer|(250k)| (50)|
|Total|(409k)|34 |
As result, Ecosystem WG is requesting 409,000 in USDC and zero ETH. This request ensures that there are sufficient resources to meet the expected future needs of the working group.
#### Grants Breakdown
| Item | USDC | ETH |
| ------------- | -------: | ------: |
| eth.limo\*\* | 85k | 10 |
| ensgrants.xyz | 0.0 | 55 |
| Fellowship | 60k | 0 |
| Discretionary | 155k | 50 |
| **Total** | **300k** | **115** |
\*\* Expected to be paid in Q1 2024.
### Initiatives Description
| Initiative | Description |
| -------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Grants | Grants support ens small grants funding, builders such as [eth.limo](https://eth.limo/), [unruggable](https://www.unruggablelabs.com/), library support, and other builder centric bounties |
| Bug Bounty | Supports the official bug bounty [program](https://docs.ens.domains/bug-bounty-program) of ENS -- soon to be managed by [immunifi](https://immunefi.com/) |
| Hackathons | Sponsorship costs and prize money for hackathons and conferences |
| Support | Support mods for social platforms, technical and non-technical educational and archival content |
| IRL | Funding In Real Life events that coincide with the existing Ethereum event schedule |
| Merch | Subsidizing the cost of creating and shipping physical ENS merchandise including shirts, hats, and pins |
| ENS Fellowship | Supports exceptional developers actively creating and contributing to the ENS ecosystem with a 6 month Fellowship |
This proposal was prepared by slobo.eth, lead steward of the ENS Ecosystem Working Group.
---
# [EP2.2.3] [Executable] Q3 & Q4 2022 Public Goods WG Budget
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/13759) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0x5c96e490f3e28d8269e8fc7e929491fb8fa5e4bd04d3379f0c4f4bb1a42dc23e) |
_Note: This was previously numbered EP16.3._
## Summary
The Public Goods Working Group is requesting funding of 430,650 USD equivalent for the second term. The budget has five categories. All figures are presented in USD equivalent.

_**July 19, 2022. ETH = 1500 USD and ENS = 11.00 USD.**_
### Multisigs / Subgroups: $431k USD Value
Funding for each category is associated with a multisig wallet that has a clearly defined mandate. These multisigs been approved by current stewards of the ENS Public Goods working group.
#### Budget

#### Description
The breakdown of each Multisig/Subgroup is the following. The PG Pod is the main multisig for the Public Goods Working Group.

### Grants: $173K USD Value
Funding in this category supports grants that will be distributed through two mechanisms for this term. Rapid Grants will be distributed through the ENS DAO small grants platform, adopting the Nouns Prop.House model. Grants may also be funded for retro-active value provided by web3 Public Goods, with Steward approval.
#### Budget

### Bounties: $113K USD Value
Funding for projects that accomplish certain goals that will be set out in a list of available bounties. Bounties will relate to initiatives and projects for which there is a need that benefits the broader web3 community.
#### Budget

### Gitcoin GR15 & GR16: $100K USD Value
Funding in this category relates to matching funds provided for the main Gitcoin Grants Rounds over the next six months, GR15 and GR16. This amount is not related to the funding for the ENS Ecosystem Round, which is funded by the Ecosystem Working Group.
#### Budget

### Unallocated: $45K USD
The funds in this category are reserved for unforeseen grants and unexpected expenses for the term.
#### Budget

---
# [EP5.7] [Social] Security Council
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/temp-check-enable-cancel-role-on-the-dao/19090) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xf3a4673fe04a3ecfed4a2f066f6ced1539a5466d61630428333360b843653c54) |
## Abstract
The primary mission of ENS DAO is to govern the protocol and allocate resources from the treasury in line with the DAO's constitution and broader objectives. However, due to changing economic dynamics, the DAO is increasingly vulnerable to attacks aimed at draining its treasury.
To safeguard the DAO's integrity and longevity, a Security Council with the authority to cancel malicious proposals is needed. To avoid perpetuating centralized power, the Security Council's authority will have a built-in expiration date. After two years, anyone will be able to call a function that revokes the council's power to veto proposals, ensuring a time-limited mechanism to counter malicious attacks while promoting more delegation and governance distribution.
## Motivation
As ENS continues to grow, its treasury in ETH is always growing. Simultaneously, the percentage of tokens actively delegated is on the decline.

This imbalance creates a risk where an attacker could acquire enough $ENS to gain control of the DAO at a cost lower than the treasury's total value. This has been a growing concern since March 2023.

Past attacks on DAOs have exploited similar vulnerabilities, with some [being thwarted](https://twitter.com/AragonProject/status/1656028382939815937) by components with veto power. Currently, the ENS governance process involves a proposal passing through the governor, relying on delegated voting power for approval. If approved, the governor queues the proposal in a timelock contract, delaying execution by two days. While the governor can cancel proposals, it follows the same pathway as a malicious proposal, introducing potential risks.
The short-term solution was delegating 3.8M $ENS to a contract that can only vote "Against"; more details about this can be found in [Nick's forum post](https://discuss.ens.domains/t/introducing-veto-ensdao-eth/19088). The attack is still profitable and, depending on market conditions can be up to a 3x ROI, like in Dec 2023. We need a mid-term solution to cancel the attack, which is this proposal. An article about this research done by the Blockful team will be published [here](https://blockful.io/blog/ens-security-council-snapshot) after the proposal is executed and there is no attack risk.
## Specification
To enhance security, a veto contract will be deployed. Controlled by a Security Council multisig and will have the PROPOSER_ROLE in the timelock, granting it the ability to cancel proposals without the power to initiate or modify other DAO actions. The scope of this proposal is to assign the PROPOSER_ROLE to the veto contract,
To ensure decentralization, the contract will also feature a time-based expiration mechanism that allows anyone to revoke the PROPOSER_ROLE after two years. This window provides time to strengthen delegation and address current vulnerabilities, facilitating the DAO's transition to a more secure governance model.
## Security considerations
Assigning the PROPOSER_ROLE to a multisig within the timelock contract is overly broad for our requirements as it allows the address to add proposals directly to the queue. If the multisig signers are compromised, they could potentially propose and execute malicious changes. Therefore our approach would be to deploy a new contract similar to the current veto.ensdao.eth contract, which can only do one action: to CANCEL a transaction in the timelock. That would be a trivially simple contract and it would be hard locked to only accept calls from a newly created SAFE multisig.
The risk is mitigated but one scenario remains: if the whole multisig is compromised then a malicious entity could kick other signers and effectively stop the DAO from executing proposals by canceling all transactions, including any that would remove this contract from the proposal role. Anyways, after 2 years, anyone can remove the proposal role.
With that in light. the following considerations are essential for ensuring the Security Council's multisig operates securely:
- Availability of Signers: It is critical to avoid scenarios where signers are unavailable during emergencies. Events like the Shanghai attack, where real-life occurrences prevent signers from accessing their wallets, must be avoided. Council members should ensure wallets and necessary equipment are accessible at all times.
- Secure Wallet Practices: Security Council addresses should be exclusive to ENS-related operations. Private keys must be stored using best practices to minimize exposure to risks.
Finding the right balance for the multisig threshold is crucial. A higher threshold can complicate coordination but reduces the risk of malicious activity. A lower threshold, while more agile, could make the DAO more susceptible to attacks or unintended consequences if a few signers are compromised. The suggested composition is a 4/8 multisig.
## Council Operations
It is in the best interest of everyone to make clear the expectations and responsibilities ENS DAO put on those members, backed by the reputation, other roles and gains those might have in the organization.
The security council is expected to act only in emergency, in the given following situations or similar cases:
- If a proposal goes against the ENS constitution
- If a proposal is approved with malicious intent against the DAO longevity/sustainability
- If such proposal is approved by any group of voters, but directly financially incentivised to vote against the DAOs interests to preserve their own financial stake.
- If any approved proposal goes directly against the DAO for the sole benefit of an attacker.
Those definitions are not exhaustive, and the trust deposited in the elected members for the council also encompasses the trust in their capacity to:
- understand ENS DAO thoroughly
- listening to the community feedback on extreme situations
- take quick action on behalf of the DAO
- comprehend the proposals being approved and their repercussions
The Security Council members will be the same signers for the veto.ensdao.eth, their identities are known, have signed a pledged to uphold the ENS constitution and live in countries with a solid legal system.
---
# [EP3.1.3] [Social] Q1/Q2 2023 Funding Request: Public Goods Working Group
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/15941) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0x41b3509b88e15677aa15680f48278517f794822fb9a79b9c621def53f1866be7) |
## Abstract
The Public Goods Working Group requests funding of 250,000 USDC and 50 ETH from the ENS DAO for Q1/Q2 2023.
This funding will be used to support projects and builders as provisioned by Article III of the ENS DAO Constitution, which provides for the funding of public goods in web3.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)). If this proposal is passed the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
| | USDC | ETH | $ENS |
| --------------------- | :-----: | :-: | :--: |
| Public Goods Multisig | 250,000 | 50 | - |
## Description
**Public Goods Multisig Balance**
The Public Goods Working Group is requesting 250,000 USDC, 50 ETH, and 0 $ENS.
The Public Goods Working Group currently has 146,548 USDC, 75 ETH, and 200 $ENS in its multisigs.
If this funding request is approved, the Public Goods Working Group multisigs will have a total of 396,548 USDC, 125 ETH, and 200 $ENS.
| | USDC | ETH | $ENS |
| --------------------------------- | ----------- | ------- | ------- |
| Carried Forward (from Q3/Q4 2022) | 146,548 | 75 | 200 |
| Requested (for Q1/Q2 2023) | 250,000 | 50 | 0 |
| **Total Balance** | **396,548** | **125** | **200** |
**Public Goods Multisig Allocations for Q1/Q2 2023 (with funding request approved)**
The table below shows the total allocations for the Public Goods Working Group multisig, and related pods, with funds carried forward from last term (Q3/Q4 2022) along with funds requested in this proposal for this term (Q1/Q2 2023).
| | USDC | ETH | $ENS |
| ----------------- | ----------- | ------- | ------- |
| Small Grants | - | 50 | - |
| Gitcoin Grants | 100,000 | - | - |
| Rapid Grants | 50,000 | 10 | - |
| Large Grants | 200,000 | 50 | - |
| Discretionary | 46,548 | 15 | 200 |
| **Total Balance** | **396,548** | **125** | **200** |
**Allocation of Requested Funds**
The 250,000 USDC and 50 ETH will be allocated to the following initiatives/outcomes.
| | USDC | ETH | $ENS |
| ------------------------- | ----------- | ------ | ----- |
| Small Grants | - | 40 | - |
| Gitcoin Grants | 50,000 | - | - |
| Rapid Grants | 50,000 | 10 | - |
| Large Grants | 150,000 | - | - |
| Discretionary | - | - | - |
| **Total Requested Funds** | **250,000** | **50** | **-** |
**Description of Initiatives/Pods**
| Initiative/Pod | Description | Multisig Signer |
| -------------- | --------------------------------------------------------------------------------------------------- | --------------------- |
| Small Grants | ETH for the top Public Goods proposals submitted to ensgrants.xyz | Public Goods stewards |
| Gitcoin Grants | Support for Open Source and Ethereum Infrastructure rounds by Gitcoin through their grants protocol | Public Goods stewards |
| Rapid Grants | Grants up to $10k each for web3 public goods | Public Goods stewards |
| Large Grants | Grants up to $100k each for web3 public goods | Public Goods stewards |
| Discretionary | Funds distributed at the discretion of stewards towards new initiatives + public goods experiments | Public Goods stewards |
This proposal was prepared by Coltron.eth, lead steward of the Public Goods Working Group.
---
# [6.7] [Executable] Transfer .ceo TLD to the DNSSEC registrar
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/temp-check-executable-transfer-ceo-tld-to-the-dnssec-registrar/20594) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/51329136884099251878034959419745117799040381230875971532699817044496491926852) |
## Abstract
The .ceo TLD, formerly owned by Kred Pty, has since been acquired by XYZ. Prior to the formation of the DAO, the previous owner asked for .ceo to be delegated to a custom address so they can manage a bespoke DNS integration. The new owner has requested that this change be undone, and that ownership of .ceo be reverted to the DNSSEC registrar so owners of .ceo TLDs can use the standard integration to claim their names on ENS.
To prove ownership of .ceo and their intention that we action this request, they have set a TXT record on `_ens.nic.ceo` to the address of the DNSSEC registrar, `0xB32cB5677a7C971689228EC835800432B339bA2B`. This can be verified with the following command:
```
dig TXT _ens.nic.ceo
```
## Specification
Call `setSubnodeOwner` on the ENS `Root` contract at `0xaB528d626EC275E3faD363fF1393A41F581c5897`, passing in the keccak256 hash of `ceo` and the address of the DNSSEC registrar, `0xB32cB5677a7C971689228EC835800432B339bA2B`.
## Transactions
---
# [EP 5.6] [Executable] Enable Self-Funding for the Endowment
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-6-executable-enable-self-funding-for-the-endowment/18998) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/45720656345448826187222659689199787286494046921153399380076959662321080408931) |
Authorizes metagov to withdraw 30 ETH from the endowment each month for fees, and reimburses metagov for 43.54 ETH in fees already incurred.
## Abstract
This proposal outlines a strategic shift allowing the Endowment to autonomously finance its operations. Specifically, it grants the Metagov stewards the authority to withdraw up to 30 ETH monthly from the Endowment, designated for payments to karpatkey and [@steakhouse](https://discuss.ens.domains/u/steakhouse) for their services in managing the Endowment. This initiative provides the stewards with a direct allowance for these expenses, eliminating the need for these costs to be continually factored into their requests for DAO budget allocations.
Additionally, this proposal seeks to reimburse the [Metagov Safe](https://etherscan.io/address/0x91c32893216dE3eA0a55ABb9851f581d4503d39b) for the payments made to karpatkey and Steakhouse Financial during 2024, covering the service fees for [January](https://etherscan.io/tx/0x4ef4b34d397e6ce1fbe1fbe3e94340dcf02d590fc9d93c7107a76282beb201c6) and [February](https://etherscan.io/tx/0xdd631a42ccc3762e285276043926c52b86dedbc70fa34dddd611585a38534a89), which total 43.54 ETH.
## Motivation
Since the initiation of the Endowment through the execution of the [first tranche](https://discuss.ens.domains/t/ep3-4-executable-fund-the-endowment-first-tranche/16277#specification-3), payments to karpatkey and Steakhouse Financial for their services have been categorized as operational expenditures. These costs were funded through the Metagov Safe, necessitating routine funding requests from the Meta-Governance Working Group to the DAO.
To simplify and enhance the financial autonomy of the Endowment, we propose granting a monthly allowance of up to 30 ETH to the Metagov Safe by leveraging the Spending Limits feature available on Safe.
This measure allows Metagov stewards to settle Endowment fees directly from funds within the Endowment itself, thus eliminating the regular necessity to refill the Metagov Safe. Should the allowance cap be reached, further withdrawals will be paused until the commencement of the next 30-day cycle. This adjustment eradicates the need for frequent funding operations, paving the way for the Endowment's sustained self-sufficiency.
The monthly cap of 30 ETH has been carefully chosen to align with the Endowment's scale and historical fee structure, ensuring it suffices to cover monthly expenses. Should there be a need to revise this limit to better suit future requirements, adjustments can be facilitated through the submission of a new proposal.
Finally, to ensure the strategic shift is applied retroactively from the outset of 2024, this proposal requests that the Endowment reimburse the Metagov Safe for expenses incurred through payments to karpatkey and Steakhouse Financial. This reimbursement, aimed at covering the service fees for January and February 2024, amounts to a total of 43.54 ETH.
## Specification
The following payload will be executed by the DAO Wallet to create the mentioned allowance and refund:
```plaintext
Calldata:
0x6a76120200000000000000000000000040a2accbd92bca938b02010e17a5b8929b49130d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000002448d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001eb004f2083f5fbede34c2714affb3105539775f7fe6400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024610b5925000000000000000000000000cfbfac74c26f8647cbdb8c5caf80bb5b32e4313400cfbfac74c26f8647cbdb8c5caf80bb5b32e4313400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024e71bdf4100000000000000000000000091c32893216de3ea0a55abb9851f581d4503d39b00cfbfac74c26f8647cbdb8c5caf80bb5b32e43134000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4beaeb38800000000000000000000000091c32893216de3ea0a55abb9851f581d4503d39b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a8c00000000000000000000000000000000000000000000000000000000001b33acd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000fe89cc7abb2c4183683ab71653c4cdc9b02d44b700000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000
Target:
0x4f2083f5fbede34c2714affb3105539775f7fe64
Value:
0
Calldata:
0x6a76120200000000000000000000000091c32893216de3ea0a55abb9851f581d4503d39b0000000000000000000000000000000000000000000000025c3d2750b08200000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000fe89cc7abb2c4183683ab71653c4cdc9b02d44b700000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000
Target:
0x4f2083f5fbede34c2714affb3105539775f7fe64
Value:
0
```
---
# [5.17.3] [Social] Funding Request: ENS Public Goods Working Group
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/5-17-3-social-funding-request-ens-public-goods-working-group/19679) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xafb1325f49057dd20fabf2908531da93565172525309a0a3a914daa7f773b0c6) |
## Abstract
The ENS Public Goods Working Group requests funding to support operations until the next funding window in April 2025.
The Public Goods working group funds projects and builders improving the Web3 ecosystem. This funding stream is authorized in Article III of the ENS DAO Constitution. The funding supports initiatives related to open-source software, tooling, research and any practical implementations that broadly benefit a wide range of users of Ethereum and Web3.
The funds requested extends current need through to next term to ensure that next season’s stewards have some available funding before the next funding window available.
The total request in this proposal is 226k USDC to be transferred from the DAO wallet to the Public Goods Working Group.
## Specification
This specification is the amount requested from the DAO treasury to the Public Goods Multisig to fulfill anticipated budgetary needs through the next formal funding window in April 2025.


Of these funds, 256k is allocated to large grants builders and initiaves through to the end of 2024. This leaves approximately 89k USD and 29.5 Ether unallocated and available to be rolled into the next term.

**Note #1:** To ensure grantees have consistency, the funds requested for this category carry the program to Q3 which is an exception from the other spending categories which plan only through April 2025.
**Note #2:** The PG Working group budget request is in USDC only; our balance of ETH will rollover to next term, but it will be left to future stewards discretion to swap it, keep it or return it to the treasury.

Subtracting the 89k available to rollover from 2024, the unmet need is 226k USDC which is the sum total to be included as the specification of this funding request.
## Spending Categories Explained
### Builder Grants
This new category consolidates the existing Large and Small grants under one category and program title. The Public Good working group has been working with a provider to create a unified platform which will be announced during frENSday in Bangkok on 11/11/2024. To ensure grantees have consistency, the funds requested for this category carry the program through to Q3 which is an exception from the other spending categories which plan through to April 2025.
- **Small Grants**
Multiple micro-grants will be distributed to builders via the Builder Grants platform to be launched. We have added the amount expected to spend in the next 5 months with the same amounts distributed in the last rounds.
- **Large Grants**
Grants up to 50k USDC with applications accepted on a rolling basis throughout the term. Large Grants will continue in Q4. The working group will continue to focus on strengthening impact measurements accomplished by grantees during the previous Large Grants cycle.
### PG Perpetual Bounty
Launched during ETHLondon in March 2024, this is a perpetual pool of funds that is available to builders who are developing projects which align with the principles of public goods in the web3 space.
This funding pool is available at any time, but also marketed specifically towards builders who are not eligible for ENS-related bounties during hackthons, but deserve support for their public goods mission.
### Events and Hackathons
The working group provides support to Public Goods events and hackathons. These funds cover expenses related to event sponsorship, bounties, judging and other participatory support roles during events.
**The current earmarked events are:**
- ETHGlobal Bangkok
- Devcon
- ETHDenver 2025
\*This list is not set as several events are still in planning stages for Q1 of 2025. The PG stewards continuously assess opportunities for expanding the public goods conversation and collaborations.
### Discretionary
The funds in this category are reserved for additional grant opportunities and expenses that arise during the term. Spending in this category is at the discretion of the working group stewards.
---
# [EP 5.27] [Executable] Revoke the DAO's ability to upgrade the name wrapper
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-27-executable-revoke-the-daos-ability-to-upgrade-the-name-wrapper/19920) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/40272043175867710511047990376059633000673751036364092383567949469124429500507) |
## Abstract
The Name Wrapper's upgrade mechanism contains a vulnerability whereby a malicious DAO could use the upgrade mechanism to seize control of wrapped names without the owner's consent, documented [here](https://discuss.ens.domains/t/security-advisory-a-malicious-dao-update-could-reduce-the-registration-duration-of-registered-eth-2lds/17576/1).
Since the v2 migration plan makes the upgrade mechanism obsolete, we now know the mechanism will never be required. This EP proposes to remove the DAO's ability to upgrade the name wrapper.
## Specification
Admin control over the name wrapper gives the DAO two functions: it can set the upgrade contract, and it can specify the address of the metadata contract. Since we want to remove the former ability while preserving the latter, we propose the following sequence of actions:
1. Deploy a new metadata contract, identical to the current one but using a proxy. The proxy instance should be owned by the DAO to provide for future metadata upgrades.
2. Update the name wrapper to reference the new metadata contract instead of the old one.
3. Revoke admin ownership over the name wrapper.
A new metadata service has been deployed at 0x806f84F3789f51352C1B0aB3fFa192665d283808, and a transparent proxy was deployed in transaction 0xd0aca1f2efb2db5e3d494649004e341decb2e94a1f30e94f301b6626702ee4c8, at address 0xabb76d7e79de010117b147761013f11630a6799f, with the initial implementation set to the above address, and the owner set to wallet.ensdao.eth. The admin contract for this proxy is at 0xeae9309ddb1aadb4cf1ebad5e51aef999833a992.
The executable component of this proposal sets the metadata service address on the name wrapper to the above proxy, then revokes ownership over it.
---
# [EP1.7] [Executable] End the $ENS and EP2 airdrops
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/12047) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/99882233577221676057992280816078245519848378270443751235073826886360950537295) |
_Note: This was previously numbered EP11._
## Abstract
The $ENS airdrop can be terminated at any time on or after May 4, 2022 by a call from the DAO, transferring remaining tokens to an address it specifies. The EP2 airdrop can be terminated at any time by revoking the token approval given to it by the DAO. This EP proposes to execute both of these actions on or shortly after May 4, 2022.
## Specification
- Call 'sweep' on the ENS token contract, specifying the DAO wallet as target address.
- Call 'approve' on the ENS token contract, specifying the EP2 airdrop contract and an allowance of 0.
## Transactions
| Address | Value | Function | Argument | Value |
| ------------------------------------------ | ----- | -------- | -------- | ------------------------------------------ |
| 0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72 | 0 | sweep | dest | 0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7 |
| 0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72 | 0 | approve | spender | 0x4A1241C2Cf2fD4a39918BCd738f90Bd7094eC2DC |
| amount | 0 | | | |
---
# [EP3.1.2] [Social] Q1/Q2 2023 Funding Request: Meta-Governance Working Group
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/15940) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0xd7eff781be059513b5cd64d79e709abbbc653944c9a8c621dc051e7b42a405cb) |
## Abstract
The Meta-Governance Working Group requests funding of 364,000 USDC, 125 ETH, and 3,500 $ENS from the ENS DAO for Q1/Q2 2023.
This funding will be used to support the governance processes of the ENS DAO as well as manage and build infrastructure to support the ENS DAO and Working Groups.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)). If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
| | USDC | ETH | $ENS |
| ------------------------ | :-----: | :-: | :---: |
| Meta-Governance Multisig | 364,000 | 125 | 3,500 |
## Description
**Meta-Governance Multisig Balance**
The Meta-Governance Working Group is requesting 364,000 USDC, 125 ETH, and 3,500 $ENS.
The Meta-Governance Working Group currently has 217,510 USDC, 17 ETH, and 8,940 $ENS in its multi-sigs.
If this funding request is approved, the Meta-Governance Working Group multi-sigs will have a total of 581,510 USDC, 142 ETH, and 12,440 $ENS.
| | USDC | ETH | $ENS |
| --------------------------------- | ----------- | ------- | ---------- |
| Carried Forward (from Q3/Q4 2022) | 217,510 | 17 | 8,940 |
| Requested (for Q1/Q2 2023) | 364,000 | 125 | 3,500 |
| **Total Balance** | **581,510** | **142** | **12,440** |
**MetaGovernance Multisig Allocations for Q1/Q2 2023 (with funding request approved)**
The table below shows the total allocations for the MetaGovernance Working Group multi-sig, and related pods, with funds carried forward from last term (Q3/Q4 2022) along with funds requested in this proposal for this term (Q1/Q2 2023).
| | USDC | ETH | $ENS |
| -------------------------------- | ----------- | ------- | ---------- |
| Steward + Secretary Compensation | 184,000 | - | 3,500 |
| Governance | 88,500 | 32 | 1,250 |
| DAO Tooling | 171,000 | 60 | 3,000 |
| DAO Sponsorship | 60,000 | 10 | - |
| Discretionary | 78,010 | 40 | 4,690 |
| **Total Balance** | **581,510** | **142** | **12,440** |
**Allocation of Requested Funds**
The 364,000 USDC, 125 ETH, and 3,500 $ENS of funds requested in this proposal will be allocated to the following initiatives/outcomes.
| | USDC | ETH | $ENS |
| -------------------------------- | ----------- | ------- | --------- |
| Steward + Secretary Compensation | 184,000 | - | 3,500 |
| Governance | 40,000 | 30 | - |
| DAO Tooling | 80,000 | 50 | - |
| DAO Sponsorship | 60,000 | 10 | - |
| Discretionary | - | 35 | - |
| **Total Requested Funds** | **364,000** | **125** | **3,500** |
**Description of Initiatives/Pods**
| Initiative/Pod | Description | Multisig Signers or Lead Signer |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| Steward + Secretary Compensation | Working Group Steward compensation totaling $144k USDC + 3,000 $ENS plus Secretary compensation of $40k USDC | MetaGov stewards |
| Governance | Fee reimbursements and initiatives related to reducing friction in the governance process | MetaGov stewards |
| DAO Tooling | Developing interfaces and dashboards to improve the governance process and increase transparency across the DAO | alisha.eth |
| DAO Sponsorship | Sponsoring DAO-specific events such as DAO NYC, DAO Tokyo, and Aragon's DAO Global Hackathon | MetaGov stewards |
| Discretionary | Funds distributed at the discretion of stewards towards new initiatives + governance experiments | MetaGov stewards |
This proposal was prepared by Katherine Wu, lead steward of the MetaGovernance Working Group.
---
# [EP3.7] [Social] Approval of ENS Name Normalization Standard (ENSIP-15)
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/16957) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xed7bbde7c1136cbb5b9090a0abd48438c97a020b9e8a1e8f6257a46d068aa2e0) |
## Abstract
This is a vote to approve [ENSIP-15: Normalization Standard.](https://docs.ens.domains/ens-improvement-proposals/ensip-15-normalization-standard)
## Motivation
### EP3.7 Motivation
- Normalization isn't enforced on-chain.
- There is no code for the DAO to execute.
- Approval for ENSIP-15 should be confirmed through a social vote.
### ENSIP-15 Motivation
- Since [ENSIP-1](https://github.com/ensdomains/governance-docs/blob/8e2e6c98198dd31fd2e982987e286fd6ce540319/governance-proposals/ensip-1-ens.md) (originally [EIP-137](https://eips.ethereum.org/EIPS/eip-137)) was finalized in 2016, Unicode has [evolved](https://unicode.org/history/publicationdates.html) from version 8.0.0 to 15.0.0 and incorporated many new characters, including complex emoji sequences.
- ENSIP-1 does not state the version of Unicode.
- ENSIP-1 implies but does not state an explicit flavor of IDNA processing.
- [UTS-46](https://unicode.org/reports/tr46/) is insufficient to normalize emoji sequences. Correct emoji processing is only possible with [UTS-51](https://www.unicode.org/reports/tr51/).
- Validation tests are needed to ensure implementation compliance.
- The success of ENS has encouraged spoofing via the following techniques:
1. Insertion of zero-width characters.
2. Using names which normalize differently between algorithms.
3. Using names which appear differently between applications and devices.
4. Substitution of confusable (look-alike) characters.
5. Mixing incompatible scripts.
## Specification
- Replace [ENSIP-1 § Name Syntax](https://docs.ens.domains/ens-improvement-proposals/ensip-1-ens#name-syntax) "UTS-46 algorithm" with link to [ENSIP-15](https://docs.ens.domains/ens-improvement-proposals/ensip-15-normalization-standard).
- Agree to normalize names according to ENSIP-15 for a safer end-user experience.
- Examples:
1. 
1. 
1. 
- Libraries implementing ENSIP-15:
1. Javascript — [adraffy/ens-normalize](https://github.com/adraffy/ens-normalize.js)
2. Javascript — [ensdomains/eth-ens-namehash](https://github.com/ensdomains/eth-ens-namehash)
3. Python — [namehash/ens-normalize-python](https://github.com/namehash/ens-normalize-python)
- Web Frameworks using ENSIP-15:
1. Javascript — [ethers/ethers.io](https://github.com/ethers-io/ethers.js/)
2. Javascript — [web3/web3.js](https://github.com/web3/web3.js)
3. Javascript — [wagmi-dev/viem](https://github.com/wagmi-dev/viem)
- Names visible to the end-user should be [**beautified**](https://docs.ens.domains/ens-improvement-proposals/ensip-15-normalization-standard#annex-beautification) for a more consistent appearance.
- Example: These labels are the same:\
[](https://camo.githubusercontent.com/1b7b4f3d0baa83d62415760a4622955e6de1f8f102a8ea5070e21ae0d44ab9ea/68747470733a2f2f692e696d6775722e636f6d2f703772785572452e706e67)
## Voting
This vote is a single choice vote. You may vote for one of the following options:
- **For**
- **Against**
- **Abstain**
By voting **For** this proposal, you are voting in favor of approving [ENSIP-15](https://docs.ens.domains/ens-improvement-proposals/ensip-15-normalization-standard).
---
# [5.17.2] [Social] Funding Request: ENS Ecosystem Working Group
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/5-17-2-social-funding-request-ens-ecosystem-working-group/19678) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xfe303865510b5ef7fabee2bcbd5081afa01f276195f57e1561ff27c477459984) |
## Abstract
The ENS Ecosystem Working Group requests funding of 836,000 USDC to support operations through April 2025. This is the only funding request of Term 5.
The ENS Ecosystem Working Group is responsible for growing and improving the ENS Ecosystem by funding builders and projects that are ENS-specific or ENS-centric.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)). If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
| | USDC | ETH | $ENS |
| ------------------------------------------------------------------------------------------------------ | :-----: | :-: | :--: |
| [ENS Ecosystem Main Multisig](https://etherscan.io/address/0x2686a8919df194aa7673244549e68d42c1685d03) | 836,000 | -- | -- |
## Description
### ENS Ecosystem Multisig Balances - As of October 4, 2023
The ENS Ecosystem Working Group multisigs:
| Multisigs | USDC | ETH |
| :--------------------------------------------------------------------------------------- | -------: | -------: |
| [Main Multisig](https://etherscan.io/address/0x2686a8919df194aa7673244549e68d42c1685d03) | 82k | 81.7 |
| [IRL](https://etherscan.io/address/0x536013c57daf01d78e8a70cad1b1abada9411819) | 21k | - |
| [Hackathon](https://etherscan.io/address/0x9b9c249be04dd433c7e8fbbf5e61e6741b89966d) | 22k | 9.8 |
| [Newsletter](https://etherscan.io/address/0x13aEe52C1C688d3554a15556c5353cb0c3696ea2) | 4k | - |
| **Total** | **129k** | **91.5** |
### Reserved for Initiatives
Ecosystem reserves amounts to make sure we can cover initiatives that are important. Reserving is not the same as spending it. For example, we are reserving 100k for the bug bounty program. The actual spend will depend on what, if any, bugs are discovered.
| Initiatives | USDC | ETH |
| :-------------- | -------: | -----: |
| Hackathon | 300k | - |
| Grants | 200k | 10 |
| Library Support | 100k | - |
| Bug Bounty | 100k | - |
| Audit Support | 100k | - |
| IRL | 50k | - |
| Support | 15k | - |
| Reserved Total | **865k** | **10** |
**Reconciliation**
| Initiatives|USDC|ETH|
|:----|----:|----:|
|Current Balance|129k| 91.5|
|Reserved|(865k)| (10)|
|Buffer|(100k)| (81.5)|
|Total|**(836k)**|**-** |
Ecosystem WG is requesting 836,000 in USDC and zero ETH. This request ensures that there are sufficient resources to meet the expected future needs of the ecosystem working group. Buffer is an additional reserve to cover unforeseen expenses or opportunities.
### Initiatives Description
| Initiative | Description |
| --------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Hackathons | For our main partner, ETHGlobal, payments are made in December for the following calendar year. We anticipate participating in at least 7 events. |
| Grants | Grants consist of [builder grants](https://discuss.ens.domains/t/term-5-grants-summary/18921), [Gitcoin rounds](https://discuss.ens.domains/t/gg20-ens-identity-round-conclusion/19301), and [ENS subgraph](https://discuss.ens.domains/t/ens-subgraph-migration-to-the-decentralised-version/19183/4) subsidization. |
| Bug Bounty | Supports the official bug bounty program of ENS administered by [immunefi](https://immunefi.com/bug-bounty/ens/information/). |
| Library Support | Support open-source libraries that ENS depends on, either directly or through a program such as [drips](https://blog.ens.domains/post/supporting-software-dependencies-with-drips). |
| Audit Support | Provide funding for auditing smart contracts that have significant prominence in the ecosystem or are expected to see widespread use. |
| IRL | Funding relates to events that coincide with conferences, such as ethCC & Devcon. |
| Support | Support mods for social platforms, technical and non-technical educational content and the [newsletter](https://discuss.ens.domains/t/ens-dao-newsletter-70-09-24-24/19621). |
### Historical Spending
For historical spending consult the spending reports for [Q1](https://discuss.ens.domains/t/term-5-working-group-spending-summary-2024-q1/19146) & [Q2](https://discuss.ens.domains/t/term-5-working-group-spending-summary-2024-q2/19449), which are released within 30 days after the end of each quarter.
---
_This proposal was prepared by [slobo.eth](https://x.com/AlexSlobodnik), Lead Steward of the Ecosystem Working Group._
---
# [EP1.2.2] [Social] Election of a new Director of The ENS Foundation
::authors
| **Status** | Passed, avsa.eth selected |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/11093) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0xc4bc562d32e59a528dec301261e8d2b3b0c6880c2b782201c2e9f1ff3979d165) |
_Note: This was previously numbered EP6.2._
## Summary
This proposal is for the election of a new Director of the ENS Foundation.
## Abstract
This social proposal puts forth a vote for the election of a new Director of the ENS foundation.
This is a **contingent proposal** that will only be ratified if [\[EP6.1\] \[Social\] Removal of Brantly Millegan as Director of The ENS Foundation](https://discuss.ens.domains/t/ep-6-1-social-removal-of-brantly-millegan-as-director-of-the-ens-foundation/11092) determines that Brantly Millegan shall be removed from the directorship of the Foundation Company.
**Contingency:** If majority "No" is voted on [\[EP6.1\] \[Social\] Removal of Brantly Millegan as Director of The ENS Foundation](https://discuss.ens.domains/t/ep-6-1-social-removal-of-brantly-millegan-as-director-of-the-ens-foundation/11092), this proposal shall be null and void.
## Motivation
By a vote of the council, Brantly Millegan was removed as a director of the Foundation Company., the DAO shall decide whether Brantly Millegan is deemed capable, or otherwise, of continuing his role as the Director of the Ethereum Foundation, and to appoint a suitable replacement if he is deems incapable.
A timeline of events that led to this proposal has been posted by Community WG Stewards in another post [here](https://discuss.ens.domains/t/ens-dao-next-steps-re-brantly/10424/15) and quoted below:
> In May 2016, Brantly Millegan tweeted that, "Homosexual acts are evil. Transgenderism doesn't exist. Abortion is murder. Contraception is a perversion. So is masturbation and porn."
>
> On February 5th, 2022, brantly.eth, well-known as a prominent representative of the ENS protocol, [defended these views in a publicly held Twitter space](https://www.dropbox.com/s/ljrp0vjibawuufr/Brantly%20ENS.mp4?dl=0).
>
> Irrespective of one's personal beliefs, we, as ENS DAO Community Stewards, must set the example for inclusivity and must not divide our community.
>
> Propagating rhetoric that is viewed as hateful and discriminatory is not conduct conducive to the role as a Community Steward. This conduct will not be tolerated despite one's contributions to the protocol.
## Specification
The appointment of the incoming Director will be held by a [ranked choice](https://en.wikipedia.org/wiki/Ranked_voting) Snapshot vote from a list of pre-determined nominees. Additionally, there shall be an option for voters to select "None of the above" or abstain from this vote.
**Draft Ranked Choice Snapshot Vote**
> Who should be elected as the new Director of the ENS Foundation?
>
> **Choice 1:** [avsa.eth](https://discuss.ens.domains/t/nominations-for-ens-foundation-director-to-replace-brantly-eth/10634/12)
>
> **Choice 2:** [daylon.eth](https://discuss.ens.domains/t/nominations-for-ens-foundation-director-to-replace-brantly-eth/10634/9)
>
> **Choice 3:** [healingvisions.eth](https://discuss.ens.domains/t/nominations-for-ens-foundation-director-to-replace-brantly-eth/10634/3)
>
> **Choice 4:** None of the above.
>
> **Choice 5:** Abstain.
**Note:** \*This list was randomized by @berrios.eth.
The nomination process for this appointment can be found [here](https://discuss.ens.domains/t/nominations-for-ens-foundation-director-to-replace-brantly-eth/10634), and a summary of Director's roles, responsibilities, compensation and liabilities can be found [here](https://discuss.ens.domains/t/role-responsibilities-of-ens-foundation-director/10632).
## Notice to The ENS Foundation
In line with this proposal, a formal notice is served to the ENS Foundation as follows:
_"Pursuant to Article 15 of the Articles of Association, the council, hereby, gives notice to the Foundation Company of the appointment of a director of the Foundation Company, to serve pursuant to the terms of its Articles._
_Whereas, the council undertook a vote, via Snapshot, to remove Brantly Millegan,_
_Whereas, the majority of votes cast was to remove Brantly MIllegan and formal Notice given to the Foundation Company of such result,_
_Whereas, the council undertook a second vote, via Snapshot, from among nominees of the council, to appoint a new director of the Foundation Company;_
_Wherefore, the person, if any, with the highest number of votes cast is hereby appointed a director of the Foundation Company._
_The results to be formally noted in the council records and the name of such person, if there be one, shall be promptly communicated to the Foundation Company without further process."_
You may view each candidates [delegate application here ](https://discuss.ens.domains/t/ens-dao-delegate-applications/815/1154)and comments in on nominees for directorship [here](https://discuss.ens.domains/t/comments-on-ens-foundation-nominees/10658).
---
# [EP5.10] [Social] Confirming the ENS DAO Security Council Members
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/temp-check-enable-cancel-role-on-the-dao/19090) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xa0b1bfadf6853b5b0d59d3c4d73c434fc6389339887d05de805361372eb17c3a) |
## Abstract
Following the successful passing of the [EP5.7](https://snapshot.org/#/ens.eth/proposal/0xf3a4673fe04a3ecfed4a2f066f6ced1539a5466d61630428333360b843653c54), this proposal aims to confirm the 8 individuals who will form the Security Council with the permissions defined in [EP5.7](https://snapshot.org/#/ens.eth/proposal/0xf3a4673fe04a3ecfed4a2f066f6ced1539a5466d61630428333360b843653c54). The Security Council will be responsible for protecting the organization from potential governance attacks by having the ability to cancel malicious proposals using the [SecurityCouncil](https://github.com/blockful-io/security-council-ens/blob/main/README.md) smart contract.
## Specification
A discussion was held in the ENS forum titled **[[Temp Check] Enable CANCEL role on the DAO](https://discuss.ens.domains/t/temp-check-enable-cancel-role-on-the-dao/19090)**.
Following that discussion, The [EP5.7](https://snapshot.org/#/ens.eth/proposal/0xf3a4673fe04a3ecfed4a2f066f6ced1539a5466d61630428333360b843653c54) proposal, which can be [found here](https://snapshot.org/#/ens.eth/proposal/0xf3a4673fe04a3ecfed4a2f066f6ced1539a5466d61630428333360b843653c54), detailed the need for a Security Council to mitigate the risk of governance attacks on the ENS DAO. The proposal passed with overwhelming support from the community, receiving 1.4 million votes and 100% approval.
As outlined in the [EP5.7](https://snapshot.org/#/ens.eth/proposal/0xf3a4673fe04a3ecfed4a2f066f6ced1539a5466d61630428333360b843653c54) proposal, the [SecurityCouncil](https://github.com/blockful-io/security-council-ens/blob/main/README.md) smart contract will be deployed, and the Security Council multisig will be granted the PROPOSER_ROLE in the timelock. This will allow the Security Council to cancel malicious proposals, without granting the ability to initiate proposals, vote on proposals, or perform any other actions. The contract also features an expiration mechanism that automatically revokes the council's veto power after 2 years, promoting decentralization.
### Proposed Security Council Members
The proposed Security Council will consist of the following 8 individuals in a 4/8 multisig configuration:
1. [nick.eth](https://www.tally.xyz/gov/ens/delegate/nick.eth)
2. [griff.eth](https://www.tally.xyz/gov/ens/delegate/0x839395e20bbb182fa440d08f850e6c7a8f6f0780)
3. [avsa.eth](https://www.tally.xyz/gov/ens/delegate/avsa.eth)
4. [lefteris.eth](https://www.tally.xyz/gov/ens/delegate/lefteris.eth)
5. [katherineykwu.eth](https://www.tally.xyz/gov/ens/delegate/katherineykwu.eth)
6. [fireeyes.eth](https://www.tally.xyz/gov/ens/delegate/fireeyesdao.eth)
7. [brantly.eth](https://www.tally.xyz/gov/ens/delegate/brantly.eth)
8. [alextnetto.eth](https://www.tally.xyz/gov/ens/delegate/alextnetto.eth)
The text of [EP5.7](https://snapshot.org/#/ens.eth/proposal/0xf3a4673fe04a3ecfed4a2f066f6ced1539a5466d61630428333360b843653c54) specified that the 5 delegates included on the veto contract would be included in the security council. To fill the remaining 3 places on the council, consideration was given to, delegation power, past activity in governance, economic skin on the game, and jurisdictional diversity (for legal reasons).
### Voting
This social proposal seeks to confirm the complete list of 8 individuals as the Security Council members. **The vote will be a simple For/Against/Abstain vote on the entire list.** If the proposal is successful, a separate executable proposal will be put forward to deploy the SecurityCouncil smart contract and grant the necessary roles.
For this vote, the 8 proposed members of the Security Council will vote "**Abstain**" to help meet quorum requirements while preserving their impartiality.
### Next Steps
Upon confirmation of the Security Council members through this social proposal, the following steps will be taken:
1. The SecurityCouncil contract will be manually deployed to Mainnet.
2. A multsig will be manually created with the 8 members as signers.
3. The multisig will accept ownership of the SecurityCouncil contract.
4. The executable proposal will grant the PROPOSER_ROLE to the SecurityCouncil contract address using the grantRole function in the timelock.
Once the executable proposal is passed and the contract is deployed with the necessary roles granted, the Security Council will be able to cancel malicious proposals to protect the ENS DAO.
After the specified expiration period (2 years), anyone can revoke the PROPOSER_ROLE from the Security Council, ensuring this is a time-limited mechanism that defaults back to a more decentralized posture.
## Success Criteria
For this social proposal to pass, the following quorum and voting requirements must be met:
1. **Quorum**: The proposal must receive a minimum of 1% of the total supply of $ENS (1 million votes) in the form of "Yes" and "Abstain" votes combined. "No" votes do not count towards quorum.
2. **Approval**: Once the quorum is reached, the proposal requires a simple majority (>50%) of "Yes" votes among the "Yes" and "No" votes to pass. "Abstain" votes do not count towards the approval calculation.
---
# [EP2.2.2] [Executable] Q3 & Q4 2022 Ecosystem WG Budget
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/13757) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0x97265786d808280adc788e6744dd07afd3ff7e2776527d18f4e19abe1bd6c1a5) |
_Note: This was previously numbered EP16.2._
## Summary
The Ecosystem Working Group is requesting funding of 2.3 million USD equivalent for the second term. The budget has four categories. All figures are presented in USD equivalent.

### Multisigs / Subgroups: $1.5m
Funding in this category is associated with a multisig wallet that has a clearly defined mandate. These multisigs been approved by current stewards of the ENS Ecosystem working group.
Examples of subgroups receiving funding include bug bounty, support mods, dot-eth websites, .limo infrastructure, hackathons, and ENS ecosystem rounds during gitcoin grants rounds.
**Budget**

**Description**

### RFP Related: $435k
Funding in this category relates to current and future proposals. Potential examples include on-chain normalization, ENS avatar, and future RFPs. Details on the RFP process can be found in this [thread](https://discuss.ens.domains/t/transitioning-the-dao-to-an-rfp-model/11821).

### Grants: $262k
Funding in this category supports retro-active grants not covered by predefined multi-sigs in the subgroups category. The majority of this funding is expected to be allocated to the prop-house.

### Unallocated: $109k
The funds in this category are reserved for unexpected needs of the working group over the term.

---
# [EP 5.13] [Executable] Security Council
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep5-13-executable-security-council/19412) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/42329103797433777309488042029679811802172320979541414683300183273376839219133) |
## Abstract
The primary mission of ENS DAO is to govern the protocol and allocate resources from the treasury in line with the DAO's constitution and broader objectives. However, due to changing economic dynamics, the DAO is increasingly vulnerable to attacks aimed at draining its treasury.
To safeguard the DAO's integrity and longevity, a Security Council with the authority to cancel malicious proposals is needed. To avoid perpetuating centralized power, the Security Council's authority will have a built-in expiration date. After two years, anyone will be able to call a [function](https://github.com/blockful-io/security-council-ens/blob/main/src/SecurityCouncil.sol#L59) that revokes the council's power to veto proposals, ensuring a time-limited mechanism to counter malicious attacks while promoting more delegation and governance distribution.

## Motivation
As ENS continues to grow, its treasury in ETH is always growing. Simultaneously, the percentage of tokens actively delegated is on the decline.
This imbalance creates a risk where an attacker could acquire enough $ENS to gain control of the DAO at a cost lower than the treasury's total value. This has been a growing concern since March 2023.
Past attacks on DAOs have exploited similar vulnerabilities, with some [being thwarted](https://x.com/AragonProject/status/1656028382939815937) by components with veto power. Currently, the ENS governance process involves a proposal passing through the governor, relying on delegated voting power for approval. If approved, the governor queues the proposal in a timelock contract, delaying execution by two days. While the governor can cancel proposals, it follows the same pathway as a malicious proposal, introducing potential risks.
The short-term solution was delegating 3.8M $ENS to a contract that can only vote "Against"; more details about this can be found in [Nick's forum post](https://discuss.ens.domains/t/introducing-veto-ensdao-eth/19088). The attack is still profitable and, depending on market conditions can be up to a 3x ROI, like in Dec 2023. We need a **mid-term solution** to cancel the attack, which is this proposal. An article about this research done by the Blockful team will be published [here](https://blockful.io/blog/ens-security-council) after the proposal is executed and there is no attack risk.
## Specification
To enhance security, the [SecurityCouncil contract](https://github.com/blockful-io/security-council-ens/blob/main/src/SecurityCouncil.sol) will be deployed, receiving the PROPOSER_ROLE in the timelock, granting it the ability to cancel proposals (callable only by the [Security Council multisig](https://etherscan.io/address/0xaa5cd05f6b62c3af58ae9c4f3f7a2acc2cdc2cc7)) without the power to initiate or modify other DAO actions. **The scope of this proposal is to assign the PROPOSER_ROLE to the SecurityCouncil contract ([Etherscan](https://etherscan.io/address/0xb8fa0ce3f91f41c5292d07475b445c35ddf63ee0#code))**.
To ensure decentralization, the contract will also feature a time-based expiration mechanism that allows anyone to revoke the PROPOSER_ROLE after two years. This window provides time to strengthen delegation and address current vulnerabilities, facilitating the DAO's transition to a more secure governance scenario.
## Security considerations
Assigning the PROPOSER_ROLE to a multisig within the timelock contract is overly broad for our requirements as it allows the address to create operations in the timelock. If the multisig signers are compromised, they could potentially propose and execute malicious changes. Therefore our approach is deploying a new contract similar to the current veto.ensdao.eth contract, which can only do one action: to CANCEL a transaction in the timelock, triggered only by the security council multisig.
The risk is mitigated but one scenario remains: if the whole multisig is compromised then a malicious entity could kick other signers and effectively stop the DAO from executing proposals by canceling all transactions, including any that would remove this contract from the PROPOSER_ROLE. Anyways, after 2 years, [anyone can remove the PROPOSER_ROLE from the contract](https://github.com/blockful-io/security-council-ens/blob/main/src/SecurityCouncil.sol#L59).
## Council Operations
It is in the best interest of everyone to make clear the expectations and responsibilities ENS DAO put on those members, backed by the reputation, other roles and gains those might have in the organization.
The security council is expected to act only in emergency, in the given following situations or similar cases:
- If a proposal goes against the ENS constitution
- If a proposal is approved with malicious intent against the DAO longevity/sustainability
- If such proposal is approved by any group of voters, but directly financially incentivised to vote against the DAOs interests to preserve their own financial stake.
- If any approved proposal goes directly against the DAO for the sole benefit of an attacker.
## Relevant links
- SecurityCouncil contract ([GitHub](https://github.com/blockful-io/security-council-ens/blob/main/src/SecurityCouncil.sol), [Etherscan](https://etherscan.io/address/0xb8fa0ce3f91f41c5292d07475b445c35ddf63ee0#code))
- Security Council multisig ([Safe](https://app.safe.global/home?safe=eth:0xaA5cD05f6B62C3af58AE9c4F3F7A2aCC2Cdc2Cc7), [Etherscan](https://etherscan.io/address/0xaA5cD05f6B62C3af58AE9c4F3F7A2aCC2Cdc2Cc7))
- Snapshot proposals:
- [[EP5.7][Social] Security Council](https://snapshot.org/#/ens.eth/proposal/0xf3a4673fe04a3ecfed4a2f066f6ced1539a5466d61630428333360b843653c54)
- [[EP5.10][Social] Confirming the ENS DAO Security Council Members](https://snapshot.org/#/ens.eth/proposal/0xa0b1bfadf6853b5b0d59d3c4d73c434fc6389339887d05de805361372eb17c3a)
- [Forum discussion](https://discuss.ens.domains/t/temp-check-enable-cancel-role-on-the-dao/19090/19)
---
# [EP4.3] [Executable] Refund Invalid .eth Names
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/16824) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/87741894125754523615596264728611635516467361279764827076788536604061840625452) |
## Abstract
This proposal initiates a transfer of 117 ETH to the Meta-Governance Working Group to facilitate the refunding of .eth names invalidated by ENSIP-15, the latest ENS name normalization standard.
## Specification
ENSIP-15 introduced a new ENS name normalization standard, resulting in the invalidation of a set of .eth names. In order to address the impact on owners of these invalidated names, refund amounts have been determined based on the following factors:
1. Remaining registration fee or renewal fee
2. Gas fees paid to register the name
3. Any premium protocol fee paid to acquire the names
These factors together have been used to calculate the refund amounts. A total of 2,973 unique addresses will be refunded a total of approximately 115 ETH.
Note: The data is from June 18, 2023 when ENSIP-15 passed. The [distributions](https://docs.google.com/spreadsheets/d/1JoLq8obUwFu_xGaXRMslAyX6nwR3njXqIc7I9sizTLU/edit?usp=sharing) and [code](https://github.com/ensdomains/normalise-refund) are both open source.
To facilitate the transfer of refunds to the affected addresses, this proposal suggests transferring the necessary ETH amount, including gas fees, to the Meta-Governance multisig. The multisig will then execute the refunds on behalf of the ENS DAO and return any leftover gas funds, if applicable.
## Transactions
| Address | Value | Function | Argument | Value |
| ------------------------------------------ | ------- | -------- | -------- | ----- |
| 0x91c32893216dE3eA0a55ABb9851f581d4503d39b | 117 ETH | | | |
---
# [EP3.2] [Executable] April 2023 Working Group Funding
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/16123) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/105292919185331921858643224173178583901390266903267892669205105842869373522526) |
## Abstract
This proposal executes all three Working Group funding requests for Q1/Q2 2023 as passed in EP 3.1.1, EP 3.1.2, and EP 3.1.3. For more detail, view the [ENS Governance docs](https://docs.ens.domains/v/governance/governance-proposals/term-3) or view the links below.
## Motivation
[**EP 3.1.1 — ENS Ecosystem Working Group**](https://docs.ens.domains/v/governance/governance-proposals/term-3/ep3.1.1-social-q1-q2-2023-funding-request-ens-ecosystem-working-group)
The ENS Ecosystem Working Group requests funding of 935,000 USDC and 254 ETH from the ENS DAO for Q1/Q2 2023.
The ENS Ecosystem Working Group is responsible for growing and improving the ENS Ecosystem by funding people and projects that are ENS-specific or ENS-centric. In line with Article III of the ENS DAO Constitution, the requested funds will be used to support projects and builders contributing to the development and improvement of the ENS protocol and the ENS ecosystem.
[**EP 3.1.2 — MetaGovernance Working Group**](https://docs.ens.domains/v/governance/governance-proposals/term-3/ep3.1.2-social-q1-q2-2023-funding-request-meta-governance-working-group)
The Meta-Governance Working Group requests funding of 364,000 USDC, 125 ETH, and 3,500 $ENS from the ENS DAO for Q1/Q2 2023.
This MetaGovernance Working Group will use this funding to support the governance processes of the ENS DAO as well as manage and build infrastructure to support the ENS DAO and Working Groups.
[**EP 3.1.3 — Public Goods Working Group**](https://docs.ens.domains/v/governance/governance-proposals/term-3/ep3.1.3-social-q1-q2-2023-funding-request-public-goods-working-group)
The Public Goods Working Group requests funding of 250,000 USDC and 50 ETH from the ENS DAO for Q1/Q2 2023.
The Public Goods Working Group will be use this funding to support projects and builders as provisioned by Article III of the ENS DAO Constitution, which provides for the funding of public goods in web3.
## Specification
1. Transfer 935,000 USDC and 254 ETH to ens-ecosystem.pod.xyz
2. Transfer 364,000 USDC, 125 ETH, and 3,500 $ENS to ens-metagov.pod.xyz
3. Transfer 250,000 USDC and 50 ETH to ens-publicgoods.pod.xyz
**Addresses:**
- 0x2686A8919Df194aA7673244549E68D42C1685d03 - ens-ecosystem.pod.xyz
- 0x91c32893216dE3eA0a55ABb9851f581d4503d39b - ens-metagov.pod.xyz
- 0xcD42b4c4D102cc22864e3A1341Bb0529c17fD87d - ens-publicgoods.pod.xyz
## Transactions
| Address | Value | Function | Argument | Value |
| ------------------------------------------ | ------- | -------- | -------- | ----------------------- |
| ens-ecosystem.pod.xyz | 254 ETH | | | |
| ens-metagov.pod.xyz | 125 ETH | | | |
| ens-publicgoods.pod.xyz | 50 ETH | | | |
| 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | | transfer | to | ens-ecosystem.pod.xyz |
| | | | value | 935000000000 |
| 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | | transfer | to | ens-metagov.pod.xyz |
| | | | value | 364000000000 |
| 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | | transfer | to | ens-publicgoods.pod.xyz |
| | | | value | 250000000000 |
| 0xc18360217d8f7ab5e7c516566761ea12ce7f9d72 | | transfer | to | ens-metagov.pod.xyz |
| | | | value | 3500000000000000000000 |
---
# [EP 5.24] [Executable] Term 5 Q4, Collective Working Group Funding Proposal
::authors
| **Status** | Rejected |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-24-executable-term-5-q4-collective-working-group-funding-proposal/19801) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/6949396467348678571876890705367894936837088742488849089767544172635343148173) |
## Description
This proposal executes all three Working Group funding requests for the October 2024 funding window as passed in [EP 5.17.1](https://discuss.ens.domains/t/5-17-1-social-funding-request-ens-meta-governance-working-group-term-5-oct-window/19677), [EP 5.17.2](https://discuss.ens.domains/t/5-17-2-social-funding-request-ens-ecosystem-working-group/19678), and [EP 5.17.3](https://discuss.ens.domains/t/5-17-3-social-funding-request-ens-public-goods-working-group/19679) as well as [EP 5.21](https://snapshot.org/#/ens.eth/proposal/0x5067725bef9cde0de0024adedb653a7a1752aefa726adf628b77048d85821a6d) and [EP 5.19](https://snapshot.org/#/ens.eth/proposal/0xfa54ff2b55f0495c96ec2d8645241bcff48ca6afe1f4925fb51f29c4667252df).
For more detail, view the [ENS Governance docs](https://basics.ensdao.org/funding-requests/funding-requests)
## Proposal Components
### 1) [Meta-governance Funding Request [EP 5.17.1]](https://discuss.ens.domains/t/5-17-1-social-funding-request-ens-meta-governance-working-group-term-5-oct-window/19677)
The Meta-governance Working Group requests funding to fulfill anticipated budgetary needs through the next formal funding window in April 2025.
| Destination | USDC | ETH | $ENS |
| :---------------------------------------------------------------------------------------------------- | :-----: | :-: | :----: |
| [ENS Meta-Gov Main Multisig](https://etherscan.io/address/0x91c32893216dE3eA0a55ABb9851f581d4503d39b) | 354,000 | 0 | 45,000 |
This amount will cover all expected expenses (including the ENS required for vesting on 5.19 and 5.21) while maintaining a 100,000 USDC prudent reserve to ensure continuity if future funding is delayed.
### 2) [Ecosystem Funding Request [EP 5.17.2]](https://discuss.ens.domains/t/5-17-2-social-funding-request-ens-ecosystem-working-group/19678)
The ENS Ecosystem Working Group requests funding to support operations through April 2025. This is the only funding request of Term 5. The working group is responsible for growing and improving the ENS Ecosystem by funding builders and projects that are ENS-specific or ENS-centric.
| Destination | USDC | ETH | $ENS |
| :----------------------------------------------------------------------------------------------------- | :-----: | :-: | :--: |
| [ENS Ecosystem Main Multisig](https://etherscan.io/address/0x2686a8919df194aa7673244549e68d42c1685d03) | 836,000 | 0 | 0 |
### 3) [Public Goods Funding Request [EP 5.17.3]](https://discuss.ens.domains/t/5-17-3-social-funding-request-ens-public-goods-working-group/19679)
The ENS Public Goods Working Group requests funding to support operations through the next funding window in April 2025. The funds requested extend current needs through to next term to ensure that next season's stewards have available funding before the next funding window.
| Destination | USDC | ETH | $ENS |
| :---------------------------------------------------------------------------------------------------- | :-----: | :-: | :--: |
| [Public Goods Main Multisig](https://etherscan.io/address/0xcD42b4c4D102cc22864e3A1341Bb0529c17fD87d) | 226,000 | 0 | 0 |
## Specification
The following transfers are to be made from the DAO treasury:
1. Transfer 354,000 USDC to the Meta-governance safe:
- Address: `0x91c32893216dE3eA0a55ABb9851f581d4503d39b`
2. Transfer 836,000 USDC to the Ecosystem safe:
- Address: `0x2686A8919Df194aA7673244549E68D42C1685d03`
3. Transfer 226,000 USDC to the Public Goods safe:
- Address: `0xcD42b4c4D102cc22864e3A1341Bb0529c17fD87d`
4. Transfer 45,000 ENS to the Meta-governance safe:
- Address: `0x91c32893216dE3eA0a55ABb9851f581d4503d39b`
#### Total transfer amount: 1,416,000 USDC and 45,000 ENS
---
# [EP 5.29] Funding request for Unruggable to build and operate a network of gateways supporting the rollout of ENSIP-19: EVM-chain Reverse Resolution
::authors
| **Status** | Rejected |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/temp-check-ep-5-29-funding-request-for-unruggable-to-build-and-operate-a-network-of-gateways-supporting-the-rollout-of-ensip-19-evm-chain-reverse-resolution/19902) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/13214174724111749338017943143826453367599509196993220699255450633508989705578) |
## Summary
We are requesting funding from the ENS DAO to build a production network of gateways. These gateways will support the rollout of reverse resolution for **Arbitrum, Base, Linea, Optimism, and Scroll.** We also plan to continue our research and development on the ENS protocol and actively contribute to the ENS ecosystem with a focus on resolving names from L2s. Our funding request focuses on infrastructure, talent acquisition and retention, and ongoing development to sustain this critical ENS infrastructure.
## Request
We are requesting **$1,200,000 USDC annually and 24,000 ENS tokens (vested over 2 years with a one year cliff).**
This request gives consideration to the feedback on our [Temp Check](https://discuss.ens.domains/t/temp-check-ep-x-x-funding-request-for-unruggable-to-build-and-operate-a-network-of-gateways-supporting-the-rollout-of-ensip-19-evm-chain-reverse-resolution/19902) on the ENS DAO forum.
## Executable Code
This proposal constitutes two streams:
- A stream of **$1,200,000 USDC** **per year** (12 months).
- A stream of **24,000 ENS** tokens over **2 years** (24 months) with a **1 year cliff** (12 months).

Both streams are controlled directly by the ENS DAO Wallet. They can be cancelled at any time with a DAO vote should Unruggable not fulfil their promises.
This calldata has been generated using thIs codebase: [](https://github.com/unruggable-labs/unruggable-stream/tree/dce44e0fc3a461f4f250c436101231e553829e03)[https://github.com/unruggable-labs/unruggable-stream/tree/3d3c49980defbab315b6e09385b22946dd9729b0](https://github.com/unruggable-labs/unruggable-stream/tree/3d3c49980defbab315b6e09385b22946dd9729b0), which generates and simulates execution of the below listed transactions.
Tenderly simulation links are listed below.
### Stream 1 - $1,200,000 USDC.
**Platform:** [Superfluid](https://www.superfluid.finance/).
Superfluid is a tried and tested platform for streaming funds. It has been used for nearly a year now for [**[EP5.2] [Executable] Commence Streams for Service Providers.**](https://www.tally.xyz/gov/ens/proposal/63865530602418424570813160277709124551851041237648860550576561576702951975816)
Initialising the Superfluid stream involves **4 transactions**:
| Description | Target Name | Target Address | Function Signature | Function Arguments | Calldata [1] | Simulation |
| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| This function approves the Super USDCx contract to spend $100,000 of USDC on behalf of the sender, the ENS DAO wallet. | USDC | [0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48](https://etherscan.io/address/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | `function approve(address spender, uint256 amount) external returns (bool)` | `["0x1BA8603DA702602A8657980e825A6DAa03Dee93a", 100000000000]` | `0x095ea7b30000000000000000000000001ba8603da702602a8657980e825a6daa03dee93a000000000000000000000000000000000000000000000000000000174876e800` | [Simulation](https://www.tdly.co/shared/simulation/7a33ba80-767d-4764-891f-b93690ad7b25) |
| This function 'upgrades' $100,000 USDC from the ENS DAO wallet/'Timelock' to USDCx. | USDCx | [0x1BA8603DA702602A8657980e825A6DAa03Dee93a](https://etherscan.io/address/0x1BA8603DA702602A8657980e825A6DAa03Dee93a) | `function upgrade(uint256 amount)` | `[100000000000]` | `0x45977d03000000000000000000000000000000000000000000000000000000174876e800` | [Simulation](https://www.tdly.co/shared/simulation/d564e4b9-3c5d-4e90-91f7-9ae78e32fbd1) |
| This function sets up the stream to the Unruggable multisig wallet. [2] | Superfluid | [0xcfA132E353cB4E398080B9700609bb008eceB125](https://etherscan.io/address/0xcfA132E353cB4E398080B9700609bb008eceB125) | `function setFlowrate(address tokenAddress, address receiverAddress, int96 amountPerSecond)` | `["0x1BA8603DA702602A8657980e825A6DAa03Dee93a", "0x64Ca550F78d6Cc711B247319CC71A04A166707Ab", 38026517538495352]` | `0x57e6aa360000000000000000000000001ba8603da702602a8657980e825a6daa03dee93a00000000000000000000000064ca550f78d6cc711b247319cc71a04a166707ab000000000000000000000000000000000000000000000000008718ea8ded5b78` | [Simulation](https://www.tdly.co/shared/simulation/725d872b-8174-4fa5-a60b-5d45eea1812f) |
| This function increases the amount of USDC (owned by the ENS DAO wallet/Timelock) that the Autowrap strategy contract is able to spend. | USDC | [0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48](https://etherscan.io/address/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) | `function increaseAllowance(address spender, uint256 increment)` | `["0x1D65c6d3AD39d454Ea8F682c49aE7744706eA96d", 1100000000000]` | `0x395093510000000000000000000000001d65c6d3ad39d454ea8f682c49ae7744706ea96d000000000000000000000000000000000000000000000000000001001d1bf800` | [Simulation](https://www.tdly.co/shared/simulation/d94d705b-0025-4500-b5d0-e4eba5221abe) |
### Stream 2 - 24,000 ENS
Platform: [Hedgey](https://hedgey.finance/)
Hedgey has been utilised by the ENS DAO for allocating delegateable ENS tokens to deserving ecosystem participants.
Initialising the Hedgey stream involves **2 transactions**:
| Description | Target Name | Target Address | Function Signature | Function Arguments | Calldata [1] | Simulation |
| --------------------------------------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Approve the `BatchPlanner` to spend 24,000 ENS tokens owned by the ENS DAO Wallet | ENS Token | [0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72](https://etherscan.io/address/0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72) | `function approve(address spender, uint256 amount) external returns (bool)` | `[ "0x3466EB008EDD8d5052446293D1a7D212cb65C646", 24000000000000000000000 ]` | `0x095ea7b30000000000000000000000003466eb008edd8d5052446293d1a7d212cb65c6460000000000000000000000000000000000000000000005150ae84a8cdf000000` | [Simulation](https://www.tdly.co/shared/simulation/82838efa-2dda-4660-abf7-991f2787388a) |
| Create the Vesting Plan. Tokens vested over 24 months, with a 12 month cliff. [3] | Hedgey Batch Planner | [0x3466EB008EDD8d5052446293D1a7D212cb65C646](https://etherscan.io/address/0x3466EB008EDD8d5052446293D1a7D212cb65C646) | `function batchVestingPlans(address locker, address token, uint256 totalAmount,(address recipient, uint256 amount, uint256 start, uint256 cliff, uint256 rate)[], uint256 period, address vestingAdmin, bool adminTransferOBO, uint8 mintType)` | `["0x1bb64AF7FE05fc69c740609267d2AbE3e119Ef82", "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", 24000000000000000000000, [["0x64Ca550F78d6Cc711B247319CC71A04A166707Ab", 24000000000000000000000, 1735065935, 1766601935, 380517503805175]], 1, "0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7", true, 4]` | `0x94d37b5a0000000000000000000000001bb64af7fe05fc69c740609267d2abe3e119ef82000000000000000000000000c18360217d8f7ab5e7c516566761ea12ce7f9d720000000000000000000000000000000000000000000005150ae84a8cdf00000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000fe89cc7abb2c4183683ab71653c4cdc9b02d44b700000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000064ca550f78d6cc711b247319cc71a04a166707ab0000000000000000000000000000000000000000000005150ae84a8cdf00000000000000000000000000000000000000000000000000000000000000676b014f00000000000000000000000000000000000000000000000000000000694c34cf00000000000000000000000000000000000000000000000000015a1422a526f7` | [Simulation](https://www.tdly.co/shared/simulation/d33f9323-8ec0-4402-a458-265b7fa546f7) |
[1] You can deep dive into this calldata at the following link: [https://ethtools.com/calldata-collections/unruggable-executable-proposal](https://ethtools.com/calldata-collections/unruggable-executable-proposal)
[2] `38026517538495352` represents $0.038.. USDC per second noting that USDC has 18 decimals and there are `31556926` seconds in a year.
[3] `period`, and `mintType` arguments are taken from the Hedgey documentation: [https://hedgey.gitbook.io/hedgey-community-docs/for-developers/technical-documentation/token-vesting/integration-and-direct-contract-interactions](https://hedgey.gitbook.io/hedgey-community-docs/for-developers/technical-documentation/token-vesting/integration-and-direct-contract-interactions)
---
# [EP5.11] [Executable] Fund the Meta-Governance Working Group (Term 5)
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep5-11-executable-proposal-fund-the-meta-governance-working-group-term-5/19358) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/76312276134880424811339391717815330375652916610982980144903695563372140531115) |
## Abstract
Meta-Governance is seeking funding to support DAO-wide operations, including Working Groups, treasury management, and governance initiatives. This request aligns with Rule 10.1.1 of the [Working Group Rules](https://docs.ens.domains/dao/wg/rules) and amendments introduced in [EP 4.8](https://docs.ens.domains/dao/proposals/4.8). This proposal will execute the funding specification according to [EP 5.9](https://snapshot.org/#/ens.eth/proposal/0x66d355555c24ed0d2fed0aee89e4fe009e2925c84144c4edc707d33e7c19e554), as amended by [EP 5.8](https://snapshot.org/#/ens.eth/proposal/0x1f328fd1fda5f3cabfdace3e521403def7ad41b0b0582e27334c135cd23c511d).
## Motivation
### [EP 5.9 — Funding Request: ENS Meta-Governance Working Group Term 5 ](https://discuss.ens.domains/t/4-4-2-social-funding-request-ens-meta-goverance-working-group/17994)
The Meta-Governance Working Group requests funding of **374,000 USDC and 150,000 ENS** from the ENS DAO treasury ([wallet.ensdao.eth](https://etherscan.io/address/0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7)). This funding will be used to support the governance processes of the ENS DAO and to manage and build infrastructure that supports the ENS DAO, its treasury, and its Working Groups.
## Specification
The following transfers are to be made:
- Transfer 374,000 USDC to [main.mg.wg.ens.eth](https://etherscan.io/address/0x91c32893216dE3eA0a55ABb9851f581d4503d39b).
- Transfer 150,000 ENS to [main.mg.wg.ens.eth](https://etherscan.io/address/0x91c32893216dE3eA0a55ABb9851f581d4503d39b).
Addresses for confirmation:
- 0x91c32893216de3ea0a55abb9851f581d4503d39b for main.mg.wg.ens.eth
---
# [EP 3.4] [Executable] Fund the Endowment (first tranche)
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/15952) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/90786656233306599444783442367171420493182391933134906270328139870999449830964) |
## Abstract
First tranche to fund the Endowment with 16,000 ETH sent from [ENS DAO](https://etherscan.io/address/0xfe89cc7abb2c4183683ab71653c4cdc9b02d44b7) to the [ENS Endowment](https://etherscan.io/address/0x4F2083f5fBede34C2714aFfb3105539775f7FE64) and an additional 150 ETH sent to the [Meta-Gov Pod](https://etherscan.io/address/0x91c32893216dE3eA0a55ABb9851f581d4503d39b) to account for karpatkey and Steakhouse monthly fees.
## Motivation
[Approximately one year ago](https://discuss.ens.domains/t/an-ens-endaoment/11756), the ENS DAO initiated a process to ensure the sustainability of ENS as a self-funded public good for the long term. The [RFP process](https://discuss.ens.domains/t/ep2-2-4-social-rfp-ens-endowment/14069) (EP2.2.4), on which [karpatkey, together with Steakhouse, made a proposal](https://discuss.ens.domains/t/updated-endaoment-proposal-karpatkey-steakhouse-financial/14799), resulted in the [selection of karpatkey](https://discuss.ens.domains/t/social-ep2-2-5-selection-of-an-ens-endowment-fund-manager/15188) (EP2.2.5).
For reference, the proposal submitted by karpatkey was for an initial size of \$52,000,000 USD (converted to 32,000 ETH for operational purposes as the Endowment will be founded in ETH and to account for the reduction of the pool according to [EP3.3](https://discuss.ens.domains/t/ep3-3-executable-sell-eth-into-usdc/15906)). The proposal included performance fees of 10% above the investment's reference currency, with $ for $-related investments and ETH for ETH-related investments, as well as an annual fee of 0.5% based on the size of the endowment, payable entirely in ETH on a monthly basis.
Karpatkey engaged in discussions with the ENS community to seek feedback on strategies and the ramp-up process, both [here](https://discuss.ens.domains/t/social-endowment-initial-conditions-required-steps/15684) and [here](https://discuss.ens.domains/t/endowment-initiation/15952). Feedback was received regarding concerns about the required time to establish trust and the need to limit executive votes for funding. Karpatkey believes that a balanced approach, such as funding 50% on day one and the remaining 50% after six months, would be a suitable tradeoff for all parties. To address questions and concerns raised, a [FAQ](https://discuss.ens.domains/t/endowment-frequently-asked-questions-faq/16228) was provided.
In the meantime, karpatkey made progress on the setup of [the Endowment safe](https://etherscan.io/address/0x4F2083f5fBede34C2714aFfb3105539775f7FE64), which has been seeded to display target positions. The necessary [managing permissions](https://docs.google.com/document/d/11xrliJIveXO6C6twYOrnGJ5QUW0Apr1LCcDY8hOfpuo/edit) have been defined and audits from [Ackee](https://github.com/gnosis/zodiac-modifier-roles/blob/main/packages/evm/docs/ZodiacModifierRolesJanuary2023abch.pdf) and [Sub 7](https://github.com/gnosis/zodiac-modifier-roles/blob/main/packages/evm/docs/ZodiacModifierRolesJanuary2023sub7.pdf) have been completed. On its part, Steakhouse has started work on providing open source [accounting and financial services](https://docs.google.com/document/d/1xS4nXx1G0QCjFS-VdG5yVmVoMa5t1q9_dFZ9N4wGSJ8/edit?usp=sharing), as well as a [monitoring Dune dashboard](https://dune.com/steakhouse/ens-steakhouse), both of which are work in progress.
If the Endowment is funded through this first tranche, karpatkey will allocate the funds as follows:
| # | Protocol | Assets | Strategy | Share per Strat | Allocated Funds | Share of Portfolio | Proj. APR | Proj. Rev | Pool TVL (MM) |
| --- | --------------------- | -------------------------------- | ------------- | --------------- | --------------- | ------------------ | --------- | -------------- | ------------------- |
| 1 | Compound v2 | DAI | USD - neutral | 50% | $5,506,971 | 20.61% | 2.21% | $121,704 | $560 |
| 2 | Compound v2 | USDC | USD - neutral | 50% | $5,506,971 | 20.61% | 2.16% | $118,951 | $637 |
| 3 | Aura Finance | wstETH - WETH | ETH - neutral | 40% | $6,282,423 | 23.51% | 10.38% | $652,116 | $208 |
| 4 | Curve | stETH - ETH | ETH - neutral | 40% | $6,282,423 | 23.51% | 5.38% | $337,994 | $1,653 |
| 5 | Stakewise/ Uniswap v3 | sETH2 - ETH Range: 1.000 - 1.006 | ETH - neutral | 20% | $3,141,212 | 11.76% | 12.77% | $401,133 | $65 |
| | **Total** | | | | **$26,720,000** | **100.00%** | **6.11%** | **$1,631,897** | |
## Specification
1. Transfer 16,000 ETH to the Endowment (0x4F2083f5fBede34C2714aFfb3105539775f7FE64)
2. Transfer 150 ETH to ens-metagov.pod.xyz (0x91c32893216dE3eA0a55ABb9851f581d4503d39b) to cover Endowment fees for Q1/Q2
Addresses:
- 0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7 - ENS DAO
- 0x4F2083f5fBede34C2714aFfb3105539775f7FE64 - endowment
- 0x91c32893216dE3eA0a55ABb9851f581d4503d39b - ens-metagov.pod.xyz
## Transactions
| Address | Value | Function | Argument | Value |
| ------------------------------------------ | ---------- | -------- | -------- | ----- |
| 0x4F2083f5fBede34C2714aFfb3105539775f7FE64 | 16,000 ETH |
| 0x91c32893216dE3eA0a55ABb9851f581d4503d39b | 150 ETH |
---
# [EP 5.5] Funding Request: ENS Public Goods Working Group Term 5 (Q1/Q2)
::authors
| **Status** | Passed |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-5-executable-funding-request-ens-public-goods-working-group-term-5-q1-q2/18885) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/48839151689001950442481252711111182244814765601408465024742109276815020082612) |
## Abstract
The ENS Public Goods Working Group requests funding to **support operations until the September 2024 funding window**.
The Public Goods working group funds projects and builders improving the Web3 ecosystems. This funding stream is authorized in [Article III](https://docs.ens.domains/dao/constitution#iii-income-funds-ens-and-other-public-goods) of the ENS DAO Constitution. This funding supports initiatives related to open-source software, tooling, research, and any practical implementations that broadly benefit a wide range of users of Ethereum and Web3.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)) and further required by [this snapshot proposal in Nov. 2023 modifying steward rules](https://snapshot.org/#/ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266). If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
The balance of the Public Goods multisig at the time of Snapshot contained 147.2k USDC and 33.5 ETH.
If passed, this proposal will transfer 450.3k USDC and 21.5 ETH from the DAO wallet to the Public Goods working group to accommodate the proposed budget below.
### Balances (March 2024)\*
| **USDC** | **ETH** | **ENS** |
| :------- | :------ | :------ |
| 147.2k | 33.5k | 200 |
\*Balances above reflect amounts at time of original proposal at time of Snapshot. Current balance information can be found at [https://enswallets.xyz](https://enswallets.xyz).
### Expected Spend (Through September 2024)\*
| **Initiative** | **USDC** | **ETH** |
| :------------------ | :--------- | :------ |
| Large Grants | 387.5k | 0 |
| Small Grants | 0 | 50 |
| Bounties | 45k | 0 |
| Events + Hackathons | 115k | 0 |
| Discretionary | 50k | 5 |
| **Total** | **597.5k** | **55** |
\*Multisig contains 200 ENS that has no planned use. This may be transferred back to the DAO wallet during the term.
---
## Description of Initiatives
### Large Grants
Grants up to 50k USDC with applications accepted on a rolling basis throughout the year-long term. Large Grants will resume in Q2. With five grantees completing milestones from last term, the budget includes remaining payouts yet to be disbursed. We plan to add at least two more grantees during Q2 while piloting new grants management software. In the second half of the year, we will run another full-size round supporting up to 10 grantees at a time with a 200k USD total prize pool.
### Small Grants
Multiple micro-grants voted on by the community. Small Grants will resume during the first half of the year shortly after ETHDenver. We have added the amount expected to spend through the end of the year with no increase from last term. This is approximately 12.5 ETH per quarter. With market fluctuations, stewards may right-size and lower the amounts distributed during round.
### Events and Hackathons
The working group will support Public Goods events and hackathons. Funds have included expenses related to the funding of hackathons, events, and related participation in events (judging, panels, speaking) where necessary.
The current earmarked events are:
- [ETHGuatemala](https://ethereum.gt/)
- [EthLatam](https://ethlatam.org/)
- [ETHGlobal London](https://ethglobal.com/events/london2024)
- [ETHCanal](https://www.ethcanal.xyz/)
- [ETHCC](https://ethcc.io/)
- [ETHGlobal Brussels](https://ethglobal.com/events/brussels)
- [DAO Tokyo](https://dao-tokyo.xyz/)
- [ETHGlobal San Francisco](https://ethglobal.com/events/sanfrancisco2024)
- [ETHGlobal Bangkok](https://ethglobal.com/events/bangkok)
- [Devcon](https://devcon.org/en/)
This list is not guaranteed as several events are still in the planning stages. The PG stewards will continuously assess opportunities to expand the public goods conversation and collaborations.
### Discretionary
The funds in this initiative are reserved for additional grant opportunities and expenses that arise during the term. Spending on this initiative is at the discretion of the working group stewards.
---
# [EP3.6] [Social] Election of new ENS Foundation director
::authors
| **Status** | Passed (elected: Alex Van de Sande) |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/17008) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x3c6b192eb2e990d74125d82ef886a1ec9e7373f4f768ed5b4adc23a03c26d649) |
## Abstract
This is a vote to elect a new director of the ENS Foundation.
## Motivation
Brantly Millegan has [communicated](https://discuss.ens.domains/t/resignation-from-the-ens-foundation-effective-upon-the-election-of-a-replacement/16676) that he would like to resign as a director of the ENS Foundation. Brantly's resignation will take effect following the appointment of a new director by the DAO.
The DAO may appoint a new director of the ENS Foundation pursuant to Article 15 of the Articles of Association of the ENS Foundation.
## Specification
The DAO will vote to appoint one new director of the ENS Foundation.
Two candidates are seeking election after successfully completing the requirements outlined in the ['Call for Nominees' post](https://discuss.ens.domains/t/call-for-nominees-ens-foundation-director/16858) on the ENS Governance forum.
The candidates seeking election are:
1. Sean Murray
2. Alex Van de Sande
The candidate with the highest number of votes above quorum will be appointed as a director of the ENS Foundation following the completion of mandatory KYC checks.
### Voting
This vote is a single choice vote. You may vote for one of the following options:
1. Sean Murray
2. Alex Van de Sande
3. None of the above
4. Abstain
By voting for a named candidate, you are voting in favor of appointing that candidate as a director of the ENS Foundation.
## Candidates
### Sean Murray
**Twitter**: [@financeguy74](https://twitter.com/financeguy74) \
**Are you 18 years of age or older?** Yes \
**Are you a U.S citizen or permanent resident?** Yes \
**Do you live in the U.S or one of its territories?** Yes
**Why should you be appointed as a director of the ENS Foundation?** \
I am capable of fulfilling the required duties but also beyond that I would use it as a platform to increase mainstream ENS adoption.
**How would you handle a request from the SEC for information about the ENS Foundation and/or the $ENS token?** \
This is an incomplete question because it does not specify who "you" is. If an SEC inquiry is addressed only to the Foundation itself, the correct answer is that the Foundation retain counsel and that counsel manage the inquiry. Should the definition of "you" be more broad, then any rational person should find my conditions stated below rational, reasonable, and necessary. They should be put in place regardless of who is chosen.
**Is there any other information you would like delegates to know about you or your nomination?** \
Yes, if by chance I am selected, my acceptance of the role is conditional on:
- The DAO authorizing the Foundation to set up D\&O insurance;
- The DAO authorizing the Foundation to compensate directors.
### Alex Van de Sande
**Twitter**: [@avsa](https://twitter.com/avsa) \
**Are you 18 years of age or older?** Yes \
**Are you a U.S citizen or permanent resident?** No \
**Do you live in the U.S or one of its territories?** No
**Why should you be appointed as a director of the ENS Foundation?** \
I won't lie: I believe this role is purely ceremonial and believe that as long as everything is working as intended, Directors should do nothing but rubberstamp what the DAO approves. But as a symbol, I do think it's important for the Directors not to be all from ENS Labs, nor for them to be in any jurisdiction that is not welcome to Crypto - and at this moment, the US is such jurisdiction.
So that's it, that's the whole of my pitch: I do not work for ENS labs, I do not respond to them, nor do I am subject to US jurisdiction. I also have been in this space for the last 10 years and you can expect me to be around in the next 10 years.
**How would you handle a request from the SEC for information about the ENS Foundation and/or the $ENS token?** \
I am not not subject to American laws, but that doesn't mean I intend to break any of them, as I still intend to keep the privilege of landing in US soil. I still believe the US to be a reasonable thriving democracy that is just right now a bit on the wrong side of the pendulum. I also believe that the ENS airdrop was a great precedent on how to do token distribution to the community: no "investor" ever received it in the airdrop, because there were no investors in ENS before the airdrop. All that received them were either involved in the community or the development.
If I ever received a request from the SEC of any other legal institutions from anywhere I would take the reasonable route: talk to my lawyer, open it with as much as possible with the community and, in an extreme case in which I believed I the requests would hurt ENS and I couldn't be open to talk about it, I would resign from the foundation.
**Is there any other information you would like delegates to know about you or your nomination?** \
I am not Dutch either, despite a lot of people seemingly believing it so.
---
# [EP3.1.1] [Social] Q1/Q2 2023 Funding Request: ENS Ecosystem Working Group
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/15938) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0x5788bf0f52ce82a1d3f7750a80f3001671ded49e4e0239dbbafd154275c78f8b) |
## Abstract
The ENS Ecosystem Working Group requests funding of 935,000 USDC and 254 ETH from the ENS DAO for Q1/Q2 2023.
The ENS Ecosystem Working Group is responsible for growing and improving the ENS Ecosystem by funding people and projects that are ENS-specific or ENS-centric. In line with Article III of the ENS DAO Constitution, the requested funds will be used to support projects and builders contributing to the development and improvement of the ENS protocol and the ENS ecosystem.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)). If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
| | USDC | ETH | $ENS |
| ---------------------- | :-----: | :-: | :--: |
| ENS Ecosystem Multisig | 935,000 | 254 | - |
## Description
**ENS Ecosystem Multisig Balance**
The ENS Ecosystem Working Group is requesting 935,000 USDC, 254 ETH, and 0 $ENS.
The ENS Ecosystem Working Group currently has 879,982 USDC, 110 ETH, and 40,936 $ENS in its multisigs.
If this funding request is approved, the ENS Ecosystem Working Group multisigs will have a total of 1,814,982 USDC, 364 ETH, and 40,936 $ENS.
| | USDC | ETH | ENS |
| --------------------------------- | :-----------: | :-----: | :--------: |
| Carried Forward (from Q3/Q4 2022) | 879,982 | 110 | 40,936 |
| Requested (for Q1/Q2 2023) | 935,000 | 254 | 0 |
| **Total Balance** | **1,814,982** | **364** | **40,936** |
**ENS Ecosystem Multisig Allocations for Q1/Q2 2023 (with funding request approved)**
The table below shows the total allocations for the ENS Ecosystem Working Group multisig, and related multisigs, with funds carried forward from last term (Q3/Q4 2022) along with funds requested in this proposal for this term (Q1/Q2 2023).
| | USDC | ETH | $ENS |
| ----------------- | :-----------: | :-----: | :--------: |
| Hackathons | 235,000 | 20 | - |
| Support | 178,000 | 20 | 1,250 |
| Grants | 220,580 | 30 | - |
| Builders | 250,000 | 39 | 2,000 |
| IRL | 84,256 | 10 | 1,000 |
| Merch | 50,967 | 5 | 500 |
| ENS Fairy | 50,000 | 174 | - |
| ENS Fellowship | 60,000 | - | - |
| Layer 2 | 150,000 | - | - |
| Normalization | 50,000 | - | - |
| ETH.Limo | 85,000 | 10 | - |
| Bug Bounty\*\* | 240,000 | - | - |
| Discretionary | 161,179 | 56 | 36,186\*\* |
| **Total Balance** | **1,814,982** | **364** | **40,936** |
\*\* The USDC in the Bug Bounty and the $ENS held by the ENS Ecosystem Working Group multisig is available if needed and will likely be carried forward into future terms.
**Allocation of Requested Funds**
The 935,000 USDC, 254 ETH, and 0 $ENS of funds requested in this proposal will be allocated to the following initiatives/outcomes.
| | USDC | ETH | $ENS |
| ------------------------- | :---------: | :-----: | :---: |
| Hackathons | 230,000 | 10 | - |
| Support | 90,000 | 5 | - |
| Grants | 105,000 | 30 | - |
| Builders | 100,000 | 15 | - |
| IRL | 45,000 | 5 | - |
| Merch | 40,000 | 5 | - |
| ENS Fairy | 50,000 | 174 | - |
| ENS Fellowship | 60,000 | - | - |
| Layer 2 | 105,000 | - | - |
| Normalization | 40,000 | - | - |
| ETH.Limo | 70,000 | 10 | - |
| **Total Requested Funds** | **935,000** | **254** | **-** |
**Description of Initiatives/Pods**
| Initiative/Pod | Description | Multisig Signer or Lead Singer |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------: |
| Hackathons | Sponsorship costs and prize money for hackathons and conferences | luc.computer |
| Support | Support mods for social platforms, technical and non-technical educational and archival content | validator.eth |
| Grants | Small grants funding for ENS proposals on ensgrants.xyz + evergreen integration bounties | Ecosystem stewards |
| Builders | Support for builders shipping code that improves ENS | Ecosystem stewards |
| IRL | Funding In Real Life events that coincide with the existing Ethereum event schedule + funding IRL community meetups | limes.eth |
| Merch | Subsidizing the cost of creating and shipping physical ENS merchandise including shirts, hats, and pins | Ecosystem stewards |
| ENS Fairy | Funds continued development of ensfairy.xyz + acquisition by validator.eth of names on temporary premium that can be utilized to grow ENS adoption + funding the ENS fairy registration bot to gift .eth names to IRL community members and builders | validator.eth |
| ENS Fellowship | Supports exceptional developers actively creating and contributing to the ENS ecosystem with a 6 month Fellowship | Ecosystem stewards |
| Layer 2 | Development of ENS Layer 2 solutions | Ecosystem stewards |
| Discretionary | Funds in the Ecosystem Working Group multisig that are utilized at the discretion of stewards | Ecosystem stewards |
This proposal was prepared by Slobo.eth, lead steward of the ENS Ecosystem Working Group.
---
# [EP2.2.4] [Social] ENS Endaoment RFP
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep2-2-4-social-rfp-ens-endowment/14069) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0x4a1aedbd9d22295f358dc4028b5a3f0a602bb5f1089dabdc2b63bf2bcce45834) |
## Overview
The DAO is seeking a fund manager to manage an endowment fund. This fund will be established from some combination of current treasury and ongoing revenue, and will exist to insulate the DAO from economic fluctuations, ensuring it can continue its core operations regardless of the wider economic outlook.
Funds will be drawn from the DAO's general funds, currently held in ETH and USDC, as well as from ongoing DAO revenue until the fund reaches its target size. Proposals should outline how much of the DAO's current funds the intend to appropriate, as well as what proportion of ongoing revenue will be required, and for how long.
The DAO's accounting basis divides assets into earned and unearned income. Unearned income corresponds to funds paid for registrations and renewals that are future-dated; for example, if a user spends $100 to register a name for 20 years, after a year $5 of that will be counted as earned income and the remainder as unearned income.
Unearned income may be incorporated into the fund, but must be risk-neutral with regard to ETH. Earned income should be risk-netural with regard to USDC.
The DAO's current balances can be viewed [here](https://datastudio.google.com/reporting/8785928a-71d5-4b17-9fea-fe1c937b064f); as of this writing they are:
- 13,069 ETH (\~$20M) in earned income
- $3,817,067 USDC
- 18,184 ETH (\~$20M) in unearned income
## Scope of Work
1. Expand the endowment structure specified in the accepted proposal into an executable form.
2. Work with the ENS Meta-Governance stewards to refine the proposal.
3. Implement the proposal onchain.
4. Administer and adjust the endowment as necessary on an ongoing basis.
## Selection Criteria
The endowment will need to be able to sustain annual withdrawals of at least $4M USDC without a long-term reduction in principal.
Proposals should include, at a minimum:
1. Intended high-level fund allocation of the endowment.
2. Initial and target size for the endowment fund, including the proportion of funds sourced from earned and unearned income.
3. How incoming revenues and income will be assigned to the fund.
4. Specific information on how the fund will be managed, and what steps will be taken to minimise custodial risk.
5. A fee schedule for the fund manager.
6. A detailed description of the reporting structures that will be put in place.
Priority will be given to proposals that:
- Minimise custodial risk by using onchain mechanisms that allow administration of the funds without the fund administrator having custody of the funds.
- Lay out a coherent strategy for how investment returns can be maintained in the short/medium/long term.
- Minimise capital risks relative to the expected returns.
- Define clear mechanisms for assessment and adjustment to changing external and internal conditions in order to continue to meet the endowment's objectives.
- Offer a high rate of return proportional to risk.
- Define a clear reporting structure for frequent and comprehensive reporting of the fund's status to the DAO.
## Timeline
| | |
| --------------------- | ------------------------------ |
| **Submission Period** | September 15 - October 10 2022 |
| **Approval Period** | October 10 - November 7 2022 |
## RFP Manager
The Meta-Governance WG stewards are the RFP managers for this RFP.
Proposals can be submitted via forum DM to the stewards. They will be held in confidence until the end of the submission period, at which point all applicants will be invited to post their proposals publicly for feedback on the forum.
Once a winning proposal is selected by the RFP managers, it will be announced and posted publicly.
## Budget
The endowment is expected to be funded out of a combination of capital and performance fees as proposed by the fund manager.
---
# [EP4.5] [Executable] Endowment permissions to karpatkey - Update 3
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/18036) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/15706104363492914432572227540113855373051896881975394006732444538096386655538) |
## Abstract
This proposal introduces new actions and strategies to the Endowment with the aim of enhancing diversification and adapting to current market conditions. Notable additions include ETH-neutral strategies involving Liquid Staking Protocols and established Money Markets.
## Motivation
Following the successful approval of [E.P. 4.2](https://www.tally.xyz/gov/ens/proposal/10686228418271748393758532071249002330319730525037728746406757788787068261444), the second tranche of the Endowment was funded with 16,000 ETH. Community feedback during the E.P. 4.2 voting window indicated a desire to reduce exposure to Lido due to concerns about centralization risks in the network. While diversifying ETH-neutral holdings was already underway, the need for further diversification and divestment from Lido became clear during [community discussions](https://discuss.ens.domains/t/4-2-executable-fund-the-endowment-second-tranche/17743) and the last [Meta-gov call](https://discuss.ens.domains/t/metagov-working-group-weekly-meeting-11am-et-tuesday/15981/43#h-2-endowment-discussion-karpatkey-steakhouse-10) before the vote's closure. Consequently, karpatkey and @steakhouse proposed a 20% cap on Lido's maximum allocation within the ETH-neutral portfolio, set to be achieved by month-end. This proposal's goal is to introduce new strategies for deploying the remaining 80% of the funds as well as other minor maintenance actions.
## Specification
Permissions to be added in this proposal:
1. Deposit ETH on Compound v3;
2. Deposit ETH or WETH on AAVE v3;
3. Deposit ETH or WETH on Spark Protocol;
4. Stake (and unstake) ETH on Stader;
5. Stake (and unstake) ETH on Ankr;
6. Removal of CowSwap permissions;
7. Removal of SushiSwap permissions;
8. Addition of an alternative getReward() for Aura pools;
9. Swaps:
1. rETH <> WETH on Balancer;
2. rETH <> WETH on Uniswap v3;
3. ankrETH <> wstETH on Balancer;
4. ETHx <> WETH on Balancer;
5. ankrETH <> ETH on Curve;
6. ETHx <> WETH on Pancake Swap
### Auditing Process
We are releasing an updated version of the ["Preset Permissions - ENS Endowment"](https://docs.google.com/document/d/1Ker_TkBJV0xmQ9Li9HB-vtdlpx1vEeVEQwpIH6WoK0o/edit?usp=sharing) document, highlighting all permissions granted to karpatkey, with proposed additions marked in green and revocations in red. We encourage community members with technical expertise to review and provide feedback on the preset update [payload](https://gist.github.com/santinomics/5b43dee4839d74e2c593ac9e7c7d1d3d).
In the auditing realm, significant progress has been made, with a [new version](https://zodiac-roles-1h9v0miw9-gnosis-guild.vercel.app/gor%3A0x74F819Fa1D95B57a15ECDEf9ce5c779C1bD6cc8A/roles/test-role/diff/4Iq1jdNbLbCBKmLAKciGaDQANiHStkCqFvJJ5KWQc) of the Zodiac Roles Modifier app developed by Gnosis Guild. When fully operational, this app will allow users to input a payload and check the before-and-after status of permissions presets, enhancing the auditing process.
Furthermore, we're actively engaged in collaborative efforts with potential partners to create a user-friendly audit report, enhancing openness for all stakeholders involved in the process. In our commitment to transparency, we're taking an additional step by offering a [self-audit report](https://github.com/karpatkey/gists/blob/main/Self%20Audit_%20%5BENS%5D%20-%20PUR%20%233%20-%20New%20ETH-Neutral%20Strategies.md). This report sheds light on our internal procedures for assessing proposed permissions and changes, providing further insight into our practices.
---
# [EP1.1] [Executable] Set the temporary premium start price to $100,000
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/9336) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/65967822514040846992464797266243157509206510058326665394616765053720727454968) |
_Note: This was previously numbered EP5._
## Summary
Increases the start price for the temporary premium added when names expire from $2,000 to $100,000.
## Abstract
When a name expires, it goes through a 90 day grace period, after which it becomes available for general registration. To prevent it becoming a race to register, a 'temporary premium' is attached, starting at $2000 and diminishing to 0 over 28 days. Done properly this results in a dutch auction for the name.
When the premium was set, $2000 was roughly the 98th percentile of all auction prices for the short name auction, and seemed like a reasonable starting price. Today, we're seeing multiple reports of names, particularly 3 character names, being snapped up by bots the moment they become available at the maximum premium.
Long-term we can improve the mechanism by setting the initial premium based on the base price of the name (so 3 letter names start off more expensive than 5+ letter names), and by introducing a nonlinear pricing curve (for example, having the price diminish by 1% per hour for 28 days). In the short term, however, I'd like to propose setting the initial premium to a much higher value, which we can do with a simple transaction from the DAO account.
Research of current trends shows:
- 2,798 names have been bought during the temporary premium period so far.
- Of these, 340 (12%) were purchased the moment they became available.
- Registrations of names during the temporary premium period have been on the increase since the DAO launched in November.
- 12 names have been purchased at a premium and flipped on OpenSea; the most valuable of these, punk.eth, went for $85,636.
Accordingly, I propose increasing the temporary premium from its current value of $2,000 to a new value of $100,000. This will ensure that the start price exceeds the secondary market resale value of almost all released domains, and corresponds to a rate of decrease of just under $150/hr, meaning that less valuable domains will still have a multi-hour period during which people can purchase them before the premium expires.
In parallel, work should be initiated on a new nonlinear premium model, so that high initial premiums can be combined with a slow decrease towards 0, allowing both high and low value names to be bid on fairly.
## Specification
1. Deploy a new instance of the `LinearPremiumPriceOracle` with the initial premium set to 100,000 USD and the period set to 28 days, with other pricing variables identical to the current oracle.
2. Call `setPriceOracle` on `controller.ens.eth`, passing in the address of the new price oracle contract.
## Code
TBD
---
# [4.4.3] [Social] Funding Request: Public Goods Working Group
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/17996) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x0a7bec3cd182dadbd043e77cf7a610a6e33c5228fabe407cb89c632b578b83a9) |
## Abstract
The ENS Public Goods Working Group requests funding of the below to **support operations until the March 2024 funding window**. The Working Group intends to refrain from requesting funds in the upcoming January 2024 Funding Window.
The Public Goods Working Group is responsible for supporting projects and builders as provisioned by Article III of the ENS DAO Constitution, which provides direction for the DAO to fund public goods that benefit the broader Ethereum and Web3 Ecosystems.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)). If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
This specification is the amount requested from the DAO treasury to the Public Goods Multisig to fulfil anticipated budgetary needs through March 2024.
| | USDC | ETH| $ENS |
| ---- | :----: | :----: | :----:|
| Amount Requested from DAO| 218,204 | 35 | 0 |
---
## Description
### Current Public Goods Wallet Balance
(Values expected as of October 20th, 2023 - Use hyperlinked wallet names to see current balances.)
| Address | USDC | ETH | $ENS |
| ---------------------------------------------------------------------------------------------------- | --- | ------- | ----- |
| [ens-publicgoods.pod.xyz](0xcD42b4c4D102cc22864e3A1341Bb0529c17fD87d) | 184,796 | 25 | 1,352 |
---
## Expenditures
### Expected Expenses through March 2024
| | USDC | ETH | $ENS |
| ---------------------------- | ----------- | ------ | --------- |
| Large Grants | 300,000 | - | - |
| Small Grants | - | 45 | - |
| Events & Hackathons | 50,000 | - | - |
| Discretionary | 53,000 | 15 | - |
| **Total Projected Expenses** | **403,000** | **60** | **1,352** |
### Description of Initiatives/Pods
#### Large Grants
Grants up to 50k USDC with applications accepted on a rolling basis throughout the term. Large Grants are resuming in Q4 2023 and should be allocated through Q1 2024.
#### Small Grants
Multiple micro-grants voted on by the community. Small Grants is resuming this term with modifications to the voting format. Three rounds, totalling 15 ETH each, are expected to occur until March 2023.
#### Events and Hackathons
The working group will provide support to Public Goods events and hackathons. Following discussions around hackathon opportunities, we want to ensure we have availing funding if requests arise.
#### Discretionary
The funds in this category are reserved for unforeseen grants and unexpected expenses for the term. Spending in this category is at the discretion of the working group stewards and may include grants not listed but in alignment with the purpose of this working group.
---
This proposal was prepared by coltron.eth, lead steward of the ENS Public Goods Working Group.
---
# [EP 6.4] [Social] Service Provider Season 2 Vote Amendment Proposal
::authors
| **Status** | Superseded by [EP 6.5](/dao/proposals/6.5) |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/20526/57) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xf0c3a2fe4bd085ea74a072cafb830aaadb4830b557a3d122eab36058a17c1860) |
## Abstract
EP 6.3 was passed with a budget of $4.5M for 2025 on the 25th of February and pertains to Service Provider budgets and allocation mechanisms for 2025. After broad discussion between delegates, working groups and service providers, a proposed change to the voting process is now being presented for vote.
## Vote
This is a proposed amendment to the evaluation criteria for Service Providers. On April 1st there was an Delegate All Hands meeting in which many delegates expressed the desire to be able to fine tune their vote in order to express preference over not only the teams, but also their respective budgets. This was followed by extensive discussion between delegates, working groups and Service Providers, leading to the below amendment:
_The goal here is to propose a new rule change while keeping the same properties as having a single budget be decided in one simultaneous vote._
### Current Evaluation Process, as voted on snapshot
> **Evaluation Process**
>
> Projects are assessed in ranked order:
>
> - If \"None Below\" is reached, evaluation stops.
> - If the candidate has been part of the Service provider program for at least a year AND if the extended budget fits within the remaining two-year stream budget, assign to the two-year stream . Subtract the extended budget from the two-year stream budget.
> - Assign to the one-year stream if:
> - The extended budget fits the one-year budget. Subtract its extended budget from the one-year stream.
> - OR if the basic budget fits the one-year budget, subtract the its basic budget from the one-year stream.
> - If none of these conditions are met, the project is eliminated.
### New proposed rule amendment
The vote will present both extended and basic budgets as separate options and a given voter can pick _either_ budget to rank their candidates. They do not need to rank both budget options separately, as they are considered the same candidate.
The rank of each candidate will be the rank of it's highest ranked budget option, according to a Copeland methodology (using average support as a tiebreaker). Then a pairwise comparison will be made between the two budget options and the preferred one will be set as its selected budget.
### Vote Processing Algorithm
1. **Votes Preprocessing**:
- For providers with both basic and extended budget options, the algorithm enforces the lowest option to be ranked immediately after the highest option (of the same provider).
- If a provider has only one budget option, no special enforcement is needed for that provider.
- This grouping ensures accurate pairwise comparisons between different between different providers and then budget options from the same provider.
2. **Pairwise Comparisons (copeland)**:
- For each pair of candidates (provider), we calculate the total voting power supporting each over the other.
- A candidate wins a head-to-head matchup if the total voting power ranking them higher exceeds that of their opponent.
- Each win contributes 1 point to a candidate's Copeland score.
- The pairwise comparison between basic and extended must also be stored, for defining the preference on the budget.
3. **\"None Below\" Handling**:
- The \"None Below\" option serves as a cutoff point in a voter's ranking.
- Candidates ranked above \"None Below\" are considered ranked.
- Candidates ranked below \"None Below\" are considered unranked by that voter.
- A ranked candidate always wins against an unranked candidate in pairwise comparisons.
4. **Scoring and Ranking**:
- Candidates are ranked by their Copeland score (descending), with average support as a tiebreaker.
### Allocation Process
1. **Budget Type Determination**:
- Each provider's budget (basic or extended) is determined by their internal head-to-head match result.
2. **Stream Allocation**:
- Candidates are processed in Copeland ranking order.
- Candidates that are in top 5 and were selected in SPP1, are elegible for the 2-year stream.
- All other candidates receive allocations from the 1-year stream.
- From top to bottom, try to fit projects in the 2 year stream budget, and then on the 1-year stream budget using the standard knapsack algorithm, stopping once budgets are exhausted or None Below is reached.
- If a service providers extended scope got a majority vote over basic scope and the extended scope doesn't fit into the remaining 1-year budget but the basic budget does, then the given service provider's basic budget is included.
- If a candidate is ranked below \"None Below\", they're rejected regardless of budget availability.
3. **Budget Transfer Mechanism**:
- After processing the top 5 candidates, any remaining 2-year budget transfers to the 1-year stream. Final 1-year budget = (Initial 1-year budget) + (Leftover 2-year budget).
4. **Rejection Criteria**:
- A candidate is rejected if:
- They're ranked below the \"None Below\" option
- There's insufficient budget
## Updated Timelines
The initial proposal stated a submission deadline of March 31st, and vote to begin _soon_ after that.
This vote will be conducted over the next 5 days. Then, if the vote is successful MetaGov will be interfacing with voting UI teams to ensure sufficient testing and timelines before the final SPP vote. There will be a minimum of 3 days between this proposal closing and the start of the final vote, to allow Service Providers to update their proposals if necessary.
## Conclusion
If this amendment proposal passes, the MetaGov working group, delegates and governance UI providers will enact the updated proposal process.
We would like to thank everyone who has taken the time to be involved in this discussion and have been blown away by the level of engagement & productivity throughout.
---
# [EP2.1] [Executable] Funding True Names Ltd
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/13391) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/112764562576314516994943312429834673309292069549953740415731020720942627228986) |
_Note: This was previously numbered EP14._
## Abstract
True Names Ltd ("TNL") developed the Ethereum Name Service ("ENS") protocol, continues to manage the development of the ENS Protocol and solely focuses on this project. Incubated at the Ethereum Foundation in 2017, TNL spun out in 2018 with the charge of designing and deploying THE next generation naming service. TNL is now a growing team of web3 enthusiasts from across the globe working together to support the ENS Ecosystem that now has a passionate community that has registered over 1 million ENS names and uses more than 500 integrations.
In 2021, TNL initiated the creation of the ENS DAO with the goal of 1) furthering the development of the ENS Protocol and 2) funding public goods projects.
In consideration of the work completed thus far this calendar year and the work in the months and years to come, per Article III of the ENS Constitution, True Names Ltd respectfully requests an evergreen grant stream that will allow the organization to continue the development and improvement of the ENS Protocol. For Calendar Year 2022, this request will amount to $4,197,500 USDC which is equivalent to a daily stream of $11,500 USDC. In Q1 of each year, TNL or the ENS Dao may make requests to alter and/or terminate this evergreen grant stream.
## Specification
We request that the ENS DAO approve a daily grant of $11,500 USDC to True Names Ltd, backdated to January 1st, 2022.
This will be accomplished by approving a dedicated token streaming contract at `0xB1377e4f32e6746444970823D5506F98f5A04201` to spend USDC on behalf of the DAO.
---
# [EP1.5] [Executable] Change to Exponential Premium Price Oracle
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/11320) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/107166664722174233740232174220463354481004128961821575530758100250899337476509) |
_Note: This was previously numbered EP9._
## Abstract
In the past we deployed the Linear Premium Oracle as a way to create a distribution mechanism that did not involve gas auctions and bots. This was largely successful and those who wanted a recently expired name could participate in the dutch auction and not have to compete on gas or with bots. Recently with the popularity of ENS increasing, the demand and the price people are willing to pay for these premium names has increased. In response to this TNL quickly drafted a [short-term solution](https://discuss.ens.domains/t/ep5-executable-set-the-temporary-premium-start-price-to-100-000/9336) to raise the premium to 100k, which we felt was the upper limit for what a linear price decay curve could handle.
There are a couple reasons for this:
1. On a linear curve, if the price is too high the price decreases too fast and the UX is bad for a user who wants an exact price (especially at the lower end of the curve)
2. If you extend the time period out, the premium lasts for too long. E.g. If we made it 1 million USD and we wanted a similar price decay speed as 100k, we would need to run it for 10 months, which seems unreasonable.
We can see from the data below, even with the new 100k premium, we have already had a 5-7 domains go for maximum, or close to maximum premium. If a domain sells for the actual premium, it means the dutch auction is not doing its job and so we need to deploy a long-term solution for dealing with premium pricing.
| Row | Label | Event Timestamp | Premium |
| --- | ----- | ----------------------- | ------------------ |
| 1 | bbc | 2022-01-30 17:46:03 UTC | 100230.75321837279 |
| 2 | mets | 2022-02-04 17:16:22 UTC | 100082.49847319399 |
| 3 | fbi | 2022-02-05 06:02:31 UTC | 99894.00632472485 |
| 4 | fly | 2022-02-04 18:49:00 UTC | 99747.22640247621 |
| 5 | ups | 2022-02-05 07:46:24 UTC | 98822.14747808539 |
| 6 | dog | 2022-02-06 16:19:05 UTC | 92950.09208752771 |
| 7 | ubs | 2022-02-01 15:31:35 UTC | 89633.15081063367 |
| 8 | ubi | 2022-02-19 17:06:17 UTC | 72161.56328771653 |
| 9 | punks | 2022-02-16 00:15:44 UTC | 59153.166146336 |
| 10 | omg | 2022-02-24 16:05:57 UTC | 33214.42499419019 |
The long-term solution would be to change the actual curve to something that could start at a very high price, would decrease rapidly at the beginning and slow down at the end so you have better UX for users. And therefore this proposal is to deploy an exponential price curve, that does exactly this. This would allow fairer bidding on both high and low priced names.
## Contract Code
https://github.com/ensdomains/ens-contracts/blob/master/contracts/ethregistrar/ExponentialPremiumPriceOracle.sol
## Specification
Call `setPriceOracle` on `controller.ens.eth`, passing in the address of the deployed `ExponentialPremiumPriceOracle` (TBD).
---
# [EP1.2.1] [Social] Removal of Brantly Millegan as Director of the ENS Foundation
::authors
| **Status** | Rejected |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/11092) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0xa9a2dc5a52ea54b478c4c5fea88540622dff7ad5000f8d146dff482c6e6b6055) |
_Note: This was previously numbered EP6.1._
## Summary
The removal of Brantly Millegan as Director of The ENS Foundation (the "Foundation Company.").
Adapted from the original [EP6](https://discuss.ens.domains/t/ep6-cancelled-see-ep-6-1-and-6-2) Proposal.
## Abstract
This social proposal puts forth a vote to enact the possible removal of Brantly Millegan as the Director of the Foundation Company.
This action is justifiable under [Clause 15 of the Articles of Association of The ENS Foundation paragraph titled 'Directors'](https://464911102-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVAbPWWNVJRvupIW5EOre%2Fuploads%2Fgit-blob-4b4ba71c3fe2a3146e9b1402d03ecb69d89c0913%2FM%26A%20-%20Incorp%20-%20The%20ENS%20Foundation%20-%2026%20October%202021.pdf?alt=media#page=9) which states that:
> The Council has the power, exercisable by notice to the Foundation Company, to appoint or remove one or more directors of the Foundation Company.
## Motivation
A timeline of events that led to this proposal has been posted by Community WG Stewards in another post [here](https://discuss.ens.domains/t/ens-dao-next-steps-re-brantly/10424/15) and quoted below:
> In May 2016, Brantly Millegan tweeted, "Homosexual acts are evil. Transgenderism doesn't exist. Abortion is murder. Contraception is a perversion. So is masturbation and porn."
>
> On February 5th, 2022, brantly.eth, well-known as a prominent representative of the ENS protocol, [defended these views in a publicly held Twitter space](https://www.dropbox.com/s/ljrp0vjibawuufr/Brantly%20ENS.mp4?dl=0).
>
> Irrespective of one's personal beliefs, we, as ENS DAO Community Stewards, must set the example for inclusivity and must divide our community.
>
> Propagating rhetoric that is viewed as hateful and discriminatory is not conduct conducive to the role as a Community Steward. This conduct will not be tolerated despite one's contributions to the protocol.
Through this Social Proposal, the DAO shall decide whether Brantly Millegan is deemed capable, or otherwise, of continuing his role as the Director of the Foundation Company, and to appoint a suitable replacement if he is deemed incapable.
## Specification
The removal of Brantly Millegan's directorship of the Foundation Company will be defined by a simple "Yes" or "No" vote with the option to abstain.
**Draft Snapshot Vote**
> Should Brantly Millegan be removed from directorship of the Foundation Company? **Choice 1:** Yes **Choice 2:** No **Choice 3:** Abstain
**If Majority "Yes:"** Brantly Millegan is voted to be removed and the results of [**\[EP6.2\] \[Social\] Election of a new Director of the ENS Foundation**](https://discuss.ens.domains/t/ep6-2-social-replacement-of-brantly-millegan-as-director-of-the-ens-foundation/11093) regarding Brantly Millegan's successor shall be effective immediately.
**If Majority "No:"** Brantly Millegan is voted to remain in a directorship position over the Foundation Company, the results of [**\[EP6.2\] \[Social\] Election of a new Director of the ENS Foundation**](https://discuss.ens.domains/t/ep6-2-social-replacement-of-brantly-millegan-as-director-of-the-ens-foundation/11093) shall be null and void.
## Notice to The ENS Foundation
In line with this proposal, a formal notice is served to the ENS Foundation as follows:
_"Pursuant to Article 15 of the Articles of Association, the council, hereby, gives notice to the Foundation Company of a vote to remove Brantly Millegan as a director of the Foundation Company._
_Whereas, the council undertook a vote, via Snapshot, to remove Brantly Millegan, as a director of the Foundation Company;_
_Whereas, by a majority of the votes cast is to remove Brantly Millegan, as a director of the Foundation Company;_
_Wherefore, Brantly Millegan is hereby removed as a director of The ENS Foundation and shall cease and desist all duties thereto._
_The result to be formally noted in the council records and shall be promptly communicated to the Foundation Company without further process._
_This Notice shall have no force or effect, if the vote to remove Brantly Millegan fails to obtain a majority of the votes cast."_
---
# [EP 5.22] [Executable] ENSv2 Development Funding
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-22-ensv2-development-funding-request/19762) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/33504840096777976512510989921427323867039135570342563123194157971712476988820) |
## Summary
This executable proposal seeks to implement the revised budget stream to ENS Labs develop, maintain and audit [ENSv2](https://discuss.ens.domains/t/ens-labs-development-proposal-ensv2-and-native-l2-support/19232). The motivation, justification, budget breakdown, and development plan was previously detailed in a [Temp Check](https://discuss.ens.domains/t/temp-check-ensv2-development-funding-request/19762) request.
## Background
With over 3 million .eth names and 20 million more ENS names registered by the likes of Coinbase, Uniswap, and Linea – ENS has become the standard for web3 identity. As Ethereum's roadmap evolves towards being rollup-centric, it's essential for ENS to adapt in parallel, ensuring it meets the needs of both the Ethereum ecosystem and its users.
To continue scaling and evolving ENS, ENS Labs is requesting an increase in its annual budget from $4.2M USDC to $9.7M USDC, and a one-time grant for future security audits of ENSv2. This revised funding is necessary for ENS Labs to develop, maintain, and audit ENSv2, a major upgrade that will enhance decentralization, flexibility, and scalability by leveraging Layer 2 solutions and redesigning the ENS protocol from the ground up.
### Links
- [\[Temp Check\] ENSv2 Development Funding Request](https://discuss.ens.domains/t/temp-check-ensv2-development-funding-request/19762)
- [ENS Labs development proposal: ENSv2 and native L2 support](https://discuss.ens.domains/t/ens-labs-development-proposal-ensv2-and-native-l2-support/19232)
- [ENS Labs Transparency Reports](https://discuss.ens.domains/t/ens-labs-transparency-reports/19806)
## Specification
This executable proposal will initiate a new daily stream of 15,075.33 USDC from the ENS DAO treasury to ENS Labs, starting on January 1, 2025\. This will run in addition to the existing streaming contract of 11,500 USDC/day at 0xB1377e4f32e6746444970823D5506F98f5A04201, for a total of 26,575.34 USDC/day ($9.7M USDC/year).
---
# Process of Submitting a Proposal
## Passing a Proposal
### Types of Proposal
There are three main types of governance proposals you can make:
1. **[Executable Proposal](https://github.com/ensdomains/docs/tree/master/src/public/governance/executable-proposal-template.md):** This is a proposal for a series of smart contract operations to be executed by accounts the DAO controls. These can include transfers of tokens as well as arbitrary smart contract calls. Examples of this include allocating funding to a workstream multisig wallet, or upgrading an ENS core contract. Executable proposals have a quorum requirement of 1% and require a minimum approval of 50% to pass.
2. **[Social Proposal](https://github.com/ensdomains/docs/tree/master/src/public/governance/social-proposal-template.md)**: This is a proposal that asks for the agreement of the DAO on something that cannot be enforced onchain. Examples of this include a proposal to change the royalty percentage for the ENS secondary market on OpenSea, or a petition to the root keyholders. Social proposals have a quorum requirement of 1% and require a minimum approval of 50% to pass.
3. **[Constitutional Amendment](https://github.com/ensdomains/docs/tree/master/src/public/governance/constitutional-amendment-template.md)**: This is a social proposal that asks the DAO to amend the constitution. Your draft proposal should include a [diff](https://en.wikipedia.org/wiki/Diff) showing the exact changes you propose to make to the constitution. Rules for amending the constitution are set in the constitution itself, and currently require a quorum of 1% and a minimum approval of two thirds to pass.
### **Phase 1: Temperature Check — Discourse**
The purpose of the Temperature Check is to determine if there is sufficient will to make changes to the status quo.
To create a Temperature Check, ask a general, non-biased question to the community on [discuss.ens.domains](https://discuss.ens.domains) about a potential change (example: "Should ENS decrease registration costs for 3-letter domains?"). Forum posts should be in the "DAO-wide -> Temperature Check" category.
Temperature checks are informal and optional; it's up to you to use the feedback to decide if you want to proceed further with your proposal.
### **Phase 2: Draft Proposal — GitHub**
The purpose of the Draft Proposal is to establish formal discussion around a potential proposal.
To create a Draft Proposal, [create a new governance proposal](https://github.com/ensdomains/governance-docs/new/main/governance-proposals) in the governance-docs repository on GitHub. Start by copying the template for an [executable proposal](https://github.com/ensdomains/docs/tree/master/src/public/governance/executable-proposal-template.md), [social proposal](https://github.com/ensdomains/docs/tree/master/src/public/governance/social-proposal-template.md), or [constitutional amendment](https://github.com/ensdomains/docs/tree/master/src/public/governance/constitutional-amendment-template.md), as appropriate. Once you have written your proposal, create a Draft Pull Request for it. Start a new post in the DAO-wide -> Draft Proposals" category with a link to the PR for discussion.
Reach out to your network to build support for the proposal. Discuss the proposal and solicit delegates to provide feedback on it. Be willing to respond to questions on the Draft Proposal topic and in comments on the pull request. Share your viewpoint, although try to remain as impartial as possible.
If your proposal is an executable proposal, you will need to specify the actions your proposal will take while it is in draft stage. You may wish to wait until the proposal is stable before doing this. The executable proposal template explains how to do this.
If your proposal is a constitutional amendment, you will need to produce a diff showing the exact changes you are proposing to make. The easiest way to do this is to go to the [constitution](/dao/constitution), click "Edit on GitHub", then click the pencil icon to edit the document in a fork. You can then create a pull request via the GitHub UI and include this in your proposal. You should do this in a separate branch to your draft proposal; while the proposal will be merged as soon as it goes to a vote, the amendment will only be merged if the proposal passes.
Once you are confident the proposal is in a stable state, you can proceed to phase 3.
### **Phase 3: Active Proposal — Snapshot / Governance Portal**
Use GitHub to flag your PR as Ready for Review. A contributor will:
1. Merge your PR if it meets the requirements.
2. Assign your proposal a proposal number in the form EP###.
3. Schedule the proposal for a snapshot vote.
If your proposal is a Social Proposal or a Constitutional Amendment, that's it! If the snapshot vote passes, the proposal is passed and you are done.
If your proposal is an Executable Proposal, you will now need to submit it to the governor contract for voting onchain.
To enact an Executable Proposal:
1. Ensure at least 100k ENS is delegated to your address in order to submit a proposal, or find a delegate who has enough delegated ENS to meet the proposal threshold to propose on your behalf.
2. Call the propose() function of the ENS governor (at [governor.ensdao.eth](https://etherscan.io/address/0x323a76393544d5ecca80cd6ef2a560c6a395b7e3)) to deploy your proposal.
Once the propose() function has been called, a seven day voting period is started. Ongoing discussion can take place on your proposal post. If the proposal passes successfully, a two day timelock will follow before the proposed code is executed.
---
# [4.4.2] [Social] Funding Request: ENS Meta-Goverance Working Group
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/17994) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x5c0d103911aaaa64ee33fc35aa30bffd7c1ca04ac2df85fb274414732c45a6f9) |
## Abstract
The ENS Meta-Governance Working Group requests funding of the below to **support operations until the March 2024 funding window**. This means the Working Group will not be requesting funds in the January 2024 Funding Window.
The Meta-Governance Working Group is responsible for providing governance oversight and supporting the management and operation of working groups through DAO tooling and governance initiatives as well as treasury management for the DAO.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)). If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
| | USDC | ETH | $ENS |
| -------------------------- | :-------: | :-: | :----: |
| ENS Meta-Gov Main Multisig | 376,000\t | 40 | 52,300 |
---
## Description
### Current Metagov Wallet Balances
(Values expected as of October 20th, 2023 - Use hyperlinked wallet names to see current balances)
| Address | ETH | USDC | $ENS |
| ---------------------------------------------------------------------------------------------------- | --- | ------- | ----- |
| [ens-metagov.pod.xyz](https://etherscan.io/address/0x91c32893216dE3eA0a55ABb9851f581d4503d39b) | 1.2\* | 162,653 | 1,990 |
| [ens-governance.pod.xyz](https://etherscan.io/address/0x4f4cAdb8AF8F1d463240c2b93952D8a16688a818) | 32 | 83,500 | 1,250 |
| [ens-daotooling.pod.xyz](https://etherscan.io/address/0x8f730f4aC5fd234df9993E0E317f07e44fb869C1) | 0 | 85,993 | |
| [ens-endowmentfees.pod.xyz](https://etherscan.io/address/0x83DD97A584C4aD50015f7aA6B48bf4970A056d8f) | 86.69 | | |
\*As of 10/18 this wallet also temporarily holds the 117 ETH being distributed via [ [EP4.3] [Executable] Refund .eth names affected by normalization updates](https://discuss.ens.domains/t/ep4-3-executable-refund-eth-names-affected-by-normalization-updates/17622)
---
## Expenditures
Meta-Gov sets aside funds to ensure coverage for critical mission initiatives. While we strive to estimate term expenditures accurately, the final spending depends on pending initiatives. We anticipate that Final Expenditures will not surpass the Expected Expenses allocated for the term.
### Expected Expenses through March 2024
| | USDC | ETH | $ENS |
| -------------------------------- | ----------- | ------ | ---------- |
| Steward + Secretary Compensation | 276,000 | - | |
| Governance | 50,000 | - | 52,300 |
| DAO Tooling | 50,000 | 30 | - |
| DAO Sponsorship | - | 10 | - |
| Discretionary | - | - | - |
| **Total Balance** | **376,000** | **40** | **52,300** |
### Description of Initiatives/Pods
**Steward + Secretary Compensation**: Working Group Steward and Secretary compensation totalling $276,000 USDC.
**Governance**: Fee reimbursements and initiatives related to reducing friction in the governance process. This can also include $ENS distributed in order to lower barriers to the governance proposal process.
**DAO Tooling**: Developing interfaces and dashboards to improve the governance process and increase transparency across the DAO.
**DAO Sponsorship**: Sponsoring DAO-specific events such as DAO NYC, DAO Tokyo, or others that fit the criteria.
**Discretionary**: Funds distributed at the discretion of stewards towards new initiatives + governance experiments.
---
This proposal was prepared by katherine.eth, lead steward of the ENS Meta-Governance Working Group.
---
# [EP4.9] [Social] Select providers for EP4.7 streams
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/18309) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x6ba81cd2997288cc49ae1b95921ec8f107e8ffb9733321d53d488e2b30710b86) |
## Abstract
Following the approval of [EP4.7](/dao/proposals/4.7) by the DAO, prospective service providers have submitted applications to be considered by the DAO for funding. This proposal collects successful applications for a vote by the DAO.
## Specification
EP4.7 provides for a budget of $3,600,000 USDC to be provided annually to qualifying service providers. All successful applications are summarized below in [random order](https://twitter.com/nicksdjohnson/status/1731660224031793376).
The purpose of this vote is to select service providers to receive streaming funding from the DAO. Per the [draft rules](https://github.com/alexvansande/governance-docs/blob/Streams-rules/service-provider-rules.md), selection of winning proposals uses the following process:
1. **Order Proposals**
- Arrange all proposals in descending order based on the number of votes received.
2. **Evaluate Proposals Sequentially**
- **Vote Threshold Check**
- If a proposal has received fewer than 1 million votes, stop the evaluation process immediately.
- **Comparison with 'None of the Above'**
- If a proposal has fewer votes than the 'None of the Above' option, stop the evaluation process.
- **Budget Check**
- If a proposal's requested budget exceeds the remaining budget, skip it and move to the next proposal.
- **Selection**
- If a proposal passes the above checks, add it to the set of selected proposals. Deduct its requested budget from the remaining budget.
### Post-Selection Procedure
- If at least one service provider is chosen, the Meta-Governance working group will post an executable vote to implement the funding streams by January 10 at the latest.
## Instructions
**Vote FOR** on any service providers whom you believe that are capable of continuous evolution and enhancement of the ENS system and for which you believe offer a good cost benefit. **Do NOT vote** to any provider you do not believe to be capable of such, for which you believe the proposed projects are out of scope or for which you believe the ask is too high for the proposals. _Only vote "None of the Above" if you want to express your disapproval with the whole system and would rather **not have ANY** of the projects be selected._
## Candidates
### NameSys.eth
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#namesysethhttpsdiscussensdomainstservice-provider-stream-nomination-thread181426-1) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/6) **Requested Budget**: $200,000 p.a.
### handle.eth
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#handleethhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814237-2) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/37) **Requested Budget**: $100,000 p.a.
### NameHash Labs
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#namehash-labshttpsdiscussensdomainstservice-provider-stream-nomination-thread1814239-3) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/39) **Requested Budget**: $600,000 p.a.
### Unruggable
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#unruggablehttpsdiscussensdomainstservice-provider-stream-nomination-thread181425-4) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/5) **Requested Budget**: $400,000 p.a.
### generalmagic.eth & pairwise.eth
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#generalmagiceth-pairwiseethhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814231-5) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/31) **Requested Budget**: $300,000 p.a.
### servais.eth / web3xplorer.com
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#servaiseth-web3xplorercomhttpsdiscussensdomainstservice-provider-stream-nomination-thread181429-6) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/9) **Requested Budget**: $100,000 p.a.
### AlphaWallet
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#alphawallethttpsdiscussensdomainstservice-provider-stream-nomination-thread1814217-7) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/17) **Requested Budget**: $100,000 p.a.
### ENS Like Protocol
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#ens-like-protocolhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814230-8) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/30) **Requested Budget**: $100,000 p.a.
### Namespace
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#namespacehttpsdiscussensdomainstservice-provider-stream-nomination-thread1814220unicketh-9) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/20?u=nick.eth) **Requested Budget**: $200,000 p.a.
### Gnosis Guild
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#gnosis-guildhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814233-10) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/33) **Requested Budget**: $600,000 p.a.
### GravityDAO
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#gravitydaohttpsdiscussensdomainstservice-provider-stream-nomination-thread1814226-11) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/26) **Requested Budget**: $100,000 p.a.
### ENS Vision Forge
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#ens-vision-forgehttpsdiscussensdomainstservice-provider-stream-nomination-thread1814219-12) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/19) **Requested Budget**: $500,000 p.a.
### Blockful
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#blockfulhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814238-13) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/38) **Requested Budget**: $300,000 p.a.
### Web3Domains.com
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#web3domainscomhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814227-14) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/27) **Requested Budget**: $500,000 p.a.
### dAppling
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#dapplinghttpsdiscussensdomainstservice-provider-stream-nomination-thread1814222-15) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/22) **Requested Budget**: $400,000 p.a.
### ESF Tools
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#esf-toolshttpsdiscussensdomainstservice-provider-stream-nomination-thread1814216-16) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/16) **Requested Budget**: $200,000 p.a.
### StableLab
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#stablelabhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814223-17) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/23) **Requested Budget**: $300,000 p.a.
### The Interceptor
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#the-interceptorhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814210-18) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/10) **Requested Budget**: $500,000 p.a.
### Tally
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#tallyhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814232-19) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/32) **Requested Budget**: $300,000 p.a.
### ENS Anti-Abuse Tools
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#ens-anti-abuse-toolshttpsdiscussensdomainstservice-provider-stream-nomination-thread1814221-20) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/21) **Requested Budget**: $100,000 p.a.
### Wildcard Labs
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#wildcard-labshttpsdiscussensdomainstservice-provider-stream-nomination-thread1814225-21) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/25) **Requested Budget**: $200,000 p.a.
### eth.limo
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#ethlimohttpsdiscussensdomainstservice-provider-stream-nomination-thread181423-22) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/3) **Requested Budget**: $500,000 p.a.
### wayback-machine.eth
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#wayback-machineethhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814229-23) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/29) **Requested Budget**: $200,000 p.a.
### Referrals powered by generalmagic.eth
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#referrals-powered-by-generalmagicethhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814234-24) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/34) **Requested Budget**: $200,000 p.a.
### unicorn.eth
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#unicornethhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814236-25) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/36) **Requested Budget**: $200,000 p.a.
### Ethereum Follow Protocol
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#ethereum-follow-protocolhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814218-26) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/18) **Requested Budget**: $500,000 p.a.
### resolverworks.eth
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#resolverworksethhttpsdiscussensdomainstservice-provider-stream-nomination-thread1814224-27) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/24) **Requested Budget**: $700,000 p.a.
### 1w3.eth
[Summary](https://discuss.ens.domains/t/ep4-7-candidates-short-description/18382#h-1w3ethhttpsdiscussensdomainstservice-provider-stream-nomination-thread181428-28) | [Full Proposal](https://discuss.ens.domains/t/service-provider-stream-nomination-thread/18142/8) **Requested Budget**: $500,000 p.a.
---
# [EP4.7] [Social] Create Service Provider Streams
::authors
| **Status** | Passed, Budget of $3.6M |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/18091) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x5748982aed143f51333befbc6cc490116648b85a2b0212fdfaf3ab848932c7ae) |
## Abstract
The intent of this proposal is to add new Streams for service providers and propose a structure on how to elect them.
## Motivation
The ENS DAO constitution mandates:
_"Any income generated by the ENS treasury should primarily ensure the long-term viability of ENS and fund the ongoing development and enhancement of the ENS system. Surplus funds may be allocated to other public goods within the web3 space as determined by ENS governance."_
The Endowment has been pivotal in securing the longevity of ENS. This proposal aims to utilize new funds to establish funding streams for service providers dedicated to the continuous evolution and enhancement of the ENS system.
### **Procedure for Implementation**
Service providers interested in participating must submit their candidacy on the ENS Forum by December 1st, demonstrating their expertise and providing a plan for enhancing the ENS system. Detailed instructions will be issued following the approval of this proposal.
Candidates must stipulate their service fees in increments of US$200,000 per annum, committing to a minimum service period of 12 months. Project budgets should not exceed US$1 million annually.
A new snapshot for approval voting will be set up. Projects must obtain a minimum of 1 million ENS in approvals to proceed.
Eligible projects will be ranked by vote count. A greedy algorithm will then be applied to select the highest-voted projects, provided the cumulative budget does not exceed the preset limit.
Funding for streams will be guaranteed for a minimum of 18 months to prevent service disruptions. However, a reassessment vote will be conducted after 12 months. Although the one-year term is non-binding, and streams may be discontinued at any time by the service provider or the DAO, proper notice will be given in advance.
All outputs financed by the DAO must be released under an Open Source License permitting derivative works.
#### **Criteria for Project Qualification**
Projects may include, but are not limited to:
- Development of alternative open-source front-ends for diverse audiences or platforms.
- Maintenance of developer tools, such as SDKs, to facilitate ENS integration into various applications.
- Implementation of a referral program, complete with necessary contracts and outreach for successful execution.
- Proposals for enhancements to ENS base layer contracts for a more refined experience or to facilitate cost-effective batch transactions.
- Creation and support of improved tools for ENS names' interoperability with decentralized storage solutions.
- Development of novel off-chain trading experiences for names or innovative NFT trading experiments to broaden ENS's appeal.
- Exploration of untapped platforms or entirely new applications for ENS.
## Voting
The vote is a ranked choice voting on the ideal budget: rank your preferred budget or if you'd rather not approve the project at all, rank NO as the highest ranked choice.
---
# [5.4.1] [Social] Funding Request: ENS Meta-Governance Working Group Term 5 (Q1/Q2)
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/5-4-1-social-funding-request-ens-meta-governance-working-group-term-5-q1-q2/18883) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0xfa1fdf0cfb94eabecf613ccd6b0c3a9247c21e64047cf330c000760970a02536) |
## Abstract
The ENS Meta-Governance Working Group requests funding of the below to **support operations until the September 2024 funding window**.
The Meta-Governance Working Group is responsible for providing governance oversight and supporting the management and operation of working groups through DAO tooling and governance initiatives as well as treasury management for the DAO.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)) and further required by [this snapshot proposal in Nov. 2023 modifying steward rules.](https://snapshot.org/#/ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266) If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
This specification is the amount requested from the DAO treasury to the Metagov Multisig to fulfill anticipated budgetary needs through September 2024.
| | USDC | ETH | $ENS |
| -------------------------- | :--: | :-: | :--: |
| ENS Meta-Gov Main Multisig | 374k | 0 | 105k |
## Description
### Current Metagov Wallet Balances
(Values expected as of March 5th, 2024 - Use hyperlinked wallet names to see current balances)
| Address | ETH | USDC | $ENS |
| ---------------------------------------------------------------------------------------------------- | ------- | ------- | ------ |
| [ens-metagov.pod.xyz](https://etherscan.io/address/0x91c32893216dE3eA0a55ABb9851f581d4503d39b) | 85.68\* | 362,463 | 15,540 |
| [ens-endowmentfees.pod.xyz](https://etherscan.io/address/0x83DD97A584C4aD50015f7aA6B48bf4970A056d8f) | 0 | 0 | 0 |
\*_This value includes 22.89 that the metagov safe loaned to the endowment fees payments that will be repaid to the metagov safe with executable that implements the new Endowment fees strategy_
## Expenditures
Meta-Gov sets aside funds to ensure coverage for mission-critical initiatives. While we strive to estimate term expenditures accurately, the final spending depends on pending initiatives. We anticipate that final expenditures will not surpass the expected expenses allocated for the term.
### Expected Expenses through September 2024
| | USDC | ETH | $ENS |
| -------------------------------- | ----------- | ------ | -------- |
| Steward + Secretary Compensation | 294,000 | - | |
| Governance | 50,000 | 5 | 105k |
| DAO Tooling | 140,000 | - | - |
| Discretionary | - | 10 | - |
| **Total Balance** | **484,000** | **15** | **105k** |
### Governance Distributions
| Recipient Category | Amount of $ENS | Method |
| --------------------------- | -------------- | ------------------------------------------------------------ |
| Contributors and Developers | 60k | Vesting contracts |
| Elected Stewards | 45k | Change to vesting contracts is planned for the 2025 guidance |
### Description of Initiatives/Pods
**Steward + Secretary Compensation**: Working Group Steward and Secretary compensation as required by [revised steard working group rules](https://snapshot.org/#/ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266) and [totaling $294,000 USDC](https://discuss.ens.domains/t/ens-dao-steward-compensation/18063).
**Governance**: Fee reimbursements and initiatives related to reducing friction in the governance process. This can also include $ENS distributed in order to lower barriers to the governance proposal process.
**DAO Tooling**: Developing interfaces and dashboards to improve the governance process and increase transparency across the DAO. An example of DAO tooling spend is our current engagement with Agora as they help build out an enhanced DAO proposal flow to streamline the proposal process.
**Discretionary**: Funds distributed at the discretion of stewards towards new initiatives + governance experiments. In this cycle, we've consolidated the former DAO Sponsorship category into this discretionary category.
## Conclusion
This funding request will allow the ENS Meta-Governance Working Group to continue its essential work in providing governance oversight, supporting the management and operation of working groups, and ensuring effective treasury management for the DAO. The requested funds will enable us to maintain our ongoing initiatives and develop new tools to enhance the governance process. We are grateful for the community's ongoing support and engagement, which is crucial to the success of the ENS DAO. The Meta-Governance Working Group remains committed to serving the ENS community and driving the long-term growth and sustainability of the ecosystem.
---
# [5.9] [Social] Funding Request: ENS Meta-Governance Working Group Term 5 (Q1/Q2)
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/5-9-1-social-funding-request-ens-meta-governance-working-group-term-5-q1-q2/19223) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x66d355555c24ed0d2fed0aee89e4fe009e2925c84144c4edc707d33e7c19e554) |
## Abstract
In March 2024, The ENS Metagov funding request for Q1/Q2 failed to gain the support needed to pass.
The ENS Meta-Governance Working Group is now using the June 2024 funding window to request the Q1/Q2 funding to cover operations until the next funding window in September 2024.
The Meta-Governance Working Group is responsible for providing governance oversight and supporting the management and operation of working groups through DAO tooling and governance initiatives as well as treasury management for the DAO.
This social proposal is submitted to satisfy the requirements set out in Rule 10.1.1 of the Working Group Rules ([EP 1.8](https://docs.ens.domains/v/governance/governance-proposals/term-1/ep12-working-group-rules)) and further required by [this snapshot proposal in Nov. 2023 modifying steward rules.](https://snapshot.org/#/ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266) If this proposal is passed, the funding request will be included in a collective executable proposal put forward by all three Working Groups.
## Specification
This specification is the amount requested from the DAO treasury to the Metagov Multisig to fulfill anticipated budgetary needs through September 2024.
| | USDC | ETH | $ENS |
| -------------------------- | :--: | :-: | :--: |
| ENS Meta-Gov Main Multisig | 374k | 0 | 150k |
## Description
### Current Metagov Wallet Balances (May 25th, 2024)
| | USDC | ETH | $ENS |
| -------------------------- | :--: | :--: | :--: |
| ENS Meta-Gov Main Multisig | 199k | 83.7 | 15k |
\*Updated Balance information can be found at https://enswallets.xyz
## Expenditures
Meta-Gov sets aside funds to ensure coverage for mission-critical initiatives. While we strive to estimate term expenditures accurately, the final spending depends on pending initiatives. We anticipate that final expenditures will not surpass the expected expenses allocated for the term.
### Expected Expenses in Q1/Q2 2024
| | USDC | ETH | $ENS |
| -------------------------------- | ----------- | ------ | -------- |
| Steward + Secretary Compensation | 294,000 | - | |
| Governance | 50,000 | 5 | 105k |
| DAO Tooling | 140,000 | - | - |
| Discretionary | - | 10 | - |
| **Total Balance** | **484,000** | **15** | **105k** |
### Governance Distributions
| Recipient Category | Amount of $ENS | Method |
| --------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Contributors and Developers | 60k | Vesting contracts |
| Elected Stewards | 90k | A change to 90k and vesting contracts was required to comply with the vesting process described in [[EP5.8](https://snapshot.org/#/ens.eth/proposal/0x1f328fd1fda5f3cabfdace3e521403def7ad41b0b0582e27334c135cd23c511d)] |
### Description of Initiatives/Pods
- **Steward + Secretary Compensation**: Working Group Steward and Secretary compensation as required by [revised steward working group rules](https://snapshot.org/#/ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266) and [totaling $294,000 USDC to cover the costs for all 9 stewards and supporting roles for a 6 month period](https://discuss.ens.domains/t/ens-dao-steward-compensation/18063).
- **Governance**: Fee reimbursements and initiatives related to reducing friction in the governance process. This can also include $ENS distributed in order to lower barriers to the governance proposal process. The $ENS distributions to stewards and service providers falls into this category.
- **DAO Tooling**: Developing interfaces and dashboards to improve the governance process and increase transparency across the DAO. An example of DAO tooling spend is our current engagement with Agora as they help build out an enhanced DAO proposal flow to streamline the proposal process.
- **Discretionary**: Funds distributed at the discretion of stewards towards new initiatives + governance experiments. In this cycle, we've consolidated the former DAO Sponsorship category into this discretionary category.
## Conclusion
This funding request will allow the ENS Meta-Governance Working Group to continue its essential work in providing governance oversight, supporting the management and operation of working groups, and ensuring effective treasury management for the DAO. The requested funds will enable us to maintain our ongoing initiatives and develop new tools to enhance the governance process. We are grateful for the community's ongoing support and engagement, which is crucial to the success of the ENS DAO. The Meta-Governance Working Group remains committed to serving the ENS community and driving the long-term growth and sustainability of the ecosystem.
---
# [EP1.3.3] [Executable] Q1 & Q2 2022 Community WG Budget
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/11046) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/115615865324623814833258987703837575663427750121726187103053182962864855260310) |
_Note: This was previously numbered EP7.3._
## Summary
1. **Community WG Operational Budget:** 115,000 USDC/DAI, 1 ETH, and 650 ENS.
2. **Elected Steward Compensation:** 27,500 in USDC/DAI.
**Total USD Value:** \~$155,050.
## Community WG Budget: Q1 & Q2 Steward Term
### 1. Operational Budget
This funding is requested to fulfill the needs of the entire Q1/Q2 term.
| Subgroup Name | Description | USDC/DAI | ETH | $ENS |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | :-----: | :--: |
| Learn Docs | Content related to user documentation, tutorials, and case studies. | 11,000 | 0 | 0 |
| Communications | Provide communications services for the DAO to include a bi-weekly digest, weekly twitter spaces, and other outreach services to drive education and engagement. | 10,000 | 0 | 100 |
| Onboarding | Facilitate and coordinate weekly onboarding calls about ENS and the DAO. Also, focus on refining the DAO onboarding process. | 10,000 | 0 | 0 |
| Discord Support Moderation | Provides 24-hr support coverage in the ENS Discord. | 66,000 | 0 | 0 |
| Translation | Administer translation services for the ENS DAO official documents and website details. | 6,000 | 0 | 0 |
| Communidad Para Hispanohablantes | Increase onboarding for the native Spanish-speaking community. | 2,000 | 0 | 0 |
| IRL Outreach | Community engagement focused on in-person events. | 10,000 | 0 | 0 |
| WG Discretionary Funds | Discretionary funding to be allocated to the above subgroups or facilitate the funding of new subgroups as the council of stewards deem necessary. | 0 | 1 | 550 |
| **Total** | **115,000** | **1** | **650** | |
**Note:** This includes subgroups and supports moderation of the ENS Discord. The Discord moderation is a necessary DAO expense carried by the community working group.
### 2. Elected Steward Compensation
Provide compensation for the three elected Community Stewards @limes , @spencecoin and @coltron.eth for the entire Q1/Q2 steward term.
| Description | Compensation | Months # | Stewards # | Total USDC |
| ------------------------- | :----------: | :--------: | :--------: | :--------: |
| Base Compensation | $1,000/month | 5.5 | 3 | 16,500 |
| Supplemental Compensation | $2,000/month | 5.5 | - | 11,000 |
| **Total** | | **27,500** | | |
**Note:** _Supplemental compensation shall be distributed to stewards and contributors involved in running operations for the WG. The supplemental compensation will be used in situations where contributors or stewards perform duties beyond what is normally expected. The steward council determines how the supplemental compensation will be split between the stewards based on the contributions of each steward._
### Considerations
Multiple parties will approve all funding disbursements using a multi-sig. This budget does not guarantee disbursement, specifically if services rendered to the DAO are incomplete or deemed unsatisfactory. If these situations arise, the working group will review them publicly at the weekly Community Steward Call.
Any funding not used will be re-allocated back to the treasury.
---
# [EP4.10] [Social] Transfer ENS Root Key Ownership to the ENS DAO
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/18338) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x8e3c8812bd52d1760eb2bbf8dc603d68860741f80d489dc051017e863151a242) |
## Abstract
The ENS DAO has established itself as the key governance entity for the Ethereum Name Service (ENS) and has demonstrated capability and responsibility in ownership of several aspects of the protocol. We now propose the further decentralization of the ENS governance structure by transferring ownership of the ENS root key from the current multisig system (multisig.ens.eth) to the ENS DAO (wallet.ensdao.eth).
## Background
### ENS Root Key
The ENS root node is currently owned by a multisig contract. The root key can control the allocation and replacement of all TLDs except for .eth. Keyholders are well-respected individuals in the Ethereum community and, with the exception of Nick Johnson, founder of ENS, are unaffiliated with ENS. While the .eth registrar contract control has been locked and cannot be affected by the root keyholders, they still hold significant powers over the structure and functioning of the ENS Protocol.
### ENS DAO Current Powers
The ENS DAO holds ownership of the .eth registrar and has control over functionalities like the NameWrapper. The DAO does not have control over the core ENS root name, which retains the capability to create new TLDs, adjust controls for existing ones, and upgrade the contracts responsible for reverse resolution.
## Proposal
1. **Transfer of Root Key Ownership**: We propose the transfer of ownership of the ENS root key from the current multisig holders to the ENS DAO. This will consolidate governance powers and further the vision of decentralized control over the ENS Protocol.
2. **TLD Management**: While .eth is permanently set and unchangeable, the DAO will inherit the power to create and manage other top-level domains (TLD). The DAO will also have the ability to lock any TLD permanently if deemed necessary.
3. **Reverse Resolutions and L2s**: The DAO will have the capacity to update reverse resolutions. Additionally, with the Labs team's support, the DAO will explore and potentially implement primary domain names on Layer 2 solutions (L2s).
## Specification
1. Current ENS root keyholders are requested to execute a transaction transferring root key ownership to the ENS DAO's designated contract.
2. The ENS DAO commits to managing the new powers and funds in line with the [ENS DAO Constitution](/dao/constitution), ensuring transparent governance and decision-making.
3. The DAO will develop policies to govern TLD management, (oracle) pricing updates, and other key decisions.
## Coda (Conclusion)
With this proposal, we aim to further decentralize the control and governance of the ENS Protocol, placing more trust and power in the hands of the ENS community via the DAO. Community discussion will ensue on the [Governance Forum](https://discuss.ens.domains). The ballot will begin on [Snapshot](https://snapshot.org/#/ens.eth) and, should it be passed, the current ENS root keyholders will initiate a transfer of the ENS root key to the ENS DAO where it will be managed according to the precepts outlined herein.
---
# [EP4.2] [Executable] Fund the Endowment (second tranche)
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/17743) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/10686228418271748393758532071249002330319730525037728746406757788787068261444) |
## Abstract
This proposal outlines the allocation of the second tranche, comprising 16,000 ETH, from the [ENS DAO](https://etherscan.io/address/0xfe89cc7abb2c4183683ab71653c4cdc9b02d44b7) to the [ENS Endowment](https://etherscan.io/address/0x4F2083f5fBede34C2714aFfb3105539775f7FE64). Additionally, it introduces minor adjustments to the existing permissions preset for maintenance purposes.
## Motivation
In March 2023, the ENS Endowment was formally established following the [joint proposal](https://discuss.ens.domains/t/updated-endaoment-proposal-karpatkey-steakhouse-financial/14799) by karpatkey and @steakhouse, after the successful execution of [E.P 3.4 - Fund the Endowment (first tranche)](https://www.tally.xyz/gov/ens/proposal/90786656233306599444783442367171420493182391933134906270328139870999449830964). The community had expressed a preference for a phased funding approach, leading to the decision to allocate the funds in two equal tranches over a six-month interval. As we reach this pivotal milestone, this proposal seeks community approval for the second tranche.
### Endowment Update
Based on the most recent monthly report detailing the Endowment's performance for August 2023, the Endowment has achieved:
- $28.03 M of ncAUM (non-custodial assets under management)
- 100% of capital utilisation
- An APY (annual percentage yield) of 4.1%
- Monthly farming results of $93,841
A comprehensive review [post](https://discuss.ens.domains/t/karpatkey-h1-2023-review-for-the-ens-endowment/17682) detailing our collaborative efforts with the ENS DAO has been recently shared on the forum. We encourage community members to consult this post for insights into our achievements and ongoing initiatives.
#### Cumulative Revenues
In the 182 days since the Endowment was established, 173 ETH have been accrued through ETH-neutral strategies and $136,764 in stablecoin revenues via USD-neutral strategies. Operational reports were initially shared on a [weekly basis](https://discuss.ens.domains/t/endowment-weekly-reports/16665) and later transitioned to [monthly updates](https://discuss.ens.domains/t/endowment-monthly-reports/17614), all of which were made available on the forum for community review.
The Endowment's phased initiation should also be taken into account when interpreting these results. Full capital utilisation was only achieved 49 days after the Endowment's inception, following the completion of the earned ETH-to-stablecoin tranche swaps. This staggered approach had a discernible impact on the reported financial metrics.
Taking into consideration the revenue generated during the most recent four months—after reaching 100% capital utilisation—the projected annual revenues at the current Endowment's size stand at 367 ETH from ETH-neutral strategies and an equivalent of $351,654 in stablecoins from USD-neutral strategies. This results in a projected Annual Percentage Rate (APR) of approximately 3.4%.
## Specification
### Fund Transfer
Transfer 16,000 ETH to the Endowment (0x4F2083f5fBede34C2714aFfb3105539775f7FE64).
### Permissions preset adjustments
In line with our commitment to streamline governance and reduce the frequency of voting events for the community, we propose targeted adjustments to the existing permissions preset within this proposal. Specifically, we are requesting three key changes:
- **Whitelisting the updated wstETH-WETH Pool and Gauge**: As part of our ongoing optimization, we propose to whitelist the updated wstETH-WETH pool on [Balancer](https://app.balancer.fi/#/ethereum/pool/0x93d199263632a4ef4bb438f1feb99e57b4b5f0bd0000000000000000000005c2) and its corresponding gauge on [Aura](https://app.aura.finance/#/1/pool/153).
- **Revoking Permissions for Aura's bb-a-usd Pool**: In light of the recent [vulnerability](https://forum.balancer.fi/t/vulnerability-found-in-some-pools/5102) disclosed by Balancer on August 24, 2023, we recommend revoking all permissions associated with potentially compromised pools. It's important to note that the Endowment had no exposure to these compromised pools at the time the vulnerability was made public.
- **Whitelisting the **_**delegatecall**_** function on Cow Swap**: A minor bug was found in the existing preset configuration. Specifically, the _signOrder_ function within Cow Swap's order signer contract is designed to be executed solely via a delegate call, a capability not currently supported by the preset. This oversight not only hindered functionality but also revealed a flaw in the SDK preset testing framework. The issue has been swiftly addressed and rectified in a recent [commit](https://github.com/gnosis/zodiac-modifier-roles-v1/commit/c22b1fa8c10b1e3cfb2c1fceef24498c25f3ea2d) to the codebase.
As is customary, we are presenting an updated version of the ["Preset permissions - ENS Endowment'' document](https://docs.google.com/document/d/1vhws_fnbIws8EUItK14V2TVIecHdJLxKzN8Za4d5L0M/edit). This document comprehensively lists all permissions granted to karpatkey, with newly requested permissions highlighted in green and any revocations marked in red.
We are also sharing the [payload](https://gist.github.com/santinomics/bb6b345ff977e7451ff0506e253ebc4c/044f46247ab57f8c65bf608b83eacefa128671f7) to apply the proposed changes for your review. We strongly encourage community members with the required technical expertise to scrutinise the content and share their invaluable feedback.
## Transactions
All transactions can be found in the following [payload](https://gist.github.com/santinomics/bb6b345ff977e7451ff0506e253ebc4c/044f46247ab57f8c65bf608b83eacefa128671f7).
---
# [EP0.4] [Social] Proposal: Creation of Foundational Working Groups and Working Group Rules
::authors
| **Status** | Passed |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/8156) |
| **Votes** | [Snapshot](https://snapshot.org/#/ens.eth/proposal/0x899ead1d9b9b98f63f6a60dc0939bef55dbe365e78c6a550f07be969a47f148b) |
_Note: This was previously numbered EP4._
## Summary
Create four foundational working groups and establish rules related to the creation, management, and dissolution of working groups within the ENS DAO.
## Abstract
The work-related activities of the ENS DAO will take place within working groups. Each working group will have a specific focus area and mission, aligned with the constitution and needs of the DAO.
This proposal establishes four foundational working groups:
1. Meta-Governance: providing governance oversight and support of the management and operation of the ENS DAO and working groups;
2. ENS Ecosystem: continuing development and improvement of the ENS protocol and ecosystem, with a focus on all technical matters related to ENS;
3. Community: supporting the people and organizations that are users of ENS, with a focus on non-technical matters; and
4. Public Goods: amplifying ENS as a public good and funding public goods within the ENS ecosystem, and more broadly within web3.
This proposal also sets out rules related to the creation and dissolution of working groups and outlines how working groups will be managed within the DAO.
## Motivation
Establishing working groups to manage DAO-related work will allow decisions to be made, and action to be taken, without the need for every initiative or decision of the DAO to be passed as a proposal.
The creation of working groups will streamline the management of the DAO into core areas that will persist, irrespective of changes in activities or contributors. A working group system will promote stability and encourage long-term thinking and planning.
The working group structure will provide new and existing community members clear onboarding pathways for meaningful participation and engagement in the DAO.
## Specification
Create four foundational working groups and establish rules related to the creation, management, and dissolution of working groups within the ENS DAO (‘**DAO**’).
1. **Working Groups**
1. Four foundational working groups will be established within the DAO:
1. Meta-Governance Working Group;
2. ENS Ecosystem Working Group;
3. Community Working Group; and
4. Public Goods Working Group.
2. **Formation of Working Groups**
1. To create a new working group, a social proposal, as defined by the [ENS governance documentation](https://docs.ens.domains/v/governance/process#types-of-proposal) (‘**Social Proposal**’), must be put forward and passed by the DAO.
2. A Social Proposal to create a new working group must demonstrate that the new working group is needed and the work cannot be undertaken within an existing working group.
3. **Dissolution of Working Groups**
1. A working group can be dissolved by passing a Social Proposal requesting the dissolution of a working group or working groups.
2. If an active proposal is put forward to dissolve a working group, all working group funds, including outgoing payments, within that working group, are to be frozen with immediate effect, pending the outcome of the vote.
3. Upon the dissolution of a working group, any and all unspent working group funds from that working group, at the time of dissolution, must be immediately returned to the DAO treasury, without delay.
4. **Working Group Stewards**
1. Each working group shall be managed by five stewards (hereafter a '**Steward**' or '**Stewards**').
2. Stewards will be elected, unless otherwise stated in these rules, to serve within working groups for a set period of time (hereafter known as a '**Term**' or '**Terms**').
3. There are two Terms each calendar year:
1. The first Term commences at 9am UTC on January 1 each year and ends immediately prior to the commencement of the second Term ('**First Term**'); and
2. The second Term commences at 9am UTC on July 1 each year and ends immediately prior to the commencement of the First Term of the following year ('**Second Term**').
4. Stewards are responsible for managing the operations of each working group.
5. The responsibilities of Stewards include, but are not limited to:
1. Managing operational tasks related to the administration of a working group;
2. Maintaining a description that sets forth the focus and intent of the working group;
3. Developing working group goals for the Term and providing a clear road map for achieving those goals, to be published in the ENS governance forum within the first 30 days of a Term;
4. Approving the creation and dissolution of sub-groups or workstreams within a working group to undertake work and/or carry out specific projects or tasks;
5. Requesting working group funds from the DAO; and
6. Approving and making funding available to sub-groups, workstreams, or contributors within a working group.
6. To request working group funds, Stewards of all working groups will collaborate to submit an active executable proposal, as defined by the [ENS governance documentation](https://docs.ens.domains/v/governance/process#types-of-proposal) ('**Collective Proposal**'), to the DAO within the final 15 days (inclusive) of the months of January, March, July, and October each calendar year (each a '**Funding Window**').
1. In order for a working group to have a funding request included in a Collective Proposal submitted to the DAO during a Funding Window, the funding request must have passed as a Social Proposal in the same Funding Window.
2. In the case of an emergency, where working group funds are needed by a working group outside of a Funding Window, an Executable Proposal may be submitted at any time by a Steward of a working group to request funds from the DAO.
5. **Steward Eligibility and Nominations**
1. Any individual is eligible to nominate themselves to be a Steward of a working group within the DAO ('**Eligible Person**' or '**Eligible Persons**').
2. To be eligible to be included in the ballot for First Term elections of a given year, Eligible Persons must nominate themselves between 9am UTC on December 6 and 9am UTC on December 9 ('**First Term Nomination Window**').
3. To be eligible to be included in the ballot for Second Term elections of a given year, Eligible Persons must nominate themselves between 9am UTC on June 6 and 9am UTC on June 9 ('**Second Term Nomination Window**').
4. An Eligible Person may nominate themselves to become a Steward of a working group or working groups during the First Term Nomination Window or the Second Term Nomination Window (each a '**Nomination Window**'), by meeting the requirements set out in a call for nominations posted in the relevant working group category of the ENS governance forum.
5. An Eligible Person who completes the steps outlined in rule 5.4 above during a Nomination Window and receives 10,000 signed votes to support their nomination will be included in the ballot as a nominee in the election for Stewards that takes place following that Nomination Window ('**Nominee**').
6. **Steward Elections**
1. Elections for working group Stewards for the First Term of a given year will take place by a ranked-choice vote of governance token holders using signed messages and will be open for 120 hours, commencing at 9am UTC on December 10 each year ('**First Term Election Window**').
2. Elections for working group Stewards for the Second Term of a given year will take place by a ranked-choice vote of governance token holders using signed messages and will be open for 120 hours, commencing at 9am UTC on June 10 each year (**'Second Term Election Window**').
3. The top-ranked Nominees from each working group vote held during a First Term Election Window or a Second Term Election Window (each an '**Election Window**'), will fill any available positions for the role of Steward for those working groups for the Term immediately following an Election Window, based on the order in which they are ranked in each working group vote.
4. A Nominee elected to serve as a Steward may not take up the role of Steward for more than two working groups during a single Term.
7. **Delay of Nominations or Elections**
1. In the event that nominations or elections for Stewards take place after a Nomination Window or after an Election Window, the nomination process or elections shall take place, as otherwise prescribed in rules 5 and 6 above, as soon as is practicable after the missed Nomination Window or missed Election Window.
2. In the event that an election takes place outside of an Election Window and after the commencement date of a new Term, outgoing Stewards from the previous Term shall stay in their positions as working group Stewards until immediately prior to 9am UTC the day following the end of the election, which, for the avoidance of doubt, is 120 hours after voting in those elections commenced.
3. In the event that an election takes place outside of an Election Window and after the commencement date of a new Term, newly elected Stewards will assume the responsibilities of stewardship within working groups at 9am UTC the day following the end of the election, as defined in rule 7.2 above, for the remainder of that Term.
8. **Core Team Stewards**
1. For the First Term, commencing January 1 2022 at 9am UTC, each working group will include two Stewards who are core team members of True Names Limited (Singapore) ('**TNL**'), as selected by TNL and not subject to the Steward nomination and election process outlined in rules 5 and 6 above.
2. For the Second Term, commencing July 1 2022 at 9am UTC, each working group will include one Steward who is a core team member of TNL, as selected by TNL and not subject to the Steward nomination and election process outlined in rules 5 and 6 above.
3. For the First Term of the following year, commencing January 1 2023, and for all Terms thereafter, all Stewards must be elected and appointed in accordance with the rules set out in rules 5 and 6 above.
9. **Removal and Replacement of Stewards**
1. Stewards may be removed at any time by:
1. a Social Proposal passed by the DAO; or
2. a simple indicative majority vote among the Stewards of a given working group, with the outcome of the vote communicated in the relevant working group category of the ENS governance forum.
2. Stewards may step down from their position at any time by communicating their intention to step down in the ENS governance forum.
3. In the event that a Steward is removed, steps down, or is unable to continue as a Steward, for whatever reason, prior to the end of a Term, any vacant positions will be filled for the remainder of a Term by the next highest ranked Nominee(s) in a given working group from the most recent working group vote in the most recent election for Stewards.
4. In the event that a Steward steps down or is removed prior to the end of a Term and is a member of the core team of TNL in accordance with rule 8.1 or 8.2 above, the vacant position(s) will be filled by a core team member or team members of TNL, as selected by TNL.
5. Any Steward selected by TNL in accordance with rule 8.1 or 8.2 above, can be removed by TNL at any time prior to the end of a Term, for any reason, and replaced by another core team member for the remainder of that Term.
10. **Compensation for Stewards**
1. Elected Stewards are eligible to receive fair compensation for their work as a Steward.
2. All requests for Steward compensation must be detailed in a Collective Proposal for working group funds submitted to the DAO in accordance with rule 4.6.
3. Stewards may not receive compensation for their role as a Steward outside of that compensation expressly provided for in a Collective Proposal submitted to the DAO in accordance with rule 10.2.
4. Stewards selected by TNL are not eligible to receive compensation from the DAO or working groups for their work as a Steward.
11. **Amendments**
1. These rules may be amended at any time by passing a Social Proposal.
## Next Steps
This proposal will be open for voting on Snapshot for five days. This vote will be a single choice vote. You may vote 'for' or 'against' the proposal, or choose to abstain from the vote.
By voting 'for' this proposal, you are voting in favor of creating four foundational working groups and establishing rules related to the creation, management, and dissolution of working groups within the ENS DAO, as provided in this proposal.
Given the time of year, Steward nominations and elections for the First Term of 2022 will be delayed until the beginning of 2022.
The Nomination Window for the First Term of 2022 will be open between 9am UTC on January 5, 2022 and 9am UTC on January 8, 2022. Any individual is eligible to nominate themselves to be a Steward of a working group or working groups during this Nomination Window. All Eligible Persons who satisfy the requirements set out in rule 5.4 and 5.5 will be included on the ballot for the First Term elections. More details about the nomination process will be available in the call for nominations released prior to the Nomination Window opening.
The Election Window for the First Term of 2022 will be open for 120 hours, commencing at 9am UTC on January 10 2022. Following the election, in accordance with rule 7.3, newly elected Stewards will assume the responsibilities of stewardship within working groups at 9am UTC on January 16, 2022, for the remainder of the First Term.
To stay up to date on developments regarding working groups, please follow the DAO-Bulletin channel in the ENS Discord Server and follow [@ens_dao](https://twitter.com/ens_dao) on Twitter.
---
# [EP4.8] [Social] Amend working groups rules to extend to a full year
::authors
| **Status** | Passed |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266) |
## Abstract
The intent of this proposal is to modify the working group guidelines to enhance the DAO's ability to attract and retain talent.
## Motivation
By increasing the term length, we aim to enable potential stewards to allocate more time to the DAO and to establish a transparent and predictable compensation structure for the delegates and token community.
### Proposed changes
#### Regarding term changes
These amendments are designed to extend the stewardship term, allowing candidates to commit more substantially to the DAO and benefit from a more stable financial prospect.
There are numerous small textual changes that change the two terms into a single one.
**Current:**
> 3.2. Stewards will be elected to serve within working groups for a set period of time (hereafter known as a 'Term' or 'Terms').
>
> 3.3. There are two Terms each calendar year:
>
> 1. The first Term commences at 9am UTC on January 1 each year and ends immediately prior to the commencement of the second Term ('First Term'); and
> 2. The second Term commences at 9am UTC on July 1 each year and ends immediately prior to the commencement of the First Term of the following year ('Second Term').
**Proposed:**
> 3.2. Stewards will be elected to serve within working groups for a set period of one calendar year (hereafter known as a 'Term').
>
> 3.3. The Term for Stewards commences at 9am UTC on January 1 each year and ends immediately prior to the commencement of the Term of the following year.
Some minor changes also occur in sections 4 and 5 just to the same effect. [See the full diff below](https://github.com/ensdomains/governance-docs/pull/44/files).
#### Regarding changes to fair compensation guidelines
The aim here is to bolster the transparency surrounding compensation practices, benefiting voters and delegates and enhancing the DAO's capacity to attract and maintain skilled contributors. These are proposed amendments to section 11 (compensation for stewards)
**Proposed Addition:**
> 10.4. The Meta-Governance working group are responsible for defining standards for fair compensation ('Compensation Guidelines').
>
> 10.5. The Compensation Guidelines shall be defined prior to the Nomination Window for each term and can only take effect for the following term. Summary
The proposed changes intend to make Steward compensation more predictable and transparent, which serves the dual purpose of attracting more talent to the space as well as being fully transparent towards the electors.
## Voting
The vote is an approval voting meaning you need to vote to approve or reject each individual change. Changes will only take effect if the vote for approval on each section is higher than the rejection
---
# [EP5.2] [Executable] Commence Streams for Service Providers
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep5-2-executable-commence-streams-for-service-providers/18615) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/63865530602418424570813160277709124551851041237648860550576561576702951975816) |
This EP will initiate the Streams for Service Providers as selected on [EP4.9](https://docs.ens.domains/dao/proposals/4.9)
## Abstract
[EP 4.7](https://docs.ens.domains/dao/proposals/4.7) proposed the concept of Streams for service providers and set its budget at $3.6M per annum. EP4.9 selected the following Service providers and their annual budget:
| Service Provider Name | Annual Stream |
| ------------------------ | ------------- |
| ETH.LIMO | $500,000.00 |
| NameHash Labs | $600,000.00 |
| Resolverworks.eth | $700,000.00 |
| Blockful | $300,000.00 |
| Unruggable | $400,000.00 |
| Wildcard Labs | $200,000.00 |
| Ethereum Follow Protocol | $500,000.00 |
| Namespace | $200,000.00 |
| UNICORN.ETH | $200,000.00 |
The Metagov Working Group has decided this will be implemented in the following manner:
1. Superfluid has been selected as the stream platform (details of which can be found on [this RFP](https://discuss.ens.domains/t/rfp-stream-platform-for-ens-service-providers/18448))
2. A new wallet, the "[Stream Management Pod](https://app.safe.global/settings/setup?safe=eth:0xB162Bf7A7fD64eF32b787719335d06B2780e31D1)" has been created as a Global.Safe (Gnosis Safe) wallet. It requires 3 out of 5 signatures to execute any transaction. Its members are the 3 Metagov Stewards, the secretary and the DAO Governor contract.
3. **This EP includes 5 Executable Transactions**, as follows:
1. **APPROVE** Superfluid contracts to wrap one month worth of USDC streaming (300,000 USDC);
2. **WRAP** one month worth of USDC streaming (300,000 USDC to USDCx);
3. **START A STREAM** to the Stream Management Pod of 0.114155251141552512 USDC per second [1];
4. **APPROVE** AutoWrap [2] contract to wrap an additional 5.1M USDC (this, added to the already wrapped 300k, should be enough to cover 18 months of the stream, after that a new EP will be required to keep the stream running);
5. **ENABLE AUTO-WRAP** to keep wrapping USDC to USDCx on a monthly basis (lower limit 200,000, upper limit 500,000).
4. Meanwhile the Stewards are reaching out to all representatives of the stream providers. Alex Urbelis, the general Counsel for ENS Labs, has drafted a service provider agreement and is acquiring the services of Urbelis to run a basic KYC and Sanctions protection. All Service Providers will be required to go through this process before receiving their first stream.
5. On February 1st Stream Management Pod will start creating individual streams to the Organizations who have completed the steps outlined in bullet #4. Because of Superfluid unique no-upfront-capital-lockup streams, it means that when all of 9 Service Providers Streams start, the Stream Management Pod will be forwarding 100% of the money it receives directly into the Service Providers. [3]
### Notes
[1]: USDC has a precision of 6 decimal places, but SuperUSDC has a precision of 18 decimals. The value of 0.114155251141552512 per second corresponds to approximately 9863.01 USDC per day and in 3,600,000.000000000018432 USDC per year in a non-leap year. In leap years (such as the current one) it will mean an extra $9.8K is paid in the 29th of February.
[2]: [Autowrapper](https://etherscan.io/address/0x1D65c6d3AD39d454Ea8F682c49aE7744706eA96d#code) is a [series](https://etherscan.io/address/0x30aE282CF477E2eF28B14d0125aCEAd57Fe1d7a1#code) of [contracts](https://etherscan.io/address/0x1D65c6d3AD39d454Ea8F682c49aE7744706eA96d#code) developed by superfluid. When pinged (and anyone would be able to ping it), if the amount of tokens that the DAO has wrapped is below the lower limit, it will automatically wrap new tokens in order to keep the stream running. The goal is to reduce smart contract risk. If Superfluid is somehow hacked, ENS will not lose more than the upper limit set here (about 50 days worth of funds). If both Superfluid AND the autowrapper are hacked at the same time, ENS cannot lose more than the maximum USDC allowance (18 months worth of funds).
[3]: Any capital that remains in the pod (due to the difference between the day this EP is executed and the day the last Service Provider stream is active) will be still considered under the DAO's ownership and will be used only as a buffer (if autowrapper fails, we will have some weeks to resolve it without interrupting payments) and to solve any logistical issues with service providers. It will NOT count towards Metagov's budget nor it will it be used to any other purpose than the service provider management.
## Specification
We will now details the transactions to be executed on this EP:
| Operation | Target | Decoded Calldata | Raw Calldata |
| ------------------------------------------------------------ | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| approve USDCx SuperToken contract to transfer 300k USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 (USDC) | `function approve(address 0x1BA8603DA702602A8657980e825A6DAa03Dee93a, uint256 300000000000)` | 0x095ea7b30000000000000000000000001ba8603da702602a8657980e825a6daa03dee93a00000000000000000000000000000000000000000000000000000045d964b800 |
| wrap 300k USDC to USDCX | 0x1BA8603DA702602A8657980e825A6DAa03Dee93a (Super-USDC) | `function upgrade(uint256 300000000000000000000000)` | 0x45977d03000000000000000000000000000000000000000000003f870857a3e0e3800000 |
| start flow to Safe with the flowrate of 0.1141... per second | 0xcfA132E353cB4E398080B9700609bb008eceB125 (Superfluid) | `function setFlowrate(address 0x1BA8603DA702602A8657980e825A6DAa03Dee93a, address 0xB162Bf7A7fD64eF32b787719335d06B2780e31D1, int96 114155251141552512) ` | 0x57e6aa360000000000000000000000001ba8603da702602a8657980e825a6daa03dee93a000000000000000000000000b162bf7a7fd64ef32b787719335d06b2780e31d100000000000000000000000000000000000000000000000001958f989989a980 |
| approve auto-wrap for 5.1M | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 (USDC) | `function approve(address 0x1D65c6d3AD39d454Ea8F682c49aE7744706eA96d, uint256 5100000000000` | `0x095ea7b30000000000000000000000001d65c6d3ad39d454ea8f682c49ae7744706ea96d000000000000000000000000000000000000000000000000000004a36fb03800` |
| create auto-wrap schedule | 0x30aE282CF477E2eF28B14d0125aCEAd57Fe1d7a1 (Autowrapper) | `function createWrapSchedule (address 0x1BA8603DA702602A8657980e825A6DAa03Dee93a, address 0x1D65c6d3AD39d454Ea8F682c49aE7744706eA96d, address 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, uint64 3000000000, uint64 1814400, uint64 4320000` | `0x5626f9e60000000000000000000000001ba8603da702602a8657980e825a6daa03dee93a0000000000000000000000001d65c6d3ad39d454ea8f682c49ae7744706ea96d000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000001baf80000000000000000000000000000000000000000000000000000000000041eb00` |
All contracts have verifiable code. You can use tools such as the [Calldata decoder](https://tools.deth.net/calldata-decoder) to verify the correctness of the calldata and etherscan to look deeper into them. You can also _simulate the transactions yourself_ using [this repo](https://github.com/d10r/ens-streams).
The terms for the createWrapSchedule are the address of the superToken (super-usdc), the address of the Strategy contract, the address of the base token (USDC), the expiry (set to the far future), lowerLimit and upperLimit. These last two are set in seconds and mean that if, when the autowrapper is pinged, the stream has less than 21 days in it's runway, then it will automatically wrap 50 days worth of funds.
---
# [6.8] [Executable] Revoke root controller role from legacy ENS multisig
::authors
| **Status** | Passed |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-6-8-executable-revoke-root-controller-role-from-legacy-ens-multisig/20644) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/83558494563463316177076768398348085020294312678713085260352382286714788322618) |
## Abstract
We have identified that the legacy ENS multisig, which originally controlled ENS before the DAO was created, still has the 'controller' role on the ENS root. This means that a majority of multisig keyholders could create or replace any ENS TLD other than .eth. .eth is locked and cannot be modified by the DAO or anyone else.
In order to correct this oversight, this proposal revokes the legacy multisig's controller role from the root contract.
## Specification
Call `setController` on the ENS `Root` contract at `0xaB528d626EC275E3faD363fF1393A41F581c5897`, passing in the address of the legacy multisig, `0xCF60916b6CB4753f58533808fA610FcbD4098Ec0`.
## Transactions
Address
Value
Function
Argument
Value
0xaB528d626EC275E3faD363fF1393A41F581c5897
0
setController
address
0xCF60916b6CB4753f58533808fA610FcbD4098Ec0
controller
false
---
# [EP 5.21] [Social] Governance Security Bounty
::authors
| **Status** | Passed |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/temp-check-governance-security-compensating-blockful-for-preventing-a-potential-attack-on-the-ens-dao/19710) |
| **Votes** | [Snapshot](https://snapshot.box/#/s:ens.eth/proposal/0x5067725bef9cde0de0024adedb653a7a1752aefa726adf628b77048d85821a6d) |
## Summary
This proposal aims to compensate the blockful team for their work in identifying, analyzing, reporting and mitigating a severe vulnerability in ENS DAO's governance structure.
## Background
In March 2024, blockful uncovered a critical vulnerability that could have led to a [~$150M](https://dune.com/steakhouse/ens-steakhouse) theft and protocol capture. Their subsequent work led to the implementation of the Security Council, significantly enhancing ENS DAO's resilience against attacks.
## Contribution Details
The team involved is a [different](https://discuss.ens.domains/t/blockful-service-provider-reports/19553#p-54163-other-contributions-not-related-to-service-provider-scope-14) squad than the one working on the scope of the [ENS service provider](https://discuss.ens.domains/t/blockful-service-provider-reports/19553). It was developed by 2 researchers, 1 smart contract engineer and 4 different auditors the team has worked with previously. Summing up to ~600 hours, the scope includes:
- Comprehensive vulnerability assessment and risk analysis: **[Here](https://mirror.xyz/research.blockful.eth/-PfMduhpxdypPrutofr6099T4ROpsAmX0fPNbvDgR_k)** is our detailed security report.
- Data analysis of ENS governance metrics and study of past DAO attacker's behaviors.
- Design, development and deployment of the Security Council contract and multisig.
- The Security Council was thought with several key features to balance security and decentralization.
- Smart contract implementation and testing ([GitHub](https://github.com/blockful-io/security-council-ens))
- Governance proposal drafting and support [[1](https://snapshot.org/#/ens.eth/proposal/0xf3a4673fe04a3ecfed4a2f066f6ced1539a5466d61630428333360b843653c54), [2](https://snapshot.org/#/ens.eth/proposal/0xa0b1bfadf6853b5b0d59d3c4d73c434fc6389339887d05de805361372eb17c3a), [3](https://www.tally.xyz/gov/ens/proposal/42329103797433777309488042029679811802172320979541414683300183273376839219133)]
More details can be found on the links above for past proposals and the [report](https://mirror.xyz/research.blockful.eth/-PfMduhpxdypPrutofr6099T4ROpsAmX0fPNbvDgR_k).
## Compensation Rationale
As a team that is totally bootstrapped and never received any investment, this support us to keep it sustainable with the resources invested towards this initiative. The requested amount represents fair compensation for:
- The potential loss prevention of ~$150M, capture of the DAO and protocol. The attack is anything but theoretical and there are actually many groups of investors who specialize in "risk free value raiders". They have exerted the attack on other DAOs before. Currently there are [unknown whales](https://etherscan.io/address/0x245445940b317e509002eb682e03f4429184059d#tokentxns) buying ENS for +450 days and have ~2M ENS, showing how feasible the scenario is, more than the average quorum, in one wallet.
- A critical code bug bounty in [ENS is $250k USDC](https://immunefi.com/bug-bounty/ens/scope/#assets). Our work was much beyond identifying and disclosing.
- Significantly lower cost compared to standard rates charged by other security service providers in the DAO space, which typically demand liquid compensation. An example is that Open Zeppelin (one of the most reputable players in security) [charges $4M/year at Compound](https://compound.finance/governance/proposals/76), which recently [suffered](https://mirror.xyz/research.blockful.eth/v0GEP49oXP1gzMDlyP91-S4XIa8PIOd0vKq-6R8f54I) this type of attack.
- Months of dedicated work by the team involved (researchers, devs and auditors).
- The long-term value added to ENS through enhanced security.
- Our commitment to ENS's long-term success and continued contribution, as evidenced by the 2-year vesting schedule.
## Compensation Structure
- Total amount: 100k USDC + 15k vested ENS tokens
- Vesting period: 2 years
- Vesting start date: April 8, 2024 (date of initial research disclosure)
- Vesting schedule: Linear vesting
## Benefits to ENS DAO
- Sets a positive precedent that **responsible vulnerability disclosure and correction are rewarded**, encouraging future security contributions
- Preserves DAO treasury liquidity by using part of the bounty in ENS tokens instead of USDC or ETH
- Enhances governance security by increasing the number of engaged, security-focused token holders
## Conclusion
By approving this compensation, ENS DAO acknowledges the critical importance of security research and proactive governance improvements. The vesting structure ensures ongoing commitment and aligns incentives for continued contribution to ENS's security and stability.
## Success Criteria
For this social proposal to pass, the following quorum and voting requirements must be met:
Quorum: The proposal must receive a minimum of 1% of the total supply of $ENS (1 million votes) in the form of "For" and "Abstain" votes combined. "Against" votes do not count towards quorum.
Approval: Once the quorum is reached, the proposal requires a simple majority (>50%) of "For" votes among the "For" and "Against" votes to pass. "Abstain" votes do not count towards the approval calculation.
---
# [EP 5.28] [Executable] Reimbursement of eth.limo's ongoing legal fees
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/ep-5-28-executable-reimbursement-of-eth-limo-s-ongoing-legal-fees/20004) |
| **Votes** | [Agora](https://agora.ensdao.org/proposals/15212507956777005315602309329914215169878587763966450377165643673108805753590) |
## Description
### Summary
This proposal aims to reimburse eth.limo for ongoing legal fees related to the operation of the eth.limo/eth.link gateway services.
### Background
#### The Human Costs of Public Goods
At eth.limo we continuously strive to deliver a high quality ENS gateway experience. We understand the critical utility that our service provides and we have dedicated countless hours to developing and maintaining the gateway infrastructure that powers thousands of dApps and dWebsites alike.
Speaking beyond purely technical requirements, one aspect of operating eth.limo that is often overlooked is the human cost. We are a small team with limited resources who are dedicated 24/7 to ensure that we respond to support requests in a timely fashion, work with other cutting edge ecosystem projects for integrations, and provide on-call availability for any server-side issues that might arise. In addition, we are constantly handling abuse complaints and other matters that often go unmentioned.
We believe in public goods and the utility they provide, which is why we have sacrificed time with friends and family in order to fulfil our obligations to the ENS and broader Web3 communities by constantly working to ensure a stable and available user experience.
#### The Legal Costs of Public Goods
Operating public infrastructure comes with a unique set of challenges, many of which we were not expecting, such as enforcement and dispute-related legal fees. Being on the frontlines of bridging Web2 → Web3 means that we are often the first point of contact for law enforcement, abuse complaints, and legal and regulatory requests. As one can imagine, this quickly begins to take a toll on our financial resources and mental health.
At present, eth.limo has been labouring under US federal requests, as a third-party, that has dragged on for months and will likely continue to do so well into 2025. We are currently unable to provide further details regarding the nature of this matter, but rest assured, as soon as we are permitted to, we will provide a more fulsome summary to the Web3 community.
As a US company, we are legally required to cooperate with the US Federal Government in response to certain types of lawful requests. Such required compliance has proven to be an extreme financial burden in the form of fees and expenses from our lawyers and emotional distress not just on us individually, but to our families as well.
We negotiated significant discounts from our counsel, who are well-versed in Web3, and who recognize the importance of eth.limo and the significant public good service it provides. At present, we have no way to anticipate expected future legal costs associated with this specific set of legal requests, nor are we able to forecast any additional legal matters or proceedings that may arise as a result of maintaining eth.limo as a public good.
This has the indirect effect of limiting our ability to grow and scale the eth.limo service, as well as to pursue future plans relating to additional integrations and roadmap efforts. To put this into perspective, we have already spent close to $250k USD in legal fees over the past few months. Without additional funding, this very likely could consume our remaining financial resources, leaving us without the ability to continue to operate eth.limo as a public good.
## Links
[Temperature Check](https://discuss.ens.domains/t/temp-check-reimbursement-of-eth-limos-ongoing-legal-fees/19976/1)
## Specification
This executable proposal will initiate a transfer of 240,632.38 USDC from the ENS DAO treasury to ethdotlimo.eth. This amount represents the ongoing legal fees related to the operation of the eth.limo/eth.link gateway services.
### Transaction Details
- **From**: ENS DAO Treasury (0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7)
- **To**: USDC Token Contract (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)
- **Recipient**: ethdotlimo.eth (0xB352bB4E2A4f27683435f153A259f1B207218b1b)
- **Amount**: 240,632.38 USDC (240632380000 considering USDC's 6 decimal places)
- **Purpose**: The reimbursement of eth.limo for ongoing legal fees related to the operation of the eth.limo/eth.link gateway services.
This transaction calls the `transfer` function of the USDC contract, transferring 240,632.38 USDC to eth.limo's address.
## Calldata
```json
{
"target": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"value": 0,
"calldata": "0xa9059cbb000000000000000000000000b352bb4e2a4f27683435f153a259f1b207218b1b0000000000000000000000000000000000000000000000000000003806ceba60"
}
```
### Rationale
By approving this compensation, ENS DAO acknowledges the importance of providing eth.limo with reimbursement of its legal fees so it can continue to operate a free and public ENS gateway that enables users to access Ethereum-native dApps and content.
---
# [EP1.3.1] [Executable] Q1 & Q2 2022 Meta-Governance WG Budget
::authors
| **Status** | Passed |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Discussion Thread** | [Forum](https://discuss.ens.domains/t/11101) |
| **Votes** | [Tally](https://www.tally.xyz/gov/ens/proposal/115615865324623814833258987703837575663427750121726187103053182962864855260310) |
_Note: This was previously numbered EP7.1._
### Summary
The goal of the Meta-Governance working group (MG WG) is to manage the administration of the ENS DAO. The MG WG shall focus on the following three areas over the Q1/Q2 2022 Term.
1. Treasury Management
2. Governance process and DAO structures
3. DAO Tooling
The following budget is requested to provide the MG WG with the resources to work on the areas mentioned above over the Q1/Q2 2022 Term.
### Proposed Budget
**Elected Steward Compensation (2,700 $ENS)**
- Elected steward compensation: 100 $ENS for each 3 elected stewards / month x 6 months = 1,800 $ENS.
- Lead coordinator compensation, to be paid to the steward that is the lead coordinator or other WG members who take on lead coordinator responsibilities, at the discretion of the Stewards: 150 $ENS / month x 6 months = 900 $ENS
#### Subgroups
**Treasury Management ($20k USDC)**
- Management and diversification strategies.
**DAO Governance ($20k USDC)**
- Documentation related to governance processes within the DAO
- Administer experiments related to onboarding, engaging, and valuing contributors within the DAO.
**DAO Tooling ($10K USDC)**
- Develop dashboards to improve transparency and visibility on DAO and ENS metrics.
The MG WG stewards may choose to purchase $ENS with the USDC requested and distribute a mix of USDC and $ENS to contributors as they see fit.
#### Total MG WG Funding Request for Q1/Q2 2022
Steward related compensation: 2,700
$ENS MG WG funding: $50k USDC
---
# Moderator Checklists [Process documentation and checklists for DAO moderators handling proposals]
## Advancing a proposal to a vote
When the author of a Draft Proposal asks for it to be advanced to a vote, and you agree, follow the below steps:
- [ ] Modify the title to include the next sequential EP number (eg `[EP4] ...`).
- [ ] Move from Draft Proposals to Active Proposals category.
- [ ] Delete Discourse poll (if any).
- [ ] [Create a Snapshot vote](https://snapshot.org/#/ens.eth/create):
- [ ] 5 day duration.
- [ ] Simple voting unless otherwise called for in the proposal.
- [ ] Copy and paste the title and contents of the proposal to the vote.
- [ ] Add a link to the forum thread at the bottom.
- [ ] Replicate the proposal in the governance docs:
- [ ] Create the header table with a link to the forum thread and snapshot vote.
- [ ] Copy and paste the title and contents of the proposal.
- [ ] Set the status to 'Active'.
- [ ] Submit as a PR for the docs.
- [ ] Edit the forum thread to link to the Snapshot vote at the top.
- [ ] Send a forum DM to the 'delegates' group announcing that the proposal is up for voting.
- [ ] Send a tweet announcing that the proposal is up for voting.
## Proposal pass/rejection
When a proposal's voting period concludes, follow the below steps:
- [ ] Update the proposal in the governance docs:
- For an Executable Proposal that just passed its Snapshot vote, set the status to 'Awaiting Execution'.
- Otherwise, set the status to 'Passed' if it passed and 'Rejected' if it did not.
- [ ] Update the forum thread:
- [ ] Remove the Snapshot/Tally link at the top.
- [ ] List the current status (as above) at the top of the proposal.
- [ ] If the proposal is Passed or Rejected, lock the thread.
- [ ] If the proposal is an Executable Proposal and passed its snapshot vote:
- Do the "Executing a proposal" checklist below if it is ready to be executed.
- Otherwise, add a note to the forum thread about when it will be scheduled for execution.
- [ ] If the proposal is a Constitutional Amendment:
- Merge the PR to the constitution if it passed.
- Close the PR otherwise.
- [ ] If the proposal requires any other action, alert those responsible of the need to enact it.
- [ ] Send a tweet announcing the result of the proposal.
## Executing a proposal
When an Executable Proposal is ready to be executed, follow the below steps. If you do not have sufficient voting power to submit an executable proposal, ask someone else to do this for you.
- [ ] Construct the executable proposal on the interface of your choice, such as [Tally](https://www.withtally.com/governance/ens/proposal/new) or [OZ Defender](https://defender.openzeppelin.com/#/admin/contracts/mainnet-0x323A76393544d5ecca80cd6ef2A560C6a395b7E3/newAdminAction?), following the instructions in the proposal.
- [ ] Have someone else double-check the proposal is structured correctly for you.
- [ ] Optionally, use [Tenderly](https://dashboard.tenderly.co) to simulate the effects of submitting the proposal directly to the timelock contract to check it has the expected effect.
- [ ] Submit the proposal to the chain.
- [ ] Add a link to the vote on Tally to the top of the proposal on the forum thread.
- [ ] Update the status of the proposal in the governance docs to 'Active'.
- [ ] Send a forum DM to the 'delegates' group announcing the proposal is up for execution, with a link to the Tally proposal page.
- [ ] Send a tweet announcing that the proposal is up for voting.
---
# Governance Process [An overview of the ENS DAO's governance processes, and how you can get involved]
This document is a suggested process for developing and advancing ENS Governance Proposals. It is a living document intended to be owned, modified and enforced by the ENS community.
## Venues
[discuss.ens.domains](https://discuss.ens.domains) is a Discourse forum for governance-related discussion. Community members must register for an account before sharing or liking posts. Registering for the forum allows community members to post in the general forum; for access to the working groups, fill out the [participant request form](https://airtable.com/shrv2xP39SmuCcd5j).
There are three workstream categories: [Meta-Governance](https://discuss.ens.domains/c/meta-governance/28), [Public Goods](https://discuss.ens.domains/c/public-goods/37), and [ENS Ecosystem](https://discuss.ens.domains/c/ens-ecosystem/32). Each category has subcategories for each of the steps of the governance process described below.
### Snapshot
[Snapshot](https://snapshot.org/#/ens.eth/) is a simple voting interface that allows users to signal sentiment off-chain. Votes on snapshot are weighted by the number of ENS delegated to the address used to vote.
### Governance portals
[Tally](https://tally.ensdao.org) and [Agora](https://agora.ensdao.org) are governance portal that allows token holders to delegate their votes, and allows delegates to create and vote on binding proposals.
## Getting Work Done
You are probably here because you want the DAO to _do_ something. The primary mechanism by which the DAO gets things done is via "Requests for Proposal" (RFPs). An RFP is a request from the DAO for contributors to offer to do work on its behalf, and receive compensation in return.
Anyone who identifies a need can write an RFP, and if the RFP is passed, anyone can write a proposal in response and be awarded the work. Even if you believe you can do the work yourself, you will still need to pass an RFP in order to be awarded the work (and corresponding compensation) by the DAO.
RFPs vary in detail and complexity. An RFP for improving the DAO's documentation may only be a paragraph or two long, and proposals for it will be equally short. At the other extreme, an RFP for managing the DAO's funds may be lengthy, and a successful proposal could be multiple pages justifying the proposer's ability to take on the job.
### The RFP Process
RFPs all follow this process:
1. Write a draft RFP ([template here](https://github.com/ensdomains/docs/blob/master/src/public/governance/rfp-template.md)) and post it as a discussion thread in the appropriate working group on [the DAO forum](https://discuss.ens.domains/). At a minimum, RFPs must:
a. Explain the need for the RFP and describe the work to be done - the scope of work and deliverables.
b. Specify the requirements for a winning bid - criteria for selection.
c. Provide a timeline for submissions and completion of the work.
d. Nominate a party who will select a winning bid and approve & disburse compensation (the RFP manager). Normally this will be the working group who adopts the RFP.
e. Specify a maximum budget for the RFP.
2. Incorporate feedback from DAO participants into your draft. When you believe it is ready, tag the stewards of the working group and request they consider adopting it.
3. If the stewards agree to adopt your RFP, they will decide if it can be paid out of WG funds, or if it needs a DAO wide vote.
a. If the RFP can be paid out of WG funds, they will set a submission period and post it as an active RFP.
b. Otherwise, the stewards will create an executable proposal (or, they may ask you to do this) asking the DAO as a whole to approve the RFP. The proposal should contain the RFP. The executable component should specify approvals from the DAO funds to the RFP manager in the amount of the maximum budget for the proposal.
4. Once the RFP is approved - either by the WG or by a DAO-wide vote - the submission period begins. You or a WG steward should create a post on the DAO forum for proposals, and anyone can submit a proposal to this thread.
5. Once the submission period is concluded, the RFP manager selects a winning bid. Normally the manager will be the stewards of the working group who has adopted your RFP.
6. The author of the winning proposal commences the work. As they meet milestones specified in the RFP and their proposal, they can request compensation from the RFP manager, who disburses it from the allocated funds.
## Passing a Proposal
### Types of Proposal
There are three main types of governance proposals you can make:
1. **[Executable Proposal](https://github.com/ensdomains/docs/tree/master/src/public/governance/executable-proposal-template.md):** This is a proposal for a series of smart contract operations to be executed by accounts the DAO controls. These can include transfers of tokens as well as arbitrary smart contract calls. Examples of this include allocating funding to a workstream multisig wallet, or upgrading an ENS core contract. Executable proposals have a quorum requirement of 1% and require a minimum approval of 50% to pass.
2. **[Social Proposal](https://github.com/ensdomains/docs/tree/master/src/public/governance/social-proposal-template.md)**: This is a proposal that asks for the agreement of the DAO on something that cannot be enforced onchain. Examples of this include a proposal to change the royalty percentage for the ENS secondary market on OpenSea, or a petition to the root keyholders. Social proposals have a quorum requirement of 1% and require a minimum approval of 50% to pass.
3. **[Constitutional Amendment](https://github.com/ensdomains/docs/tree/master/src/public/governance/constitutional-amendment-template.md)**: This is a social proposal that asks the DAO to amend the constitution. Your draft proposal should include a [diff](https://en.wikipedia.org/wiki/Diff) showing the exact changes you propose to make to the constitution. Rules for amending the constitution are set in the constitution itself, and currently require a quorum of 1% and a minimum approval of two thirds to pass.
### **Phase 1: Temperature Check — Discourse**
The purpose of the Temperature Check is to determine if there is sufficient will to make changes to the status quo.
To create a Temperature Check, ask a general, non-biased question to the community on [discuss.ens.domains](https://discuss.ens.domains) about a potential change (example: “Should ENS decrease registration costs for 3-letter domains?”). Forum posts should be in the "DAO-wide -> Temperature Check" category.
Temperature checks are informal and optional; it's up to you to use the feedback to decide if you want to proceed further with your proposal.
### **Phase 2: Draft Proposal — GitHub**
The purpose of the Draft Proposal is to establish formal discussion around a potential proposal.
To create a Draft Proposal, [create a new governance proposal](https://github.com/ensdomains/governance-docs/new/main/governance-proposals) in the governance-docs repository on GitHub. Start by copying the template for an [executable proposal](executable-proposal-template.md), [social proposal](social-proposal-template.md), or [constitutional amendment](constitutional-amendment-template.md), as appropriate. Once you have written your proposal, create a Draft Pull Request for it. Start a new post in the DAO-wide -> Draft Proposals" category with a link to the PR for discussion.
Reach out to your network to build support for the proposal. Discuss the proposal and solicit delegates to provide feedback on it. Be willing to respond to questions on the Draft Proposal topic and in comments on the pull request. Share your viewpoint, although try to remain as impartial as possible.
If your proposal is an executable proposal, you will need to specify the actions your proposal will take while it is in draft stage. You may wish to wait until the proposal is stable before doing this. The executable proposal template explains how to do this.
If your proposal is a constitutional amendment, you will need to produce a diff showing the exact changes you are proposing to make. The easiest way to do this is to go to the [constitution](/dao/constitution), click "Edit on GitHub", then click the pencil icon to edit the document in a fork. You can then create a pull request via the GitHub UI and include this in your proposal. You should do this in a separate branch to your draft proposal; while the proposal will be merged as soon as it goes to a vote, the amendment will only be merged if the proposal passes.
Once you are confident the proposal is in a stable state, you can proceed to phase 3.
### **Phase 3: Active Proposal — Snapshot / Governance Portal**
Use GitHub to flag your PR as Ready for Review. A contributor will:
1. Merge your PR if it meets the requirements.
2. Assign your proposal a proposal number in the form EP###.
3. Schedule the proposal for a snapshot vote.
If your proposal is a Social Proposal or a Constitutional Amendment, that's it! If the snapshot vote passes, the proposal is passed and you are done.
If your proposal is an Executable Proposal, you will now need to submit it to the governor contract for voting onchain.
To enact an Executable Proposal:
1. Ensure at least 100k ENS is delegated to your address in order to submit a proposal, or find a delegate who has enough delegated ENS to meet the proposal threshold to propose on your behalf.
2. Call the propose() function of the ENS governor (at [governor.ensdao.eth](https://etherscan.io/address/0x323a76393544d5ecca80cd6ef2a560c6a395b7e3)) to deploy your proposal.
Once the propose() function has been called, a seven day voting period is started. Ongoing discussion can take place on your proposal post. If the proposal passes successfully, a two day timelock will follow before the proposed code is executed.
## **Governance Terminology**
**ENS**: An ERC-20 token that designates the weight of a user’s voting rights. The more ENS a user has in their wallet, the more weight their delegation or vote on a proposal holds.
**Delegation**: ENS holders cannot vote or create proposals until they delegate their voting rights to an address. Delegation can be given to one address at a time, including the holder’s own address. Note that delegation does not lock tokens; it simply adds votes to the chosen delegation address.
**Executable Proposal**: An executable proposal is a type of proposal that is executed by the governance contract through timelock. It can replace the governance contract, transfer tokens from the community treasury, or perform an almost infinite range of other on-chain actions. In order to create a proposal, an address must have at least 0.1% (100k ENS) of all ENS delegated to their address. Proposals are stored in the “proposals” mapping of the Governor smart contract. All proposals are subject to a 7-day voting period.
**Quorum**: In order for a vote to pass, a certain percentage of ENS tokens must vote in the affirmative. The current quorum requirements are:
- Executable Proposals: 1%
- Social Proposals: 1%
- Constitutional Amendments: 1%
The purpose of this quorum is to ensure that the only measures that pass have adequate voter participation.
**Voting on Executable Proposals**: Users can vote for or against single proposals once they have voting rights delegated to their address. Votes can be cast while a proposal is in the “Active” state. Votes can be submitted immediately using “castVote” or submitted later with “castVoteBySig” (For more info on castVoteBySig and offline signatures, see EIP-712). If the majority of votes (and a 1% quorum of ENS) vote for a proposal, the proposal may be queued in the Timelock.
**Voting Period**: Proposals on Snapshot have a 5 day voting period. Once an executable proposal has been put forward, ENS community members will have a seven day period (the Voting Period) to cast their votes.
**Timelock**: All governance actions are delayed for a minimum of 2 days by the timelock contract before they can be executed.
---
import { Avatar } from '../../components/Avatar'
# ENS DAO Stewards
The DAO is governed through a democratic process in which all major matters are decided through a vote open to all holders of governance tokens. Those who wish can also delegate their voting power, entrusting somebody else to keep tabs on the latest DAO matters.
Delegates can be chosen or switched at any time, often without cost, via [Tally](https://www.tally.xyz/gov/ens), [Agora](https://agora.ensdao.org/) or other apps.
For more day-to-day matters, there are Stewards who are elected for a one-year term. The election happens every year on December 10 and lasts for 5 days. Stewards make decisions on governance, hold public meetings, and decide on grants, sponsorships, and other matters. Stewards are divided into different working groups, each reflecting their specific focus.
Read the [full active rules governing the Working Groups](/dao/wg/rules)
## Stewards for 2025 Term
#### Meta-Governance Working Group
| | | |
| ---------------------------------------- | --------------------------------------- | ------------------------------------------ |
| Spence / 5pence.eth | Alex / netto.eth | Cam / daostrat.eth |
#### ENS Ecosystem Working Group
| | | |
| --------------------------------------- | --------------------------------------- | ---------------------------------------- |
| slobo.eth | limes.eth | Donnie / daemon.eth |
#### Public Goods Working Group
| | | |
| ----------------------------------------- | ---------------------------------------- | ------------------------------------------------- |
| coltron.eth | simona.eth | sovereignsignal.eth |
| |
## Past Stewards
### 2024
#### Meta-Governance Working Group
| | | |
| ---------------------------------------- | -------------------------------------- | ------------------------------------------- |
| Spence / 5pence.eth | Alex / avsa.eth | Marcus / estmcmxci.eth |
#### ENS Ecosystem Working Group
| | | |
| --------------------------------------- | --------------------------------------- | ------------------------------------- |
| slobo.eth | limes.eth | 184.eth |
#### Public Goods Working Group
| | | |
| ----------------------------------------- | ---------------------------------------- | ---------------------------------------- |
| coltron.eth | Simona / simona.eth | vegayp.eth |
### 2023 Q3/Q4
#### Meta-Governance Working Group
| | | |
| -------------------------------------- | ---------------------------------------- | ------------------------------------------- |
| Nick Johnson / nick.eth | Spence / 5pence.eth | Katherine Wu / katherine.eth |
#### ENS Ecosystem Working Group
| | | |
| --------------------------------------- | --------------------------------------- | ------------------------------------- |
| slobo.eth | limes.eth | 184.eth |
#### Public Goods Working Group
| | | |
| ----------------------------------------- | ---------------------------------------- | ---------------------------------------- |
| coltron.eth | Simona / simona.eth | vegayp.eth |
### 2023 Q1/Q2
#### Meta-Governance Working Group
| | | |
| -------------------------------------- | ---------------------------------------- | ------------------------------------------- |
| nick.eth | simona.eth | Katherine Wu |
#### ENS Ecosystem Working Group
| | | |
| --------------------------------------- | --------------------------------------- | --------------------------------------- |
| slobo.eth | limes.eth | yambo.eth |
#### Public Goods Working Group
| | | |
| -------------------------------------- | ----------------------------------------- | ---------------------------------------- |
| avsa.eth | coltron.eth | vegayp.eth |
### 2022 Q3/Q4
#### Meta-Governance
| | | |
| ----------------------------------------- | ---------------------------------------- | -------------------------------------- |
| coltron.eth | simona.eth | nick.eth |
#### ENS Ecosystem
| | | |
| ------------------------------------------ | ------------------------------------------- | --------------------------------------- |
| bobjiang.eth | validator.eth | slobo.eth |
#### Community
| | | |
| --------------------------------------- | ----------------------------------------- | ------------------------------------------- |
| limes.eth | coltron.eth | validator.eth |
#### Public Goods
| | | |
| --------------------------------------------- | ---------------------------------------------- | -------------------------------------- |
| anthonyware.eth | ceresstation.eth | avsa.eth |
### 2022 Q1/Q2
#### Meta-Governance
| | | |
| -------------------------------------- | --------------------------------------------- | --------------------------------------- |
| jmj.eth | simona.eth | james.eth |
| | | |
| nick.eth | leontalbert.eth | |
#### ENS Ecosystem
| | | |
| --------------------------------------- | ----------------------------------------- | ------------------------------------------ |
| ginge.eth | slobo.eth | bobjiang.eth |
| | | |
| nick.eth | jefflau.eth | |
#### Community
| | | |
| ----------------------------------------- | ------------------------------------------- | -------------------------------------------- |
| limes.eth | coltron.eth | spencecoin.eth |
| | | |
| brantly.eth | validator.eth | |
#### Public Goods
| | | |
| ----------------------------------------- | ---------------------------------------------- | -------------------------------------- |
| sumedha.eth | ceresstation.eth | avsa.eth |
| | | |
| matoken.eth | ricmoo.eth | |
---
# ENS DAO Working Group Rules
_This document represents the current state of the Working Group Rules as created by [EP0.4](https://snapshot.box/#/s:ens.eth/proposal/0x899ead1d9b9b98f63f6a60dc0939bef55dbe365e78c6a550f07be969a47f148b), and amended by [EP1.8](https://snapshot.box/#/s:ens.eth/proposal/0xc7186cf8bebe47600f8d847e76f7971ea97b48bc04eda1e07780aff91fb6410d) and [EP4.8](https://snapshot.box/#/s:ens.eth/proposal/0x26a5c8dec547837495707e70446d1e7cd874a91f75753c602998f6e70083a266). These should represent the canonical version of the rules and any social proposal to amend it should include a PR to this document._
:::note
The numbering system of EP's was changed after Working Group rules were established, which is why the above proposals have different numbers than Snapshot displays.
:::
## 1. Formation of Working Groups
1. To create a new working group, a social proposal, as defined by the ENS governance documentation ('Social Proposal'), must be put forward and passed by the DAO.
2. A Social Proposal to create a new working group must demonstrate that the new working group is needed and the work cannot be undertaken within an existing working group.
## 2. Dissolution of Working Groups
1. A working group can be dissolved by passing a Social Proposal requesting the dissolution of a working group or working groups.
2. If an active proposal is put forward to dissolve a working group, all working group funds, including outgoing payments, within that working group, are to be frozen with immediate effect, pending the outcome of that vote.
3. Upon the dissolution of a working group, any and all unspent working group funds from that working group, at the time of dissolution, must be immediately returned to the DAO treasury, without delay.
## 3. Working Group Stewards
1. Each working group shall be managed by three stewards (hereafter a 'Steward' or 'Stewards').
2. Stewards will be elected to serve within working groups for a set period of one calendar year (hereafter known as a 'Term').
3. The Term for Stewards commences at 9am UTC on January 1 each year and ends immediately prior to the commencement of the Term of the following year.
4. Stewards are responsible for overseeing the operation of working groups in accordance with these rules and the ENS DAO constitution.
5. The responsibilities of Stewards include, but are not limited to:
1. Requesting working group funds from the DAO in accordance with these rules;
2. Approving the creation of sub-groups or workstreams within a working group to undertake work and/or carry out specific projects or tasks;
3. Dissolving sub-groups or workstreams within a working group;
4. Using discretion to make working group funds available to sub-groups, workstreams, or contributors within a working group;
5. Using discretion to disburse working group funds to people and/or projects in accordance with the ENS DAO constitution; and
6. Acting as keyholders of working group multi-sigs.
## 4. Steward Eligibility and Nominations
1. Any individual is eligible to nominate themselves to be a Steward of a working group within the DAO ('Eligible Person').
2. To be eligible for the election for the annual Term, Eligible Persons must nominate themselves between 9am UTC on December 6 and 9am UTC on December 9 ('Nomination Window').
3. An Eligible Person may nominate themselves to become a Steward of a working group during the Nomination Window, by meeting the requirements set out in a call for nominations posted in the relevant working group category of the ENS governance forum.
4. An Eligible Person who completes the steps outlined in rule 4.3 above during the Nomination Window and receives 10,000 signed votes to support their nomination will be included in the ballot as a nominee in the election for Stewards that takes place following that Nomination Window ('Nominee').
## 5. Steward Elections
1. Elections for working group Stewards for the upcoming year will take place by a vote of governance token holders using signed messages and will be open for 120 hours, commencing at 9am UTC on December 10 each year ('Election Window').
2. The top-ranked Nominees from the working group vote held during the Election Window will fill any available positions for the role of Steward for those working groups for the upcoming Term, based on the order in which they are ranked in the vote.
3. A Nominee elected to serve as a Steward may not take up the role of Steward for more than two working groups during their Term.
## 6. Delay of Nominations or Elections
1. In the event that nominations or elections for Stewards take place after a Nomination Window or after an Election Window, the nomination process and/or elections shall take place, as otherwise prescribed in rules 4 and 5 above, as soon as is practicable after the missed Nomination Window or missed Election Window.
2. In the event that an election takes place outside of an Election Window and after the commencement date of a new Term, outgoing Stewards from the previous Term shall stay in their positions as working group Stewards until immediately prior to 9am UTC the day following the end of the election, which, for the avoidance of doubt, is 120 hours after voting in those elections commenced.
3. In the event that an election takes place outside of an Election Window and after the commencement date of a new Term, newly elected Stewards will assume the responsibilities of stewardship within working groups at 9am UTC the day following the end of the election, as defined in rule 6.2 above, for the remainder of that Term.
## 7. Removal and Replacement of Stewards
1. Stewards may be removed at any time by:
1. a Social Proposal passed by the DAO; or
2. a simple indicative majority vote among Stewards of all working groups, with the outcome of that vote communicated in the relevant working group category of the ENS governance forum.
2. Stewards may step down from their position at any time by communicating their intention to step down in the ENS governance forum.
3. In the event that a Steward is removed, steps down, or is unable to continue as a Steward, for whatever reason, prior to the end of a Term, a new election must be held to fill any vacant Steward positions, in accordance with rule 6 above.
## 8. Lead Stewards
1. Each working group must appoint a lead Steward within the first five days of a Term (hereafter a 'Lead Steward' or 'Lead Stewards').
2. Only current elected Stewards of a working group are eligible to serve as Lead Stewards within a given working group.
3. Lead Stewards may be appointed or removed from that role at any time by a simple indicative majority vote among the Stewards of a working group, with the outcome of that vote communicated in the relevant working group category of the ENS governance forum.
4. In the event that a Lead Steward steps down from the position or is removed as a Lead Steward before the end of a Term in accordance with rule 8.3 above, a new Lead Steward must be appointed within five calendar days.
5. A Steward who is appointed to serve as a Lead Steward of a working group will remain in that position, as Lead Steward, from the date of appointment until the end of their elected Term as a Steward or until they are removed as a Lead Steward in accordance with rule 8.3 above or until they are removed as a Steward in accordance with rule 7 above.
6. Lead Stewards are responsible for the operational management and administration of working groups and are expected to provide regular updates to the DAO in the ENS governance forum related to working group progress, achievements, and challenges.
7. The responsibilities of Lead Stewards include, but are not limited to:
1. Acting as a representative of a working group;
2. Managing resource requests from sub-groups, workstreams, and contributors within a working group;
3. Initiating the disbursement of working group funds on an as-needed basis;
4. Providing reports of working group spending in the ENS governance forum; and
5. Maintaining open communications with DAO participants in the ENS governance forum.
## 9. DAO Secretary
1. At the start of each Term, the current Stewards of each working group shall collaborate to appoint an individual who will serve as the secretary of the DAO (hereafter 'Secretary' or 'Secretaries').
2. The Secretary may be appointed or removed from that role at any time by a majority vote of all elected Stewards in a given Term with the outcome of that vote communicated in the ENS governance forum.
3. The Secretary will remain in that position, as Secretary of the DAO, from the date of appointment until the end of a given Term or until the date at which they are removed from that position in accordance with rule 9.2 above.
4. Secretaries are eligible to receive fair compensation for their work as Secretary of the DAO.
5. Compensation for the Secretary of the DAO is to be paid by the Meta-Governance Working Group using funds requested in accordance with rule 10 below.
6. Any individual is eligible to be appointed as the Secretary of the DAO, including past and present working group Stewards.
7. The Secretary is responsible for managing working relationships and communications across working groups as well as performing administrative duties for the DAO.
8. The responsibilities of the Secretary include, but are not limited to:
1. Managing a DAO-wide calendar;
2. Coordinating and attending working group meetings where possible and ensuring meeting summaries are posted in the ENS governance forum;
3. Assisting Stewards with coordination challenges within working groups; and
4. Acting as a multi-sig keyholder for each working group.
## 10. Working Group Funds
1. To request working group funds, Stewards of all working groups will collaborate to submit an active executable proposal, as defined by the ENS governance documentation ('Collective Proposal'), to the DAO during the months of January, April, July, and October each calendar year (each a 'Funding Window').
1. In order for a working group to have a funding request included in a Collective Proposal submitted to the DAO during a Funding Window, the funding request must have passed as a Social Proposal in the same Funding Window.
2. In the case of an emergency, where working group funds are needed by a working group outside of a Funding Window, an executable proposal, as defined by the ENS governance documentation, may be submitted at any time by a Steward of a working group to request funds from the DAO.
2. Working group funds requested and approved in accordance with rule 10.1 above are to be paid out into separate working group multi-sigs controlled by the DAO.
3. Each working group multi-sig must have four keyholders, made up of three current elected Stewards for that working group and the Secretary of the DAO for that Term, with no other keyholders permitted.
4. Working group funds may be disbursed from working group multi-sigs with three-of-four keyholder signing.
5. Stewards of a given working group shall have the discretion to reallocate funds approved in a Collective Proposal where appropriate and where it is not in conflict with any rules of the DAO, DAO bylaws, or the ENS DAO constitution.
## 11. Compensation for Stewards and Lead Stewards
1. Stewards are eligible to receive fair compensation for their work as a Steward or Lead Steward in the DAO.
2. All requests for Steward or Lead Steward compensation must be detailed in a Collective Proposal for working group funds submitted to the DAO in accordance with rule 10.1 above.
3. Stewards may not receive compensation for their role as a Steward or Lead Steward outside of that compensation expressly provided for in a Collective Proposal submitted to the DAO in accordance with rule 10.1 above.
4. The Meta-Governance working group are responsible for defining standards for fair compensation ('Compensation Guidelines').
5. The Compensation Guidelines shall be defined prior to the Nomination Window for each term and can only take effect for the following term.
## 12. Amendments
1. These rules may be amended at any time by passing a Social Proposal.
---
# Welcome to ENS DAO [The ENS DAO governs the ENS protocol and treasury]
## Context for ENS
- [**ENS landing page**](https://ens.domains): Where you can register and manage ENS names.
- [**X.com**](https://x.com/ensdomains): The official ENS Twitter account.
- [**Discord**](https://chat.ens.domains): Where the users can get support and the community can hang out.
## Context for Governance
- [**Forum**](https://discuss.ens.domains): For discussion on governance proposals and working group operations.
- [**Snapshot**](https://snapshot.org/#/ens.eth): For off-chain proposals.
- [**Agora**](https://agora.ensdao.org): For onchain proposals and token delegation.
- [**Tally**](https://www.withtally.com/governance/ens): For onchain proposals and token delegation.
## Onboarding & Participation
[**Participation Request Form**](https://airtable.com/shrv2xP39SmuCcd5j): To request write-access for restricted categories of the Discourse Forum, please fill in this form.
---
# ENS DAO Constitution
The ENS constitution is a set of binding rules that determine what governance actions are legitimate for the DAO to take.
Each article has examples of permissible and non permissible actions. These examples are illustrative and should not be considered a binding part of the text of the constitution itself.
## I. Name ownership shall not be infringed
ENS governance will not enact any change that infringes on the rights of ENS users to retain names they own, or unfairly discriminate against name owners' ability to extend, transfer, or otherwise use their names.
### **Examples**
**Permissible**: ENS governance may enact a change affecting the registration or extension costs of all names based on transparent criteria such as length, as long as it pursues a goal outlined in this constitution.
**Not Permissible**: ENS governance must not enact a change increasing or reducing the extension costs of a list of existing ENS names, as this would unfairly benefit or penalise a handpicked group.
## II. Fees are primarily an incentive mechanism
The primary purpose of registration fees is as an incentive mechanism to prevent the namespace becoming overwhelmed with speculatively registered names. A secondary purpose is to provide enough revenue to the DAO to fund ongoing development and improvement of ENS. ENS governance will not enact any fee other than for these purposes.
### **Examples**
**Permissible**: ENS governance may increase the price of name registrations in order to address excessive speculative registrations induced by a price that is set too low, or because the current price is insufficient to fund ongoing ENS operations at a reasonable level.
**Not Permissible**: ENS governance must not enact a change imposing a fee for claiming DNS domains inside ENS, because such a fee would be purely an income generating measure and not an incentive mechanism.
## III. Income funds ENS and other public goods
Any income generated to the ENS treasury is to be used first of all to ensure the long-term viability of ENS, and to fund continuing development and improvement of the ENS system. Funds that are not reasonably required to achieve this goal may be used to fund other public goods within web3 as ENS governance sees fit.
ENS governance will not allocate funds to a team or individual who does not commit to uphold the same principles outlined in this constitution in their use of the allocated funds.
### **Examples**
**Permissible**: ENS governance may offer grant funding for a public good unrelated to ENS or Ethereum, so long as doing so does not affect the long-term viability of ENS.
**Not Permissible**: ENS governance must not use the funds to support projects that conflict with the goals of ENS.
## IV. ENS Integrates with the global namespace
In order to facilitate making the most widely usable naming system, ENS aims to integrate with the legacy DNS naming system to the greatest extent possible without sacrificing decentralization of ENS. ENS governance will not enact changes that compromise ENS's ability to do this.
### **Examples**
**Permissible**: ENS governance should grant control of a top-level domain to its owner in the DNS system on request.
**Not permissible**: ENS governance must not create new top-level domains unless those domains have been granted to ENS by a DNS authority.
## V. Amendments to this constitution by majority vote
Any change may be made to this constitution only by two-thirds majority and at least 1% of all tokens participating.
---
# The ENS Foundation
## Why have a legal entity?
Having a legal entity that represents the DAO in the "real world" is valuable for a number of reasons:
- It provides limited liability to DAO participants for the actions of the DAO. Without a legal entity, participants may be individually held liable for anything the DAO as a whole does.
- It is capable of complying with taxation requirements - without a legal entity, DAO participants may be held liable for a proportion of the DAO's income, even if they are not able to access these funds.
- It is capable of entering into contracts with other "real world" entities, of holding assets (including IP rights), and so forth.
For a more detailed discussion of this topic, see [this excellent blog post](https://mirror.xyz/0x954888B7a5C6736F4955dF18B556D8328FD02f61/5K9llACK4tzu5WHL68CM3bBsmSleL_XxJ2kRGYnwp7A).
## What is The ENS Foundation?
The ENS Foundation is a Foundation Company Limited By Guarantee, incorporated in the Cayman Islands. Foundation companies are nonprofits; The ENS Foundation has no shareholders and cannot pay out dividends to its directors or members. For more details on how foundations work, see [this article](https://www.careyolsen.com/briefings/overview-cayman-islands-foundation-companies).
The ENS Foundation has three directors: Nick Johnson, Kevin Gaspar, and Alex Van de Sande. Directors are in charge of the day-to-day running of the foundation.
The ENS Foundation has one supervisor. The supervisor is an administrative role whose job is to make sure that the directors are doing their jobs in accordance with Cayman Islands law. The position of supervisor is filled by a Cayman Islands firm, DS Limited.
The ENS Foundation's Articles of Incorporation give significant powers to the ENS DAO (referred to as "The Council" in the Articles). The DAO may vote to:
- Appoint or remove a director, member, or supervisor.
- Prohibit admitting any members in future.
- Instruct the directors to wind up the foundation, and specify what charity or other foundation should receive the foundation's assets.
Though not specified directly in the Articles, the DAO may also instruct the directors to take action on behalf of the Foundation - such as signing a contract, engaging a company for a service the DAO requires, or delegating some of the directors' powers to a DAO working group.
## Foundation Expenses
Running a Foundation is not free, and comes with some real-world costs. An incomplete list of anticipated expenses includes:
- Registered Office & Secretary Services: $10,000 USD p/a
- Supervisory Services: $30,000 USD p/a
- Agent for service of process: $1,200 USD p/a
- Companies Register Fees: $850 USD p/a
The Directors may ask the DAO for reimbursement of these fees when they are incurred so that the Foundation can continue to operate.
## Documents
For transparency, important documents relating to the Foundation can be found below. Meeting minutes, resolutions, accounts, and other documentation will be uploaded here as it is made available to the directors.
:::note
[M & A - Incorp - The ENS Foundation - 26 October 2021.pdf](https://github.com/ensdomains/governance-docs/blob/main/assets/M%26A%20-%20Incorp%20-%20The%20ENS%20Foundation%20-%2026%20October%202021.pdf)
:::
:::note
[Certificate of Incorporation - The ENS Foundation - 26 October 2021.pdf](https://github.com/ensdomains/governance-docs/blob/main/assets/Certificate%20of%20Incorporation%20-%20The%20ENS%20Foundation%20-%2026%20October%202021.pdf)
:::
:::note
[Dir Res - Stage 2 - The ENS Foundation (27.10.21).pdf]()
:::
:::note
[Resignation and appointment of a director.pdf](https://github.com/ensdomains/governance-docs/blob/main/assets/Resignation%20and%20appointment%20of%20a%20director.pdf)
:::
:::note
[Brantly resignation.pdf](https://github.com/ensdomains/governance-docs/blob/main/assets/Brantly%20resignation.pdf)
:::
---
# The ENS Token
:::info
ENS Airdropped tokens to anyone who held an ENS name on _October 31st, 2021_.
**THERE ARE NO PLANS FOR ANOTHER AIRDROP**. Please be weary of any notices of
airdrops as these could turn out fraudulent.
:::
All major decisions of the ENS DAO governance rely on the ENS Governance Token, which was distributted to ENS owners in 2021. The token can be found at [token.ensdao.eth](https://etherscan.io/address/token.ensdao.eth) on Ethereum Mainnet and is the only official governance token for ENS DAO.
The $ENS token allocation can be seen in the pie chart below.

## Can I recover tokens accidentally sent to the wrong address?
The answer depends on the address the token was sent to.
- If you accidentally sent the token to [token.ensdao.eth](https://etherscan.io/address/token.ensdao.eth) or [wallet.ensdao.eth](https://etherscan.io/address/wallet.ensdao.eth), then it might be recoverable. Contact the [Meta-governance working group](/dao/stewards/) and explain the situation.
- If the tokens were sent to the null address (0x000...0000) or an address with a typo, then the tokens are unrecoverable and there's nothing that anyone can do.
- If the tokens were sent to an exchange or a third party, then contact that third party for help.
---
# ENS DAO Security Council
The ENS DAO Security Council is a 4-of-8 multi-sig with a limited mandate: to cancel malicious proposals that threaten the DAO, particularly those that would compromise the treasury. It was created to address vulnerabilities stemming from low voter participation relative to treasury size.
## Purpose and Powers
The security council is expected to act only in emergency, in the given following situations or similar cases:
- If a proposal goes against the [ENS Constitution](/dao/constitution)
- If a proposal is approved with malicious intent against the DAO longevity/sustainability
- If such [a] proposal is approved by any group of voters, but [they are] directly financially incentivised to vote against the DAO's interests to preserve their own financial stake
- If any approved proposal goes directly against the DAO for the sole benefit of an attacker
The council cannot propose, amend, or otherwise initiate governance actions.
## Time-Limited Authority
The Security Council's cancel authority is time-limited. Two years after deployment (at unix timestamp `1784919179`), any address may call `renounceTimelockRoleByExpiration()` to permanently disable the cancel role, promoting decentralisation over time.
## Relevant Proposals
The veto was first introduced in [Introducing veto.ensdao.eth](https://discuss.ens.domains/t/introducing-veto-ensdao-eth/19088), and approved via social proposal [EP 5.7](/dao/proposals/5.7). Additional discussion occured in [Enable the cancel role on the DAO](https://discuss.ens.domains/t/temp-check-enable-cancel-role-on-the-dao/19090). Member appointments were confirmed in social proposal [EP 5.10](/dao/proposals/5.10), and the Security Council was formally implemented in Executable [EP 5.13](/dao/proposals/5.13).
## Contract Details
[`SecurityCouncil.sol`](https://github.com/blockful-io/security-council-ens) is deployed to [0xb8fa0ce3f91f41c5292d07475b445c35ddf63ee0](https://etherscan.io/address/0xb8fa0ce3f91f41c5292d07475b445c35ddf63ee0) on Ethereum Mainnet. It is owned by the [4-of-8 multi-sig](https://etherscan.io/address/0xaA5cD05f6B62C3af58AE9c4F3F7A2aCC2Cdc2Cc7) specified in [EP 5.10](/dao/proposals/5.10).
---
import { AddressRecords } from '../../components/AddressRecords'
import { ConnectKits } from '../../components/ConnectKits'
import { EnsProfile } from '../../components/EnsProfile'
import { TextRecords } from '../../components/TextRecords'
import { Card } from '../../components/ui/Card'
# Quickstart
Hey there 👋, this is the quickstart guide. If you want to learn the process checkout [everything about ENS in dApps](/web/).
If you would rather just clone an example repository checkout these:
## Starter Kits
## Add to your dApp
This quickstart guide assumes you have a basic understanding of React.
### Installation
```bash [Terminal]
npm install wagmi viem @tanstack/react-query
```
### Showing the User Profile
The below codesnippet demonstrates how you can create a basic user profile section that shows the users ENS name and avatar.
The snippet leverages the [useAccount](https://wagmi.sh/react/hooks/useAccount), [useEnsName](https://wagmi.sh/react/hooks/useEnsName), and [useEnsAvatar](https://wagmi.sh/react/hooks/useEnsAvatar) hooks from wagmi.
```tsx
import { useAccount, useEnsAvatar, useEnsName } from 'wagmi'
export const EnsProfile = () => {
const { address } = useAccount()
const { data: name } = useEnsName({ address, chainId: 1 })
const { data: avatar } = useEnsAvatar({ name, chainId: 1 })
return (
{name}{address}
)
}
```
:::info
ENS resolution always starts from L1 regardless of the chain the user is connected to. This is why we specify `chainId: 1` for Ethereum Mainnet in the wagmi hooks above.
:::
### Text Record Lookups
:::code-group
```tsx [TextRecords.tsx]
// [!include ~/components/TextRecords.tsx]
```
```ts [useEnsTexts.ts]
// [!include ~/hooks/useEnsTexts.ts]
```
:::
### Address Record Lookups
While ENS resolution always starts from Ethereum L1, you can store addresses for other chains in ENS records.
:::code-group
```tsx [AddressRecords.tsx]
// [!include ~/components/AddressRecords.tsx]
```
```ts [useEnsAddresses.ts]
// [!include ~/hooks/useEnsAddresses.ts]
```
:::
---
import { EmbedLink } from '../../components/EmbedLink'
import { Card } from '../../components/ui/Card'
# Subdomains
We believe that any place an address is used, a name should be able to be used instead.
The smart contracts you interact with have names, the deposit address for your favorite exchange has a name, your favorite DAO has a name, or maybe you use subnames to keep your wallets organized.
Luckily, the ENS Protocol has so much to offer for you to play with. There are a variety of ways you can give out subdomains to your apps users, set them up for yourself, or more.
If you are interested in naming smart contracts specifically, check out the [Naming Smart Contracts](/web/naming-contracts) page.
## Different Types of Subnames
ENS subnames come in a variety of forms: L1, L2, and offchain. From a technical perspective, L2 and offchain subnames are quite similar, but there are some tradeoffs to consider when choosing which one to use.
### L1 Subnames
If you own a .eth name like nick.eth and go to create a subname in [the manager app](https://app.ens.domains/nick.eth?tab=subnames), you will be creating a subname on Ethereum Mainnet (L1) by default. This is the simplest way to create a subname with the least amount of moving pieces, but ultimately you are limited by the gas fees of Ethereum Mainnet.
If you'd like to issue L1 subnames to your users, read our guide on [creating an onchain subname registrar](/wrapper/creating-subname-registrar).
### L2 Subnames
Developers can connect an ENS name on L1 with their own smart contracts on any L2 network, and [depending on the implementation](/learn/ccip-read), this could be fully trustless while significantly reducing the cost of issuing subnames.
[Durin](https://durin.dev/) is an opinionated approach to issuing ENS subnames on L2. It takes care of the L1 Resolver and offchain gateway parts of the [CCIP Read stack](/resolvers/ccip-read) for you, so you can focus on the business logic of your L2 smart contracts.
### Offchain Subnames
Offchain subnames are exactly what they sound like - subnames that live in a centralized database on private servers, also powered by [CCIP Read](/resolvers/ccip-read). If your goal is to name a large amount of EVM addresses quickly and cheaply, with a low barrier to entry, offchain subnames might be for you. Often times, managing offchain names is as simple as interacting with a REST API.
From a user perspective, offchain subnames are hardly different than onchain subnames. They will not appear in wallet applications as NFTs like the previous two approaches, but they can resolve all the same data (addresses, text records, etc).
There are multiple API providers that offer programmatic access to offchain subnames such as [NameStone](https://namestone.com/) and [Namespace](https://namespace.ninja/), along with open-source examples like [gskril/ens-offchain-registrar](https://github.com/gskril/ens-offchain-registrar).
---
import { EnsProfile } from '../../components/EnsProfile'
import { Badge } from '../../components/ui/Badge'
import { Card } from '../../components/ui/Card'
# Primary Names
We can all agree 42-character long machine-optimized addresses (eg. 0x225...c3b5) are not aesthetically pleasing.
Fortunately, it is super easy to retrieve a user's preferred name, and this page will show you how.
0xb8c...67d5to
In order to convert them to human-readable names, we use [the reverse registrar](/registry/reverse).
The reverse registrar is a smart contract that allows users to register their preferred name, referred to as their "primary name" for simplicity purposes.
This functionality exists on Mainnet Ethereum today, and is [coming soon to L2s](#l2-primary-names) as well.
## Getting a Primary Name
:::info
**Important**: After retrieving a name from reverse resolution, you **must** verify it by performing a forward resolution on that name to confirm it still resolves to the original address. This prevents spoofing or misconfiguration. If the addresses don't match, display the original address instead of the name.
:::
Looking up a users primary name is very simple. In most web3 libraries (wagmi, viem, ethers, web3py, etc.), you will find a built-in function to do a lookup by address as shown below. In most cases, the library will handle the verification for you.
Note that all ENS requests are made from Ethereum Mainnet, even if your application is on an L2.
:::code-group
```tsx [Wagmi]
// https://wagmi.sh/react/hooks/useEnsName
import { useEnsName } from 'wagmi'
import { mainnet } from 'wagmi/chains'
export const Name = () => {
const { data: name } = useEnsName({
address: '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5',
chainId: mainnet.id, // resolution always starts from L1
})
return
Name: {name}
}
```
```ts [Ethers v5]
const address = '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5';
const name = await provider.lookupAddress(address);
// Always verify the forward resolution
if (name) {
const resolvedAddress = await provider.resolveName(name);
if (resolvedAddress !== address) {
// If verification fails, use the original address
return address;
}
}
```
```ts [Viem]
// https://viem.sh/docs/ens/actions/getEnsName.html
import { publicClient } from './client'
const ensName = await publicClient.getEnsName({
address: '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5',
})
```
```py [Web3.py]
# https://web3py.readthedocs.io/en/latest/ens_overview.html#get-the-ens-name-for-an-address
from ens.auto import ns
name = ns.name('0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5')
```
```go [Go]
package main
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
ens "github.com/wealdtech/go-ens/v3"
)
func main() {
client, _ := ethclient.Dial("https://rpc.ankr.com/eth")
name, _ := ens.ReverseResolve(client, common.HexToAddress("0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5"))
fmt.Println("Name:", name)
// Name: nick.eth
}
```
:::
🎉 And that's it! Now you can turn all your pages from this, to this:
0xb8c2...67d5sent 0.1 ETH to0xd8dA....6045
turns into
nick.ethsent 0.1 ETH tovitalik.eth
## Setting Primary Names
In some cases you might want to encourage users to set their primary name. This might be in the event you are issuing names, or want people to be part of a community.
To do so, you can use the `setName()` function on the [reverse registrar contract](/learn/deployments).
## L2 Primary Names
:::warning
Primary names are currently only supported on Ethereum Mainnet. Soon, primary names are also coming to L2s and are already available on testnets. This will make it possible for users to have an end-to-end experience with ENS on L2.
:::
New contracts will be deployed to popular L2s (starting with Base, OP Mainnet, Arbitrum, Linea, and Scroll) that allow users to declare a name as their primary onchain identity. The contract interface will look something like this (not finalized):
```solidity
/// @notice Sets the `name()` record for the reverse ENS record associated with the calling account.
/// @param name The name to set
/// @return The ENS node hash of the reverse record
function setName(string memory name) external returns (bytes32);
/// @notice Sets the `name()` record for the reverse ENS record associated with the addr provided account.
/// Can be used if the addr is a contract that is owned by an SCA.
/// @param addr The address to set the name for
/// @param name The name to set
/// @return The ENS node hash of the reverse record
function setNameForAddr(
address addr,
string memory name
) external returns (bytes32);
/// @notice Sets the `name()` record for the reverse ENS record associated with the contract provided that is owned with `Ownable`.
/// @param contractAddr The address of the contract to set the name for (implementing Ownable)
/// @param owner The owner of the contract (via Ownable)
/// @param name The name to set
/// @param coinTypes The coin types to set. Must be inclusive of the coin type for the contract
/// @param signatureExpiry The expiry of the signature
/// @param signature The signature of an address that will return true on isValidSignature for the owner
/// @return The ENS node hash of the reverse record
function setNameForOwnableWithSignature(
address contractAddr,
address owner,
string calldata name,
uint256[] memory coinTypes,
uint256 signatureExpiry,
bytes calldata signature
) external returns (bytes32);
/// @notice Sets the `name()` record for the reverse ENS record associated with the addr provided account using a signature.
/// @param addr The address to set the name for
/// @param name The name of the reverse record
/// @param coinTypes The coin types to set. Must be inclusive of the coin type for the contract
/// @param signatureExpiry Date when the signature expires
/// @param signature The signature from the addr
/// @return The ENS node hash of the reverse record
function setNameForAddrWithSignature(
address addr,
string calldata name,
uint256[] calldata coinTypes,
uint256 signatureExpiry,
bytes calldata signature
) external returns (bytes32);
```
This provides multiple ways to set a primary name, usable for EOAs or smart contracts which is a big improvement over the current L1-only implementation.
After retrieving a name from L2 reverse resolution, you must verify it by performing a forward resolution for the corresponding cointype on that name to confirm it still resolves to the original address. Let's look at an example:
Say I own gregskril.eth on mainnet. The name resolves to my EOA `0x179A...9285` because I've set the ETH address for that name. I call `setName("gregskril.eth")` on the Base reverse registrar, and I expect that my primary name is now `gregskril.eth` on Base. But that's actually not the case.
ENS names can resolve to [different addresses on different chains](/web/resolution), and since gregskril.eth in the example above has only specified an ETH address, the verification process will fail. In order to fix this, I need to set the Base address for gregskril.eth which is on L1 in this case. This is done by calling `setAddr(namehash("gregskril.eth"), 0x179A...9285)` on the resolver for the name.
Now that gregskril.eth resolves to `0x179A...9285` when using the Base cointype, and `name(0x179A...9285)` on the Base reverse registrar returns `gregskril.eth`, my primary name is fully set.
### L2 Reverse Registrar Deployments
:::note
The final deployment addresses will be different from the ones below. These are only for testnets.
:::
| L2 Testnet Chain | Address |
| ---------------- | ------------------------------------------ |
| Base Sepolia | 0x00000BeEF055f7934784D6d81b6BC86665630dbA |
| OP Sepolia | 0x00000BeEF055f7934784D6d81b6BC86665630dbA |
| Arbitrum Sepolia | 0x00000BeEF055f7934784D6d81b6BC86665630dbA |
| Scroll Sepolia | 0x00000BeEF055f7934784D6d81b6BC86665630dbA |
| Linea Sepolia | 0x00000BeEF055f7934784D6d81b6BC86665630dbA |
{/* Slightly rewritten from https://github.com/ensdomains/ens-contracts/tree/feature/simplify-reverse-resolver/contracts/reverseRegistrar */}
### Setting Records
On these chains, you can set a primary name for the sender in a few ways:
- `setName()` most simply, using the `msg.sender`'s address
- `setNameForAddr()` for smart contracts that implement the Ownable pattern, where `owner() == msg.sender`
- `setNameForAddrWithSignature()` for EOAs or smart contracts with an ERC-1271 or ERC-6492 signature to set a reverse record on behalf of a user
- `setNameForOwnableWithSignature()` which combines the functionality of the previous two functions to set the reverse record on behalf of a smart contract that implements `Ownable`
### Signatures for setting records
The signature format for `setNameForAddrWithSignature` is:
```
validatorAddress, // 0xAe91c512BC1da8B00cd33dd9D9C734069e6E0fcd for testnets
functionSignature, // 0x2023a04c
name, // string name value
addr, // address to set name for
coinTypes, // array of coinTypes wanting to be set
signatureExpiry // expiry of the signature, up to 1 hour in the future
```
The signature format for `setNameForOwnableWithSignature` is:
```
validatorAddress, // 0xAe91c512BC1da8B00cd33dd9D9C734069e6E0fcd for testnets
functionSignature, // 0x975713ad
name, // string name value
contractAddr, // contract address to set name for
owner, // owner address of contract (i.e. the signature being verified)
coinTypes, // array of coinTypes wanting to be set
signatureExpiry // expiry of the signature, up to 1 hour in the future
```
## Do's and Dont's
Under no situation is it recommended to force a user to change their primary name, nor doing so without clearly notifying the user of what the transaction they are about to execute could modify.
Doing so could be seen as hostile or undesired behaviour by end users and might degrade their experience with your app.
---
import { EnsProfile } from '../../components/EnsProfile'
import { Card } from '../../components/ui/Card'
# Avatars
Personalization of profiles is what makes identity great.
This page covers the very special **avatar** record that enables users to take their avatar with them across the web.
## Getting the user's Avatar
Avatars are an awesome way for users to express themselves. To get the user's avatar, all you need is their **name**. If you only have their address, see [primary names](/web/reverse#get).
The following code snippets let you get the avatar for a user.
:::code-group
```tsx [Wagmi]
// https://wagmi.sh/react/hooks/useEnsAvatar
import { useEnsAvatar } from 'wagmi'
function App() {
const { data: ensAvatar } = useEnsAvatar({
address: 'nick.eth',
chainId: 1, // (1 = Ethereum Mainnet, 11155111 = Sepolia)
})
return (
)
}
```
```ts [Ethers]
// https://docs.ethers.org/v5/api/providers/provider/#Provider-getAvatar
const ensAvatar = await provider.getAvatar('nick.eth')
```
```ts [Viem]
// https://viem.sh/docs/ens/actions/getEnsAvatar.html
import { normalize } from 'viem/ens'
import { publicClient } from './client'
const ensAvatar = await publicClient.getEnsAvatar({
name: normalize('nick.eth'),
})
```
```py [Web3.py]
# https://web3py.readthedocs.io/en/latest/ens_overview.html#read-text-metadata-for-an-ens-record
from ens.auto import ns
avatar = ns.get_text('alice.eth', 'avatar')
```
```go [Go]
package main
import (
"fmt"
"github.com/ethereum/go-ethereum/ethclient"
ens "github.com/wealdtech/go-ens/v3"
)
func main() {
client, _ := ethclient.Dial("https://rpc.ankr.com/eth")
domain, _ := ens.Normalize("nick.eth")
resolver, _ := ens.NewResolver(client, domain)
avatar, _ := resolver.Text("avatar")
fmt.Println("Avatar: ", avatar)
}
```
:::
### The Metadata Service
The [metadata service](https://metadata.ens.domains/docs) is run by ENS Labs. It is a free service web service that allows you to retrieve the
avatar of an ENS name via a web request, as opposed to adding extra logic to your application and interacting
with an ethereum node. This is of course centralised and should be used if absolutely necessary.
## What exactly is an Avatar Record?
An avatar record is simply a [text record](/web/records) that has "avatar" as its key and a URI as its value,
with some rules about what URI schemes are supported and how to process them. For more info, see [ENSIP-11](/ensip/12).
## Supported URI schemes
Clients are expected to support a number of URI schemas, which aren't always web URIs, so the final result you see in your application
will vary depending on how the library you are using has decided to handle avatar records.
- `http(s):` - URI Scheme for HTTP(S) URLs. Libraries will most likely return the result directly.
- `ipfs:` - URI scheme for [IPFS hashes](). Libraries may decide to fetch the result from a public gateway for you.
- `data:` - URI Scheme for [data URIs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs).
Libraries will most likely return the result directly.
- `eip155:` - The URI scheme for EIP-155 identifiers for linking to NFTs on Ethereum based chains. A little complicated
to resolve manually, most libraries should resolve this for you and return the underlying resource.
:::note
For EIP-155 NFT Avatars the nft must be owned by the wallet address the ENS
name resolves to. This is done by checking the `ownerOf` method on the NFT
contract.
:::
## Common schemes that aren't officially supported
- `ethereum:` - The URI scheme for Ethereum addresses
- `bzz:` - The URI scheme for Swarm hashes
## File Information
Avatars come in many different shapes and sizes. Not just the above URI schemas, but also in different file formats, sizes, and more.
Although standards exist for some of these, files are **not required** to follow these standards.
Below is some information about the avatars your app might be loading.
| FileProperty: | Info/Recommendation |
| -------------- | ------------------------------------------------------------------------------- |
| File Extension | Mostly `png`, `jpeg`, `jpg`, `webp`, `gif`, `webm`, but could be anything |
| File Size | We recommend having sensible timeouts |
| Aspect Ratio | We recommend `object-fit: cover` or setting a background color |
| Transparency | We recommend setting a background color as some images may contain transparency |
Luckily most browsers and network libraries have default timeouts to start with, we highly recommend that if you are doing any manual avatar downloading or fetching you add a sensible timeout.
---
# Listing a Users Names
In some cases you might want to show off all names that a user owns. Due to the nature of how the ENS Protocol works under the hood, this might be a slightly more difficult task than expected.
Fortunately, tooling has been developed to accommodate for this and to make it easier.
## Why not all names?
Not all ENS names exist onchain ([learn more about wildcard resolution](/ensip/10)), meaning we don't always know which names a user owns/controls.
The notable exception is [second-level](/terminology#first-layer) [.eth names](/registry/eth). Ownership of these names are onchain and indexable through scanning events on the appropriate smart contracts. Note that this does not necessarily mean address and text records associated with the name are onchain ([read more about offchain resolvers](/resolvers/ccip-read)).
## Guidelines
When using one of the methods described below it is important to keep in mind that you should always allow for a user to manually enter a name, as not all names are indexable.
It is generally recommended to allow users to input a name using an [input box](/web/design#2-resolving-input-fields) and to verify it resolves to the correct address upon user-completion.
## The Graph
The [ENS subgraph](/web/subgraph) indexes all events from relevant smart contracts and exposes them via a GraphQL endpoint. Note that addresses in filters must be lowercased.
ENSjs makes it easy to run common queries on the subgraph with strong type safety. Docs can be found [here](https://github.com/ensdomains/ensjs/tree/main/docs/subgraph).
```graphql
{
domains(where: { owner: "0x225f137127d9067788314bc7fcc1f36746a3c3b5" }) {
name
}
wrappedDomains(
where: { owner: "0x225f137127d9067788314bc7fcc1f36746a3c3b5" }
) {
name
}
}
```
---
import { Card } from '../../components/ui/Card'
# Sign In With Ethereum (SIWE) [A specification that leverages ethereum signatures to perform authentication]
## Specification ([see spec](todo-link))
The SIWE Specification is rather simple. It describes a message format that a user ought to sign using their keys to be able to sign-in.
The message MUST include [a subject](todo-link), [nonce](todo-link), and a [timestamp](todo-link). These parameters prevent replay-attacks, cross-site usage, and more, all while maintaining a very human readable form.
An example payload looks like the following:
```
localhost wants you to sign in with your Ethereum account:
0x225f137127d9067788314bc7fcc1f36746a3c3B5
This is a test statement.
URI: https://localhost/login
Version: 1
Chain ID: 1
Nonce: abcdef1234567890
Issued At: 2023-01-30T00:00:00.000Z
```
## Implementations
The team at [SpruceID](https://login.xyz) has done a phenomenal job writing plug-and-play utilities that let you easily integrate ethereum-based authentication into your project.
Whether you are using [Next.js](todo-link), [React](todo-link), or [OpenID Connect](todo-link).
In addition to the above SIWE has been integrated into [connectkit](todo-link) & more.
---
# Naming Contracts [Learn how to name your smart contracts with ENS]
While it's commonly known that regular user accounts can have [primary names](/web/reverse), it's less known that smart contracts can also have names.
In order for you to manage the primary name of your smart contract, you need to own the [reverse node](/terminology#reverse-node) for the contract address. There are several ways of doing this, depending on if you are actively developing your contract or if it is already deployed.
:::note
To enable reverse resolution, you must set both the reverse record and the ETH address to the contract’s deployed address.
:::
Skip to [Naming Tools](#naming-tools) for a frontend solution to naming your smart contracts.
## New Contracts
Depending on your use case, there are a few ways to set a smart contract's primary name.
If you want to be able to change the name later, you have two options:
- **(Recommended)** Make the contract [Ownable](https://docs.openzeppelin.com/contracts/5.x/access-control) and set yourself as the owner.
- Take ownership of the reverse node (`{address}.addr.reverse`) for the contract.
The Ownable method is preferred since it's widely used and works well with Etherscan.
For contracts without admin permissions, you can set the reverse name in the constructor. This name can't be changed later.
Let's look at a few examples.
### ReverseClaimer.sol
:::note
While this method works perfectly at the ENS protocol level, Etherscan does not index the contract events correctly so it may not appear in their UI.
:::
This is a simple drop-in module that transfers ownership of the reverse node to an address of your choice, which can then update the reverse name at any time.
```solidity
import "@ensdomains/ens-contracts/contracts/registry/ENS.sol";
import "@ensdomains/ens-contracts/contracts/reverseRegistrar/ReverseClaimer.sol";
contract MyContract is ReverseClaimer {
constructor (
ENS ens
) ReverseClaimer(ens, msg.sender) {}
}
```
When you deploy your contract, the deployer account (`msg.sender`) will be given ownership of the reverse node for that contract address. This gives you authorization to call `setName(node, newName)` on the latest public resolver ([resolver.ens.eth](https://etherscan.io/address/resolver.ens.eth)), where `node` is the reverse node for the contract address and `newName` is the name you want to set it to.
To find the reverse node for your contract address, you can use the following viem script:
```ts
import { Hex, keccak256 } from 'viem'
import { namehash } from 'viem/ens'
function getNodeFromParentNodeAndLabelhash(parentNode: Hex, labelhash: Hex) {
return keccak256((parentNode + labelhash.split('0x')[1]) as Hex)
}
const myContractAddress = '0x...' // replace with your contract address
const node = getNodeFromParentNodeAndLabelhash(
namehash('addr.reverse'),
labelhash(myContractAddress.slice(2).toLowerCase())
)
console.log(node)
```
### Ownable (recommended)
:::note
An example of this is [ownable.contract.gtest.eth](https://etherscan.io/address/ownable.contract.gtest.eth#code)
:::
If you want to be able to change the name in the future, you can make your smart contract [Ownable](https://docs.openzeppelin.com/contracts/5.x/api/access#Ownable).
```solidity
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract Contract is Ownable {
constructor(address initialOwner) Ownable(initialOwner) {}
}
```
The [ReverseRegistrar](/registry/reverse) supports the Ownable interface and will let the `owner` of a contract set its primary name without having to add any ENS-specific code.
Once this contract is deployed, call `setNameForAddr(address, address, address, string)` on the Reverse Registrar ([reverse.ens.eth](https://etherscan.io/address/reverse.ens.eth)) from your authorized owner account.
- The first address argument is the address of your contract
- The second address argument is the owner of your smart contract
- The third address argument is the `defaultResolver()` from the Reverse Registrar
- The fourth argument is the ENS name to set it to
### Set a name in the constructor
:::note
An example of this [contract.gtest.eth](https://etherscan.io/address/contract.gtest.eth#code)
:::
If you don't want to be able to change the name in the future, you can inherit the following contract and assign an ENS name directly in the constructor.
```solidity
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
interface ENS {
function owner(bytes32 node) external view returns (address);
}
interface IReverseRegistrar {
function setName(string memory name) external returns (bytes32);
}
// Variation of ReverseClaimer.sol from @ensdomains/ens-contracts that sets the reverse name directly.
contract ReverseSetter {
/// @dev The ENS registry
ENS private constant ens = ENS(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e);
/// @dev Output of namehash("addr.reverse")
bytes32 private constant ADDR_REVERSE_NODE =
0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2;
/// @param name The reverse name to set for this contract's address.
constructor(string memory name) {
IReverseRegistrar reverseRegistrar = IReverseRegistrar(
ens.owner(ADDR_REVERSE_NODE)
);
reverseRegistrar.setName(name);
}
}
```
Using this in your contract is as simple as:
```solidity
import {ReverseSetter} from "./ReverseSetter.sol";
contract Contract is ReverseSetter {
constructor(string memory name) ReverseSetter(name) {}
}
```
## Existing Contracts
If your contract is already deployed you might still be able to set a name for it.
If your contract supports the [Ownable](https://docs.openzeppelin.com/contracts/5.x/api/access#Ownable) interface from OpenZeppelin, read the section above.
### Safe, Multisig & DAO
If your contract is a Safe, Multisig, DAO, or has a function that can send arbitrary ETH calls, you can use the [ReverseRegistrar](/registry/reverse) contract directly to set a name for it.
You might even be able to use the [ENS Manager App](https://ens.app/) inside of your safe app to set a primary name.
## Naming Tools
:::warning
These are 3rd party tools and not officially supported by ENS Labs.
:::
[Enscribe](https://app.enscribe.xyz/) is a tool designed to simplify the process of naming smart contracts with ENS names. The application enables users to deploy new smart contracts with a primary name directly and easily name existing smart contracts.
Enscribe simplifies what is otherwise a multi-step, error-prone process by offering:
- Atomic contract deployment using `CREATE2`
- Naming `Ownable`, `ERC173`, `ReverseClaimer` and `ReverseSetter` contracts as described above
- ENS subname creation, forward resolution and reverse record assignment
- Naming of existing contracts, with an easy way to locate contracts that you've already deployed
Even if you don't own an ENS name, you can still utilize Enscribe's hosted ENS parent, `deployd.eth`, to create subnames like `my-app.deployd.eth` and set them as the primary name for your contract.
To learn more, refer to the [Enscribe Docs](https://www.enscribe.xyz/docs/).
---
import { EmbedLink } from '../../components/EmbedLink'
import { Card } from '../../components/ui/Card'
# Getting Started [Integrate ENS into your dApp]
This section walks you through how to leverage the ENS open standards to improve the user experience of your app.
{/* TODO: Break the following examples into a component to fetch live data */}
## Quickstart
If you are looking to jumpstart your journey with ENS, or you are looking for a quick reference, visit the [Quickstart](/web/quickstart) page.
## Tools and Libraries
ENS is an integral part of the Ethereum ecosystem.
Fortunately, the open-source community is to the rescue, and almost all of the tools and libraries you use today support ENS.
To learn more check out the [tools & libraries section](/web/libraries).
## Avatars, Addresses & Records
Information about a name is fetched from its resolver. This can be done using pre-built features included in popular [web3 libraries](/web/libraries) (recommended), or by calling a resolver contract directly.
If you're interested in interacting with ENS resolvers, you might find the [Resolver Reference](/resolvers/interfaces) section helpful.
## Subnames
---
import { EnsProfile } from '../../components/EnsProfile'
import { SendTransactionDemo } from '../../components/SendTransaction'
import { Card } from '../../components/ui/Card'
# Design Guidelines [Guidelines for designing interfaces that use ENS names]
ENS is a tool to simplify the experience for your users by making blockchain addresses human-readable.
Here are a series of guidelines and tools that will help you make good design choices and better implement ENS in your product.
## When to show ENS names
In every instance where a user might otherwise see an Ethereum address, you can instead display an ENS name (with its avatar, if relevant).
This is true for both read and write operations.
An example of read operations where it's appropriate to show an ENS name is the connected wallet status or representing an action from another user like a vote ([Snapshot is a great example](https://snapshot.box/)).
An example of write operations where it's appropriate to show an ENS name is when a user is inputting an address of any kind (token transfer, smart contract interaction, etc.).
Beyond these use cases, remember that the [ENS Public Resolver](/resolvers/public) allows you to link [different kinds of resources](/web/records) to ENS names.
## 1. Replacing Ethereum addresses with ENS Names
:::note
An ENS name should only be shown in place of an Ethereum address if the user has set a reverse record for their address, and if the reverse record matches the forward resolution. [Learn more about primary names](/web/reverse).
:::
### 1.1 - Displaying ENS names instead of Ethereum addresses
{/* TODO: build a sample UI of ENS names in a list, like votes or messages or transfers */}
When replacing Ethereum addresses with ENS names you should consider these facts and best practices:
- **Design a truncated version of the ENS name:** ENS names can be very long; besides not being character-limited, users can create an infinite number of nested subdomains.
If you do show a truncated version of the name, you should provide a way to view the full name, such as expanding it on hover.
- **Not all ENS names end with .eth**: ENS supports [.eth](/registry/eth) and most DNS TLDs such as [.com, .xyz, and 1200+ others](/dns/tlds).
A correct implementation of ENS treats any dot-separated name as a potential ENS name and will attempt a look-up.
### 1.2 - Always provide an option to see the Ethereum address associated with the ENS name
If you are showing the ENS name in its entirety or a truncated version, you should:
- **Always provide the user a way to display the full Ethereum address**: Notice how if you type "ens.eth" in the [example above](#send_transaction), the resolved ETH address appears under the name.
This is especially important in high-risk situations, such as when the user is about to send a transaction or interact with a smart contract.
- **Allow the user to copy the full Ethereum address**: Allow the user to copy the full address either through a copy button or by selecting it.
- **Optionally give the user a way to automatically open the Ethereum address in a block explorer** such as Etherscan.
- **Optionally show the** **balance amount of signed-in users.** User research shows that users tend to recognise their own Ethereum address through their balance, as well as the address itself.
This is meant only for the currently "signed in" user: only show their own balance and avoid showing the balance of other users.
## 2. Resolving input fields
{/* */}
Input fields where a user is supposed to insert Ethereum addresses should also accept and resolve ENS names. These inputs indicate that the user wants to interact with another user's Ethereum address or contract.
Follow these guidelines to create the best experience:
- **Wait before resolving the ENS name**: Debounce input fields that accept ENS names to avoid unnecessary network calls. You can also wait for the user to type a minimum of 1 character on both sides of the dot before resolving the name. For example, if the user type "ens.", there is no chance of it being a valid ENS name and therefore no need to resolve it. But after the user types "ens.e", it should be treated as a potential ENS name.
- **Don't overwrite the input field with the Ethereum address:** Show the resolved ENS name near the input field instead.
- **Always display both the ENS name** _**and**_ **the Ethereum address together** : Do this after it has successfully been resolved.
## Other guidelines and tips
### Usernames for accounts that don't have an ENS name
You can offer free ENS names to your users which would not only improve their experience in your application, but also across the Ethereum ecosystem.
See [how to issue subdomains](/web/subdomains).
### Caching and updating ENS Names
If your application needs to display many ENS Names in the UI, you can consider **caching** (for a short period of time) the ENS Name after it has been resolved or after the user has added the name in an input field.
Your **optimistic UI** can display the names from cache **in non-risky situations**, in which your user for example is simply browsing, but doesn't need to act or make decisions based on the information displayed.
However, **in all risky situations** \(eg transferring anything of value or interacting with a smart contract\), you should **perform a direct live resolution** and get the most up to date information from the ENS Registry.
Also consider that users can change their information at any time which may not be tracked in the onchain registry, so you should **periodically validate the information you cached**. [Learn more about offchain ENS names](/learn/ccip-read).
### Notes on displaying Ethereum Addresses (with or without ENS names)
Even when ENS names are not available, [research](https://medium.com/@lyricalpolymath/web3designdecisionframework-e84075816515) [shows](https://medium.com/@lyricalpolymath/web3-design-principles-f21db2f240c1) that there are some good practices to follow when displaying Ethereum addresses in dApps.
- **Always show the initial ' 0x '** to indicate it's an address.
- When displaying the name in shorthand versions, **show the first 5 and last 4 characters of the address**.
This is not a security requirement as vanity addresses can be spoofed relatively simply; this is a good practice because some users check the beginning of the name and others check the end of the name.
Also, four is the highest number of elements that our mind can easily chunk, parse and remember well.
- **Always provide a way to display the full Ethereum address.**
## Front-end tools
[Thorin](https://thorin.ens.domains/) is a react component library for the ENS design system.
It provides a set of components that make it easier to follow the guidelines and best practices described above.
---
import { EmbedLink } from '../../components/EmbedLink'
import { EnsProfile } from '../../components/EnsProfile'
import { Card } from '../../components/ui/Card'
# Address Lookup [Learn how to resolve blockchain addresses from human-readable names with ENS.]
The ENS Protocol aims to make it easy to use Ethereum.
It does this by providing a simple way to use human-readable names instead of long machine-readable addresses.
## Getting the users Ethereum Address
The goal here is to take a name, such as `nick.eth`, and convert it to an address, such as `0x225f137127d9067788314bc7fcc1f36746a3c3B5`.
The simplest thing you can do is start with a name, and resolve it to an address.
We call this a "forward lookup".
Think of places where users can enter names, such as sending transactions, chatting, etc.
Note that all dot-separated strings should be treated as potential ENS names, since ENS supports [many TLDs](/dns/tlds). A common mistake is to only treat strings that end in `.eth` as ENS names.
:::code-group
```tsx [Wagmi]
import { useAccount, useEnsAvatar, useEnsName } from 'wagmi'
export const Name = () => {
const { data: ensName } = useEnsAddress({
address: 'luc.eth', // The name to lookup
chainId: 1, // The chain to start resolution on (Ethereum mainnet or testnet)
})
return
{ensName || address}
}
```
```ts [Ethers.js]
const address = await provider.lookupAddress('luc.eth')
```
```ts [Viem]
import { normalize } from 'viem/ens'
import { publicClient } from './client'
const ensAddress = await publicClient.getEnsAddress({
name: normalize('luc.eth'),
})
```
```py [web3.py]
from ens.auto import ns
address = ns.address('alice.eth')
```
```rust [ethers-rs]
let provider = Provider::::try_from("https://mainnet.infura.io/v3/...")?;
let address = provider.lookup_address("luc.eth").await?;
```
```go [go-ens]
package main
import (
"fmt"
"github.com/ethereum/go-ethereum/ethclient"
ens "github.com/wealdtech/go-ens/v3"
)
func main() {
client, _ := ethclient.Dial("https://rpc.ankr.com/eth")
domain, _ := ens.Normalize("luc.eth")
resolver, _ := ens.NewResolver(client, domain)
address, _ := resolver.Address()
fmt.Println("Address:", address.Hex())
}
```
```ts [ensjs]
import { createEnsPublicClient } from '@ensdomains/ensjs'
import { http } from 'viem'
import { mainnet } from 'viem/chains'
const client = createEnsPublicClient({
chain: mainnet,
transport: http(),
})
const subgraphRecords = client.getSubgraphRecords({ name: 'ens.eth' })
const records = client.getRecords({
name: 'ens.eth',
records: {
coins: [...(subgraphRecords?.coins || []), 'BTC', 'ETH', 'ETC', 'SOL'],
texts: [
...(subgraphRecords?.texts || []),
'avatar',
'email',
'description',
],
contentHash: true,
abi: true,
},
})
```
```csharp [nethereum]
var ensService = new Nethereum.ENS.ENSService(web3)
var address = await ensService.ResolveAddressAsync('alice.eth')
```
:::
To learn what happens under the hood when you do a forward lookup, read the [resolution](/resolution) section.
## Multi-Chain Addresses (BTC, LTC, etc)
ENS Names aren't just limited to storing Ethereum addresses.
Any blockchain address (BTC, LTC, SOL, etc.) can be queried by [SLIP-0044](https://github.com/satoshilabs/slips/blob/master/slip-0044.md) coin type or a value derived from an EVM Chain ID (specified in [ENSIP-11](/ensip/11)). This includes Ethereum L2 networks such as OP Mainnet and Base.
For EVM Chains besides Mainnet Ethereum, always use its [ENSIP-11](/ensip/11) coin type, irrespective of being included in SLIP-0044 (like Ether Classic).
The standardization of multichain addresses was first introduced in [ENSIP-9](/ensip/9), and also [EIP-2304](https://eips.ethereum.org/EIPS/eip-2304).
:::note
Regardless of the chain you're resolving an address for, ENS resolution always starts from Ethereum L1.
:::
:::code-group
```tsx [Wagmi]
// https://wagmi.sh/react/api/hooks/useEnsAddress
import { useEnsAddress } from 'wagmi'
import { arbitrum, base } from 'wagmi/chains'
const name = 'gregskril.eth'
const evmChainIdToCoinType = (chainId: number) => {
return (0x80000000 | chainId) >>> 0
}
export const MyAddresses = () => {
// SLIP-0044 Coin Types (see ENSIP-9)
const { data: bitcoinAddr } = useEnsAddress({ name, coinType: 0, chainId: 1 })
const { data: solanaAddr } = useEnsAddress({
name,
coinType: 501,
chainId: 1,
})
// EVM Chain IDs (see ENSIP-11)
const { data: baseAddr } = useEnsAddress({
name,
coinType: evmChainIdToCoinType(base.id),
chainId: 1,
})
const { data: arbitrumAddr } = useEnsAddress({
name,
coinType: evmChainIdToCoinType(arbitrum.id),
chainId: 1,
})
return (
)
}
```
```ts [Viem]
// https://viem.sh/docs/ens/actions/getEnsAddress.html#cointype-optional
const ensName = await publicClient.getEnsAddress({
name: normalize('wagmi-dev.eth'),
coinType: 0, // BTC
})
```
```ts [Ethers.js]
// https://docs.ethers.org/v5/api/providers/provider/#EnsResolver
const resolver = await provider.getResolver('luc.eth')
const btcAddress = await resolver?.getAddress(0)
```
```py [web3.py (Python)]
# https://web3py.readthedocs.io/en/latest/ens_overview.html#multichain-address-resolution
from ens.auto import ns
eth_address = ns.address('alice.eth', coin_type=60)
```
:::
| Network | Coin Type |
| ------------ | ---------- |
| Bitcoin | 0 |
| Litecoin | 2 |
| Dogecoin | 3 |
| Ethereum | 60 |
| Solana | 501 |
| OP Mainnet | 2147483658 |
| Polygon | 2147483785 |
| Base | 2147492101 |
| Arbitrum One | 2147525809 |
... and many many more following [SLIP-0044](https://github.com/satoshilabs/slips/blob/master/slip-0044.md) and [ENSIP-11](/ensip/11)
### Decoding Address Hashes
ENS resolvers store all addresses in bytes, which may have to be encoded to their respective address formats. To do this, we recommend using the [@ensdomains/address-encoder](https://www.npmjs.com/package/@ensdomains/address-encoder) package.
## Advanced
---
# Subgraph
This is a page covering the graph's ENS subgraph. The ENS subgraph indexes on-chain events of second-level .eth names, and DNS imported names.
It allows us to build a reasonable approximation of the ENS names an address owns.
To read more about why not all names (such as Offchain & Gasless Names) show up in the subgraph read the [listing names](/web/enumerate) page.
## The Graph
The Graph is a protocol for indexing and querying data from blockchains. There are multiple subgraphs that you can use to query information about ENS names.
These subgraphs are available for [mainnet](https://api.thegraph.com/subgraphs/name/ensdomains/ens), [sepolia](https://api.studio.thegraph.com/query/49574/enssepolia/version/latest) and [holesky](https://api.studio.thegraph.com/query/49574/ensholesky/version/latest).
:::note
Developers are welcome to use our rate limited API endpoints above for
testing, but it is highly encouraged to [sign up for a free account with
TheGraph](https://thegraph.com/studio/apikeys/) to get your own API key.
:::
## GraphQL Schema
The schema for the ENS subgraph is defined in [/schema.graphql](https://github.com/ensdomains/ens-subgraph/blob/master/schema.graphql).
## Use Cases
There are certain use cases where the graph is better for querying ENS specific information than through the resolution process.
One of such use-cases is querying which NFT names are owned by a specific address.
## Example Queries
One can explore the following examples interactively via the [Graph Explorer Playground](https://thegraph.com/explorer/subgraphs/5XqPmWe6gjyrJtFn9cLy237i4cWw2j9HcUJEXsP5qGtH?view=Playground&chain=arbitrum-one)
### Getting a list of names owned by an account
Ensure the address is lowercase
```graphql
query getDomainsForAccount {
domains(where: { owner: "0xa508c16666c5b8981fa46eb32784fccc01942a71" }) {
name
}
}
```
### Getting the top domain for an account based on the longest registry
```graphql
query getDomainForAccount {
account(id: "0xa508c16666c5b8981fa46eb32784fccc01942a71") {
registrations(first: 1, orderBy: expiryDate, orderDirection: desc) {
domain {
name
}
}
id
}
}
```
returns
```json
{
"data": {
"account": {
"registrations": [
{
"domain": {
"name": "datanexus.eth"
}
}
],
"id": "0xa508c16666c5b8981fa46eb32784fccc01942a71"
}
}
}
```
### Searching for a subdomain
```graphql
query getSubDomains($Account: String = "messari.eth") {
domains(where: { name: "messari.eth" }) {
name
id
subdomains(first: 10) {
name
}
subdomainCount
}
}
```
returns
```json
{
"data": {
"domains": [
{
"name": "messari.eth",
"id": "0x498ada62251a1227664ace8d97b0de2dcc6652ddf61e6fb5d3150f43ccf599e6",
"subdomains": [
{
"name": "subgraphs.messari.eth"
},
{
"name": "bd.messari.eth"
}
],
"subdomainCount": 2
}
]
}
}
```
### Getting the expiry of an ENS domain
```graphql
query getDomainExp($Account: String = "paulieb.eth") {
registrations(
where: { domain_: { name: $Account } }
first: 1
orderBy: expiryDate
orderDirection: desc
) {
expiryDate
}
}
```
returns
```json
{
"data": {
"registrations": [
{
"expiryDate": "1714752524"
}
]
}
}
```
---
import { ConnectKits } from '../../components/ConnectKits'
import { Libraries } from '../../components/Libraries'
# Tools & Libraries [Tools to help you interface with the ENS protocol]
## Quickstart Kits
There are a few plug-and-play kits that you can use to jumpstart your project. These kits will include everything you need to have users connect their wallet, have names showing, avatars, and more, right out of the box!
## Libraries
There are many ways to interface with the ENS Ethereum smart contracts, indexers, and metadata services. Whether you're building a dApp, a backend service, or interacting with ENS from your smart contract, there's a library out there to help you get started.
---
import { EmbedLink } from '../../components/EmbedLink'
import { TextRecords } from '../../components/TextRecords'
import { Card } from '../../components/ui/Card'
# Text Records
With every name come a set of records. These records are key value pairs that can be used to store information about the profile.
Think of this as a user's **digital backpack**. Utalized for storage of preferences, public details, and more.
Text records allow us to attach and read any key value pair from an ENS name. The most popular records have been standardised.
One example of a standardised record is the [avatar record](/web/avatars) which is used to store a user's profile picture.
## Getting Records
To fetch the record for a specific name, you can use one of the following methods:
:::code-group
```tsx [Wagmi]
// https://wagmi.sh/react/api/hooks/useEnsText
import { normalize } from 'viem/ens'
import { useEnsText } from 'wagmi'
export const MyProfile: FC<{ name: string }> = ({ name }) => {
const { data } = useEnsText({
name: normalize('luc.eth'),
key: 'com.twitter',
})
return (
Twitter: {data}
)
}
```
```tsx [Ethers]
// https://docs.ethers.org/v5/api/providers/provider/#EnsResolver
const provider = new ethers.providers.JsonRpcProvider()
const resolver = await provider.getResolver('luc.eth')
const twitter = await resolver.getText('com.twitter')
```
```tsx [Viem]
// https://viem.sh/docs/ens/actions/getEnsText.html
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { normalize } from 'viem/ens'
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
})
const ensText = await publicClient.getEnsText({
name: normalize('luc.eth'),
key: 'com.twitter',
})
```
```python [web3.py]
# https://web3py.readthedocs.io/en/latest/ens_overview.html#text-records
from ens.auto import ns
# set text
ns.set_text('alice.eth', 'url', 'https://example.com')
# get text
url = ns.get_text('alice.eth', 'url')
assert url == 'https://example.com'
```
```go [Go]
package main
import (
"fmt"
"github.com/ethereum/go-ethereum/ethclient"
ens "github.com/wealdtech/go-ens/v3"
)
func main() {
client, _ := ethclient.Dial("https://rpc.ankr.com/eth")
domain, _ := ens.Normalize("luc.eth")
resolver, _ := ens.NewResolver(client, domain)
twitter, _ := resolver.Text("com.twitter")
fmt.Println("Twitter: ", twitter)
}
```
:::
## Types of Records
Here are some of the most commonly used records:
| Name | Usage | Reference | Example |
| ----------- | -------------------------------------------- | --------------------- | ------------------- |
| display | Preferred capitalization | [ENSIP-5](/ensip/5) | Luc.eth |
| avatar | Avatar or logo (see [Avatars](/web/avatars)) | [ENSIP-5](/ensip/5) | ipfs://dQw4w9WgXcQ |
| description | Description of the name | [ENSIP-5](/ensip/5) | DevRel @ ENS Labs |
| keywords | List of comma-separated keywords | [ENSIP-5](/ensip/5) | person, ens |
| email | Email address | [ENSIP-5](/ensip/5) | luc@ens.domains |
| mail | Physical mailing address | [ENSIP-5](/ensip/5) | V3X HQ |
| notice | Notice regarding this name | [ENSIP-5](/ensip/5) | This is a notice |
| location | Generic location (e.g. "Toronto, Canada") | [ENSIP-5](/ensip/5) | Breda, NL |
| phone | Phone number as an E.164 string | [ENSIP-5](/ensip/5) | +1 234 567 890 |
| url | Website URL | [ENSIP-5](/ensip/5) | https://ens.domains |
| header | Image URL to be used as a header/banner | [ENSIP-18](/ensip/18) | ipfs://dQw4w9WgXcQ |
### Other Records
Currently there are a few records that have been standardised. However you are welcome to store any key value pair you desire.
We generally recommend to stick to a pattern, or prefix things with your app or protocol (eg. `com.discord`, or `org.reddit`), as such to avoid collisions.
One of the newer standardised records is the "header" record. This header record, similar to the avatar record, accepts any IPFS, Arweave, EIP155, or regular URL to an image resource.
The image is then displayed as a banner on the profile page and tends to be in a 1:3 aspect ratio.
## Setting Records
When records are loaded they are loaded from the resolver responsible for the name. As resolvers are user controlled, we cannot guarantee a write function is available.
This makes it a more in-depth process to update a users records.
---
# Multichain [L2 & Crosschain Resolution]
## ENS L2
The ENS Labs team recently announced our plans and roadmap for scaling ENS to the entire internet and beyond. You can read more [on our blog](https://blog.ens.domains/post/ensv2), [on X](https://twitter.com/ensdomains/status/1795440186513576318), and [the forums](https://discuss.ens.domains/t/technical-feedback-thread-for-ensv2/19233).
The roadmap involves migrating .eth registrations to a new system, in addition to improved support for existing L2 solutions.
You can find out more on the [changelog](/changelog).
## But isn't ENS on mainnet?
Yes, technically. The resolution process always starts on mainnet. There needs to be, one source of truth after all. However, the name
resolution process can branch off to other chains, offchain gateways and much more.
To read a more in-depth explanation of how resolution works, checkout the [section dedicated to the Resolution Process](/resolution/).
## My dapp is on X but I want ENS
The ENS Protocol can be used on/for any chain!
If you are building a non-mainnet dApp and want to use ENS names simply [add a Mainnet RPC to your Wagmi config](/web/libraries) and specify `chainId: 1` in your config like so:
```tsx
import { useAccount, useEnsAvatar, useEnsName } from 'wagmi'
const Name = () => {
const { data: ensName } = useEnsAddress({
address: 'luc.eth',
chainId: 1, // (1 = Ethereum, 11155111 = Sepolia) // [!code hl]
})
return
{ensName || address}
}
```
And voila! You can now resolve ENS names anywhere! 🎉
---
import { Card } from '../../components/ui/Card'
# Resolution
The process by which we load information about a name is called resolution. It's a simple process, but it's important to understand.
Here is a diagram of some of the contracts involved when resolving a name.
The resolution process involves multiple parts. Most notably the [Registry](/registry/ens), multiple Registrars ([ETH Registrar](/registry/eth), [DNS Registrar](/registry/dns), [Reverse Registrar](/registry/reverse), etc)
and the concept of a [Resolver](/resolvers/quickstart).
## How to resolve
Here is a little peek at what happens under the hood of your favourite library when you do a name lookup.
### 1. Find the Resolver
Every name has a "resolver". A resolver is simply a contract that implements the [resolver specification](/resolvers/quickstart) and can be queried for information about a name.
To get the resolver responsible for a name, you can query [The Registry](/registry/ens) for the `resolver` of a name.
:::code-group
```solidity [Solidity]
ENS.resolver(bytes32 node) view returns (address)
```
```tsx [Wagmi]
// https://wagmi.sh/react/api/hooks/useEnsResolver
import { normalize } from 'viem/ens'
import { useEnsResolver } from 'wagmi'
export const MyResolver = () => {
const { data: myResolver } = useEnsResolver({
name: normalize('luc.eth'), // The name to lookup
})
return
{myResolver}
}
```
```ts [Ethers]
const resolver = await provider.getResolver('luc.eth')
```
```ts [viem]
// https://viem.sh/docs/ens/actions/getEnsResolver.html
import { normalize } from 'viem/ens'
import { publicClient } from './client'
const ensResolver = await publicClient.getEnsResolver({
name: normalize('luc.eth'),
})
```
```py [Web3.py]
# https://web3py.readthedocs.io/en/latest/ens_overview.html#working-with-resolvers
from ens.auto import ns
resolver = ns.resolver('alice.eth')
```
:::
To verify which specifications are implemented by a resolver, you can call the `supportsInterface(bytes4 interfaceID)` on the resolver with the interfaceID you would like to test for.
### 2. Query the Resolver
Now you have found the resolver responsible for the name in question, you can query it for the information you are interested in.
There are many ways you can query the resolver, `addr()` `text()` `contenthash()` `abi()` etc.
If the resolver supports text records, you can call `text()` to get that text record for the name.
More about loading information from a resolver can be found [here](/resolvers/interacting).
#### Wildcard Resolution
In addition, all of the above functions can be sent to the `resolve()` function, specified in [ENSIP-10](/ensip/10).
This allows for not only multicall functionality, but also easier implementation of EIP-3668, and more.
Most clients & many resolvers utilize wildcard resolution as their primary form of resolution.
## Reverse Resolution
Due to the modular nature of how ENS is designed, it is also possible to lookup the "primary name" of an address.
This process actually uses forward resolution under the hood, you read that right - its just forwards resolution.
To look up the primary name of a given address, you must do a resolver lookup for `addr.reverse` and then query the `name()` field on the resolver.
This name field returns the "preferred" name for the address. You should always follow up a reverse lookup with a forward lookup to verify that the resulting name points back to the original address. If the address doesn't match, display the address rather than the reversed name.
```solidity
/// @dev The starting point for all ENS resolution is the Registry
ENS ens = 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e;
/// @dev The node hash for "addr.reverse"
bytes32 ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2;
/// @dev Returns the node hash for a given account's reverse records, `{address}.addr.reverse`
function reverseNode(address addr) public pure returns (bytes32) {
return keccak256(
abi.encodePacked(ADDR_REVERSE_NODE, sha3HexAddress(addr))
);
}
/// @dev Get the reverse record for an address
function getReverseRecord(address addr) public view returns (string) {
bytes32 reverseNodeHash = reverseNode(addr);
// Get the resolver for the reverse node
Resolver resolver = ens.resolver(reverseNodeHash);
// Get the address's preferred name
return resolver.name(reverseNodeHash);
}
```
:::info
**Important**: The client MUST perform a forward resolution on a user's reverse record to verify the address matches the one you are looking up. The example above does not perform this verification.
:::
Please note that many libraries already have functionality to do this. You can read more about it in the [Getting a Primary Name](/web/reverse) section.
---
import { NameProcessing } from '../../components/NameProcessing'
import { Card } from '../../components/ui/Card'
# Name Processing [Normalization and recommendations for how to handle names]
When interacting with the ENS smart contracts directly, it is important to note that names are not stored as strings. [Libraries](/web/libraries) handle name encoding for you when implementing basic name resolution, but you may need to handle the encoding yourself when interacting with the protocol directly.
Below is an interactive tool that shows all the different formats of names and how to implement them.
## Name Normalization
Normalization is the process of canonicalizing a name before running it through the [Namehash](#namehash) algorithm. It is important to always normalize all input, because even one little difference (like a capital vs lowercase character) will cause the namehash to be completely different.
For example, `NaMe.EtH` normalizes to `name.eth`. This ensures that the correct Registry node is used, no matter how the user types in the name.
ENS names are validated and normalized using the [ENSIP-15](/ensip/15) normalization algorithm.
Previously, [UTS-46](https://www.unicode.org/reports/tr46/) was used, but that is insufficient for emoji sequences. Correct emoji processing is only possible with [UTS-51](https://www.unicode.org/reports/tr51/). The [ENSIP-15](/ensip/15) normalization algorithm draws from those older Unicode standards, but also adds many other validation rules to prevent common spoofing techniques like inserting zero-width characters, or using confusable (look-alike) characters. See here for additional discussion on this: [Homogylphs](https://support.ens.domains/en/articles/7901658-homoglyphs)
A standard implementation of the algorithm is available at [@adraffy/ens-normalize](https://github.com/adraffy/ens-normalize.js). This library is used under the hood in [viem](https://viem.sh/docs/ens/utilities/normalize), [ENSjs](https://github.com/ensdomains/ensjs/blob/main/packages/ensjs/src/utils/normalise.ts#L27), and others.
```js
import { normalize } from 'viem/ens'
// Uses @adraffy/ens-normalize under the hood
const normalized = normalize('RaFFY🚴♂️.eTh')
// => "raffy🚴♂.eth"
```
If the name was not able to be normalized, then that method will throw an error. A name is valid if it is able to be normalized.
## Namehash
:::note
You **MUST** [normalize](#normalize) a name before you attempt to create a namehash! If you don't, then the hash you get may be incorrect. Some libraries like [ensjs](https://github.com/ensdomains/ensjs) will automatically do this for you.
:::
In the core ENS registry, names are stored as a hash instead of the raw string to optimize for gas, performance, and more. This hashed value is typically referred to as a `node`. The node is a hex-encoded 32-byte value that is derived from the name using the `namehash` algorithm defined in [ENSIP-1](/ensip/1).
Namehash is a recursive algorithm that hashes each part of the name, then hashes the results together. Because recursive functions aren't very efficient in Solidity, it's usually best to derive the namehash offchain and pass to it a contract. Luckily, there are libraries that do this for us.
:::code-group
```tsx [Viem]
// https://viem.sh/docs/ens/utilities/namehash
import { namehash, normalize } from 'viem/ens'
const normalizedName = normalize('name.eth')
const node = namehash(normalizedName)
```
```ts [Ethers.js]
// https://docs.ethers.org/v6/api/hashing/#namehash
import { ensNormalize, namehash } from 'ethers/hash'
const normalizedName = ensNormalize('name.eth')
const node = namehash(normalizedName)
```
```python [ens-namehash-py]
# https://github.com/ConsenSysMesh/ens-namehash-py
from namehash import namehash
node = namehash('name.eth')
```
```rust [namehash-rust]
// https://github.com/InstateDev/namehash-rust
fn main() {
let node = &namehash("name.eth");
let s = hex::encode(&node);
}
```
```solidity [Solidity]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@ensdomains/ens-contracts/contracts/utils/NameEncoder.sol";
contract MyContract {
function namehash(string calldata name) public pure returns (bytes32) {
(, bytes32 node) = NameEncoder.dnsEncodeName(name);
return node;
}
}
```
:::
### Algorithm
The specification for the namehash algorithm was originally defined in [EIP-137](https://eips.ethereum.org/EIPS/eip-137#namehash-algorithm) (same as [ENSIP-1](/ensip/1)).
It's a recursive algorithm that works its way down until you hit the root domain. For `ens.eth`, the algorithm works like so:
```
namehash('ens.eth') = keccak256(namehash('eth') + labelhash('ens'))
namehash('eth') = keccak256(namehash('') + labelhash('eth'))
namehash('') = 0x0000000000000000000000000000000000000000000000000000000000000000
```
That last line is a special case: The namehash for an empty string (representing the root domain) is 32 null bytes.
If you plug everything in above, you'll end up with the final namehash value:
```
namehash('') = 0x0000000000000000000000000000000000000000000000000000000000000000
labelhash('eth') = keccak256('eth') = 0x4f5b812789fc606be1b3b16908db13fc7a9adf7ca72641f84d75b47069d3d7f0
namehash('eth') = keccak256(namehash('') + labelhash('eth')) = keccak256(0x00000000000000000000000000000000000000000000000000000000000000004f5b812789fc606be1b3b16908db13fc7a9adf7ca72641f84d75b47069d3d7f0) = 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae
labelhash('ens') = keccak256('ens') = 0x5cee339e13375638553bdf5a6e36ba80fb9f6a4f0783680884d92b558aa471da
namehash('ens.eth') = keccak256(namehash('eth') + labelhash('ens')) = keccak256(0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae5cee339e13375638553bdf5a6e36ba80fb9f6a4f0783680884d92b558aa471da) = 0x4e34d3a81dc3a20f71bbdf2160492ddaa17ee7e5523757d47153379c13cb46df
```
This brings us to the final node for ens.eth: `0x4e34d3a81dc3a20f71bbdf2160492ddaa17ee7e5523757d47153379c13cb46df`
### Reverse Nodes
The [Reverse Node](/terminology#reverse-node) is a node in the Registry that can be claimed for any Ethereum account. The name this node represents is `[addr].addr.reverse`, where `[addr]` is the Ethereum public address (lowercase, without the "0x"). These reverse nodes are typically used to set a [Primary Name](#primary-name) for an account.
To generate the namehash for a reverse node:
- Take the input address and:
- Remove the "0x" at the beginning
- Convert all characters to lowercase
- Add `.addr.reverse` to the end
- Run this result through the namehash algorithm
For example, for address `0x481f50a5BdcCC0bc4322C4dca04301433dED50f0`, the name for the reverse node is:
- `481f50a5bdccc0bc4322c4dca04301433ded50f0.addr.reverse`
And the resulting namehash for the reverse node is:
- `0x58354ffdde6ac279f3a058aafbeeb14059bcb323a248fb338ee41f95fa544c86`
## Labelhash
:::note
You **MUST** [normalize](#normalize) a name before you attempt to create a labelhash! If you don't, then the hash you get may be incorrect.
:::
Labelhash is the Keccak-256 hash of a single label (e.g. `name` in `name.eth`), used in places that don't require the full name.
One example of where labelhash is used is in the [BaseRegistar](/registry/eth), since it only supports registering 2LDs (second-level domains, like `name.eth`) and not 3LDs+ (e.g. `sub.name.eth`). The token ID of a second-level .eth name in the BaseRegistar is the uint256 of the labelhash.
:::code-group
```tsx [Viem]
// https://viem.sh/docs/ens/utilities/labelhash
import { labelhash, normalize } from 'viem/ens'
const normalizedLabel = normalize('label')
const hash = labelhash(normalizedLabel)
```
```tsx [Ethers]
// https://docs.ethers.org/v6/api/crypto/#keccak256
import { keccak256 } from 'ethers/crypto'
import { ensNormalize } from 'ethers/hash'
import { toUtf8Bytes } from 'ethers/utils'
const normalizedLabel = ensNormalize('label')
const labelhash = keccak256(toUtf8Bytes(normalizedLabel))
```
```solidity [Solidity]
string constant label = "label";
bytes32 constant labelhash = keccak256(bytes(label));
```
:::
## DNS Encoding
:::note
You **MUST** [normalize](#normalize) a name before you DNS-encode it! If you
don't, then when you pass those DNS-encoded bytes into a contract method,
incorrect namehashes/labelhashes may be derived.
:::
This is a binary format for domain names, which encodes the length of each label along with the label itself. It is used by some of the ENS contracts, such as when wrapping names in the [Name Wrapper](/wrapper/overview) or resolving data with [ENSIP-10](/ensip/10).
:::code-group
```tsx [Viem]
import { packetToBytes } from 'viem/ens'
import { toHex } from 'viem/utils'
const name = 'name.eth'
const dnsEncodedName = toHex(packetToBytes(name))
// 0x046e616d650365746800
```
```tsx [Ethers]
// https://docs.ethers.org/v6/api/hashing/#dnsEncode
import { dnsEncode } from 'ethers/lib/utils'
const dnsEncodedName = dnsEncode('name.eth')
```
```solidity [Solidity]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@ensdomains/ens-contracts/contracts/utils/NameEncoder.sol";
contract MyContract {
function dnsEncode(string calldata name) public pure returns (bytes memory) {
(bytes memory dnsEncodedName,) = NameEncoder.dnsEncodeName(name);
return dnsEncodedName;
}
}
```
:::
### Decoding
To decode a DNS-encoded name, you can use `bytesToPacket()` from ENSjs.
```tsx
import { bytesToPacket } from '@ensdomains/ensjs/utils'
import { hexToBytes } from 'viem/utils'
const dnsEncodedName = '0x046e616d650365746800'
const name = bytesToPacket(hexToBytes(dnsEncodedName))
// name.eth
```
### Algorithm
To DNS-encode a name, first split the name into labels (delimited by `.`). Then for each label from left-to-right:
- One byte to denote the length of the label
- The UTF-8 encoded bytes for the label
- If this is the last label, then one final NUL (`0x00`) byte.
For example, to DNS-encode `my.name.eth`:
- `0x02` (length of the label "my")
- `0x6D79` (UTF-8 encoded bytes of "my")
- `0x04` (length of the label "name")
- `0x6E616D65` (UTF-8 encoded bytes of "name")
- `0x03` (length of the label "eth")
- `0x657468` (UTF-8 encoded bytes of "eth")
- `0x00` (end of name marker)
Final result: `0x026d79046e616d650365746800`
:::note
Since the length of each label is stored in a single byte, that means that with this DNS-encoding scheme, each label is limited to being 255 UTF-8 encoded bytes in length. Because of this, names with longer labels cannot be wrapped in the [Name Wrapper](/wrapper/overview), as that contract uses the DNS-encoded name.
:::
---
import { DNSGrid } from '../../components/DNSGrid'
import { DNSUsageExamples } from '../../components/DNSUsageExamples'
import { EmbedLink } from '../../components/EmbedLink'
# DNS on ENS [ENS supports DNS names, allowing users to import DNS names into ENS.]
The Ethereum Name Service is so much more than just `.eth` names. It is a general-purpose naming system that can be used for any kind of name. This includes DNS names.
DNS functionality was originally introduced in [ENSIP-6](/ensip/6).
## Importing a DNS name
There are currently two ways of importing a DNS name into ENS. Both methods require you enable DNSSEC on your domain, and setup a TXT record.
This record is then verified using smart contracts on the Ethereum blockchain.
To import a name, simply visit the [ENS Manager](https://ens.app), type in your name, and click "Import DNS". You will walked through setting up your DNS records.
Additionally you can read more about the records and specifications here:
## Why DNS on ENS?
ENS aims to extend the existing functionality of the DNS system. This also means that existing DNS names (such as `.com`, `.org`, or `.xyz`) should be able to leverage the benefits of the ENS resolution process.
## DNS Names in the wild
DNS names are widely used and many users may already have one without even realizing it. Some major platforms that issue subdomains of their DNS names include:
## Top-Level Domains
In addition to allowing any DNSSEC enabled name to be imported, ENS also allows existing DNS TLDs to take control of their smart-contract resolution process.
Resulting in even more seamless integration with the DNS system.
A list of all supported TLDs can be found [here](/dns/tlds)
---
import { DNSGrid } from '../../components/DNSGrid'
import { EmbedLink } from '../../components/EmbedLink'
import { EnsProfile } from '../../components/EnsProfile'
import { Card } from '../../components/ui/Card'
# What is the Ethereum Name Service?
The Ethereum Name Service (ENS) is a distributed, open, and extensible naming system based on the Ethereum blockchain.
ENS maps human-readable names like 'alice.eth' to machine-readable identifiers such as Ethereum addresses, other cryptocurrency addresses, content hashes, metadata, and more.
ENS also supports 'reverse resolution', making it possible to associate metadata such as primary names or interface descriptions with Ethereum addresses.
Top-Level Domains (TLDs), like `.eth` and `.test`, are owned by smart contracts called [registrars](/registry/eth), which specify rules governing the allocation of their names.
Enabling seamless interoperability with the DNS (Domain Name System).
## ETH Registrar
The [ETH Registrar](/registry/eth) is the registrar for the `.eth` TLD, it allows for trustless decentralized names to be issued as tokens on the Ethereum Blockchain.
Registration is done through smart contracts, and name ownership is secured by the Ethereum blockchain.
## DNS + ENS
ENS has similar goals to DNS, the existing Internet's Domain Name Service, and aims to extend its capability.
ENS also supports importing DNS names through the use of DNSSEC.
Allowing you to take your `.com`, `.xyz`, or `.art` (and more) into the ENS ecosystem. Read more about DNSSEC names [on this page](/learn/dns).
## Subnames
Because of the hierarchical nature of ENS, anyone who owns a domain at any level can take control of resolution.
Users can create subdomains manually, or take matters into their own hands and write their own resolution logic.
For instance, if Alice owns 'alice.eth', she can create 'pay.alice.eth' and configure it as she wishes.
Or, use a [Custom Resolver](/resolvers/quickstart), and programmatically issue subdomains, for example in an App, Community, or DAO.
## ENS Manager App
You can try ENS out for yourself now by using the [ENS Manager App](https://ens.app/), or by using any of the many ENS enabled applications on [our homepage](https://ens.domains/).
---
import { EnsProfile } from '../../components/EnsProfile'
import { Card } from '../../components/ui/Card'
# Resolution [The ENS Resolution Process]
:::note
This document aims to provide a brief overview of how resolution works, to
read more about resolution checkout the [dedicated Resolution
Section](/resolution).
:::
One of the major parts of the ENS protocol is the resolution process. The Resolution process at its core is the process of converting a human-readable name to a machine-readable address.
Though there is a lot of smart contract magic under the hood, the ENS system consists of two main paths: [Forward Resolution](#forward-resolution), used to go from name to address (and load other extra data), and [Reverse Resolution](#reverse-resolution), used to go from address to name.
## Forward Resolution
Forwards resolution is the process of going **from name to address**. As well as to load the records associated to a name.
These records include but are not limited to **discord**, **twitter**, **github**, **email**, **timezone**, and more.
{/* TODO: Turn this into a component and fetch live data */}
➡️⬇️
ETH Address: 0x5555...3dCa
BTC Address: 1RicMoo...Jyn
Twitter: @ricmoo
Github: @ricmoo
...
Implementing forwards resolution in a dApp can be as simple as using a single line of code!
To learn more about how to implement forwards resolution, check out the [Address Resolution](/web/resolution) documentation.
## Reverse Resolution
Reverse resolution is the process of going **from address to name**. This is a crucial part of the ENS system, as it allows for any address, to be resolved into a human readable name.
Instead of pages filled with addresses, you can now show the names of the people behind the addresses.
0x225...c3B5to
Implementing reverse resolution in a dApp can be as simple as using a single line of code!
To learn more about how to implement reverse resolution, check out the [Address Resolution](/web/reverse) documentation.
---
import { ContractDeployments } from '../../components/ContractDeployments'
import { EmbedLink } from '../../components/EmbedLink'
import { Card } from '../../components/ui/Card'
# Deployments
:::note
This page contains information that is only relevant to developers who would
like to interact with the contract manually. Most libraries will handle this
for you.
:::
ENS is multichain (read more [here](/web/multichain)) and can be used in any application.
In addition to being able to query many address formats and record types, data can be stored [practically anywhere](/learn/ccip-read).
However, resolution needs to start somewhere, so the entrypoint for resolution is Ethereum Mainnet, alongside the most popular testnets.
Ethereum Mainnet
→
BaseArbitrumOffchain...
## Deployments
Listed below you will find a list of latest deployments of registries, resolvers, and more.
### Mainnet
Interact with ENS on Ethereum Mainnet via [app.ens.domains](https://app.ens.domains).
### Sepolia
Interact with ENS on the Sepolia testnet via [sepolia.app.ens.domains](https://sepolia.app.ens.domains).
### Holesky
Interact with ENS on the Holesky testnet via [holesky.app.ens.domains](https://holesky.app.ens.domains).
## But what about multichain?
While the core ENS protocol lives on Ethereum Mainnet, it can be used to resolve data for any chain!
---
# Layer 2 & Offchain Resolution
All ENS resolution starts on Ethereum Mainnet (or testnet).
However, by leveraging [CCIP Read](/resolvers/ccip-read) and [Wildcard Resolution](/ensip/10), name resolution can be taken cross-chain, off-chain, and more.
This allows for a lot of flexibility in how you can use your ENS and for storage of your ENS records on your favourite Layer 2, or even off-chain.
## ENS on Layer 2
In the resolution process, clients first fetch the resolver associated with the name in the ENS registry on L1. That resolver is responsible for telling the client where to find the data associated with the name such as the addresses, text records, etc.
If you want to register and resolve (sub)names from L2, you would write a resolver smart contract that defers resolution to the L2 and ideally verifies that data against the L2's storage proofs posted to L1. This process can be done with the [Unruggable Gateway](https://gateway-docs.unruggable.com/).
An example implementation of Layer 2 resolving is:
### linea.eth
Linea was the first L2 team to build a trust-minimized ENS subname system. Names are stored on Linea, verified with [storage proofs](https://docs.linea.build/developers/tooling/cross-chain/ccip-read-gateway) on L1, and function as ENS subnames such as [greg.linea.eth](https://app.ens.domains/greg.linea.eth). You can try it out [here](https://names.linea.build/).
### clv.eth
Clave is focused on enhancing user experience and security through a mobile wallet that leverages account abstraction and device hardware. Clave accounts come with usernames that are now stored onchain in ZKsync Era, verified with [storage proofs](https://github.com/getclave/zksync-storage-proofs) in L1, and issued as ENS subnames such as [ulas.clv.eth](https://enstate.rs/n/ulas.clv.eth). You can read more about the implementation [here](https://blog.getclave.io/introducing-onchain-clave-usernames-with-ens).
## Primary Names on Layer 2
The process of setting primary names from L2 is under active development. This doc will be updated as more information becomes available.
## Offchain Resolution
Moving resolution processes off-chain offers numerous advantages, including efficiency gains and reduced congestion on the main blockchain; however, it also introduces trade-offs in terms of trust, as it necessitates reliance on external systems.
Depending on the implementation, names could be stored in a database or be ephemeral.
Advantages of offchain name storage include gaslessness and instant updates.
If this sounds appealing consider [writing an Offchain Resolver](/resolvers/ccip-read).
Popular implementations of offchain names include but are not limited to:
### cb.id
Coinbase Wallet is one of the largest mobile wallets issuing free ENS subnames to their users.
These names are stored off-chain on coinbase servers, and can be registered from the Coinbase Wallet App or Browser Extension.
An example of a cb.id is [lucemans.cb.id](https://enstate.rs/n/lucemans.cb.id).
### uni.eth
Uniswap Wallet is another popular mobile wallet that issues free ENS subnames to their users.
You can read more about the Uniswap Wallet ENS integration [here](https://blog.uniswap.org/introducing-uni-eth-your-unique-web3-username).
An example of a uni.eth is [lucemans.uni.eth](https://enstate.rs/n/lucemans.uni.eth).
### lens.xyz
The Lens Ecosystem is also readily accessible through the officially maintained `lens.xyz` gateway.
This means that you can lookup your favourite lens name such as [lucemans.lens.xyz](https://enstate.rs/n/lucemans.lens.xyz) and retrieve the information seamlessly.
---
import { EmbedLink } from '../../components/EmbedLink'
import { Repository } from '../../components/Repository'
# Smart Contracts
The Ethereum Name Service is made up of a set of smart contracts.
These smart contracts are responsible for storing and managing information associated with names.
## Resolution
The resolution process outlines how a name is resolved.
This includes the process of finding a resolver, and then using that resolver to fetch information about a name.
## Resolvers
Every name has a resolver, responsible for fetching information about a name, such as address, avatar, and more.
Resolvers allow for programmatic control over the information associated with a name, implemented in solidity.
There are various resolvers to choose from, such as the [Public Resolver](/resolvers/public) or [write your own](/resolvers/writing).
## Registry & Registrars
The smart contracts can be found on github at [ensdomains/ens-contracts](https://github.com/ensdomains/ens-contracts).
## Source Code
The code for the ENS Smart Contracts as well as information around the latest deploys, tests, and more can be found on github.
# Contracts
```sol
// ---- ccipRead/IBatchGateway.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IBatchGateway {
struct Request {
address sender;
string[] urls;
bytes data;
}
function query(
Request[] memory
) external view returns (bool[] memory failures, bytes[] memory responses);
}
// ---- ccipRead/CCIPBatcher.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import {IBatchGateway} from "./IBatchGateway.sol";
import {CCIPReader, EIP3668, OffchainLookup} from "./CCIPReader.sol";
contract CCIPBatcher is CCIPReader {
/// @dev The batch gateway supplied an incorrect number of responses.
error InvalidBatchGatewayResponse();
uint256 constant FLAG_OFFCHAIN = 1 << 0; // the lookup reverted `OffchainLookup`
uint256 constant FLAG_CALL_ERROR = 1 << 1; // the initial call or callback reverted
uint256 constant FLAG_BATCH_ERROR = 1 << 2; // `OffchainLookup` failed on the batch gateway
uint256 constant FLAG_EMPTY_RESPONSE = 1 << 3; // the initial call or callback returned `0x`
uint256 constant FLAG_EIP140_BEFORE = 1 << 4; // does not have revert op code
uint256 constant FLAG_EIP140_AFTER = 1 << 5; // has revert op code
uint256 constant FLAG_DONE = 1 << 6; // the lookup has finished processing (private)
uint256 constant FLAGS_ANY_ERROR =
FLAG_CALL_ERROR | FLAG_BATCH_ERROR | FLAG_EMPTY_RESPONSE;
uint256 constant FLAGS_ANY_EIP140 = FLAG_EIP140_BEFORE | FLAG_EIP140_AFTER;
/// @dev An independent `OffchainLookup` session.
struct Lookup {
address target; // contract to call
bytes call; // initial calldata
bytes data; // response or error
uint256 flags; // see: FLAG_*
}
/// @dev A batch gateway session.
struct Batch {
Lookup[] lookups;
string[] gateways;
}
/// @dev Use `CCIPReader.ccipRead()` to call this function with a batch.
/// The callback `response` will be `abi.encode(batch)`.
function ccipBatch(
Batch memory batch
) external view returns (Batch memory) {
for (uint256 i; i < batch.lookups.length; i++) {
Lookup memory lu = batch.lookups[i];
if ((lu.flags & FLAGS_ANY_EIP140) == 0) {
uint256 flags = _detectEIP140(lu.target)
? FLAG_EIP140_AFTER
: FLAG_EIP140_BEFORE;
for (uint256 j = i; j < batch.lookups.length; j++) {
if (batch.lookups[j].target == lu.target) {
batch.lookups[j].flags |= flags;
}
}
}
bool old = (lu.flags & FLAG_EIP140_AFTER) == 0;
(bool ok, bytes memory v) = _safeCall(!old, lu.target, lu.call);
if (ok || (old && v.length == 0)) {
lu.flags |= FLAG_DONE;
if (v.length == 0) {
v = abi.encodePacked(bytes4(lu.call));
lu.flags |= FLAG_EMPTY_RESPONSE;
}
} else if (bytes4(v) == OffchainLookup.selector) {
lu.flags |= FLAG_OFFCHAIN;
} else {
lu.flags |= FLAG_DONE | FLAG_CALL_ERROR;
}
lu.data = v;
}
_revertBatchGateway(batch); // reverts if any offchain
return batch;
}
/// @dev Check if the batch is "done". If not, revert `OffchainLookup` for batch gateway.
function _revertBatchGateway(Batch memory batch) internal view {
IBatchGateway.Request[] memory requests = new IBatchGateway.Request[](
batch.lookups.length
);
uint256 count;
for (uint256 i; i < batch.lookups.length; i++) {
Lookup memory lu = batch.lookups[i];
if ((lu.flags & FLAG_DONE) == 0) {
EIP3668.Params memory p = decodeOffchainLookup(lu.data);
requests[count++] = IBatchGateway.Request(
p.sender,
p.urls,
p.callData
);
}
}
if (count > 0) {
assembly {
mstore(requests, count) // truncate to number of offchain requests
}
revert OffchainLookup(
address(this),
batch.gateways,
abi.encodeCall(IBatchGateway.query, (requests)),
this.ccipBatchCallback.selector,
abi.encode(batch)
);
}
}
/// @dev CCIP-Read callback for `ccipBatch()`.
/// Updates `batch` using the batch gateway response. Reverts again if not "done".
/// @param response The response from the batch gateway.
/// @param extraData The contextual data passed from `ccipBatch()`.
/// @return batch The batch where every lookup is "done".
function ccipBatchCallback(
bytes calldata response,
bytes calldata extraData
) external view returns (Batch memory batch) {
(bool[] memory failures, bytes[] memory responses) = abi.decode(
response,
(bool[], bytes[])
);
if (failures.length != responses.length) {
revert InvalidBatchGatewayResponse();
}
batch = abi.decode(extraData, (Batch));
uint256 expected;
for (uint256 i; i < batch.lookups.length; i++) {
Lookup memory lu = batch.lookups[i];
if ((lu.flags & FLAG_DONE) == 0) {
if (expected < responses.length) {
bytes memory v = responses[expected];
if (failures[expected]) {
lu.flags |= FLAG_DONE | FLAG_BATCH_ERROR;
} else {
EIP3668.Params memory p = decodeOffchainLookup(lu.data);
bool ok;
(ok, v) = p.sender.staticcall(
abi.encodeWithSelector(
p.callbackFunction,
v,
p.extraData
)
);
if (ok) {
lu.flags |= FLAG_DONE;
if (v.length == 0) {
v = abi.encodePacked(p.callbackFunction);
lu.flags |= FLAG_EMPTY_RESPONSE;
}
} else if (bytes4(v) != OffchainLookup.selector) {
lu.flags |= FLAG_DONE | FLAG_CALL_ERROR;
}
}
lu.data = v;
}
++expected;
}
}
if (expected != responses.length) {
revert InvalidBatchGatewayResponse();
}
_revertBatchGateway(batch);
}
}
// ---- ccipRead/CCIPReader.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
/// @author Modified from https://github.com/unruggable-labs/CCIPReader.sol/blob/341576fe7ff2b6e0c93fc08f37740cf6439f5873/contracts/CCIPReader.sol
/// MIT License
/// Portions Copyright (c) 2025 Unruggable
/// Portions Copyright (c) 2025 ENS Labs Ltd
/// @dev Instructions:
/// 1. inherit this contract
/// 2. call `ccipRead()` similar to `staticcall()`
/// 3. do not put logic after this invocation
/// 4. implement all response logic in callback
/// 5. ensure that return type of calling function == callback function
import {EIP3668, OffchainLookup} from "./EIP3668.sol";
import {BytesUtils} from "../utils/BytesUtils.sol";
contract CCIPReader {
/// @dev A recursive CCIP-Read session.
struct Context {
address target;
bytes4 callbackFunction;
bytes extraData;
bytes4 myCallbackFunction;
bytes myExtraData;
}
/// @dev Special-purpose value for identity callback: `f(x) = x`.
bytes4 constant IDENTITY_FUNCTION = bytes4(0);
/// @dev Same as `ccipRead()` but the callback function is the identity.
function ccipRead(address target, bytes memory call) internal view {
ccipRead(target, call, IDENTITY_FUNCTION, "");
}
/// @dev Performs a CCIP-Read and handles internal recursion.
/// Reverts `OffchainLookup` if necessary.
/// @param target The contract address.
/// @param call The calldata to `staticcall()` on `target`.
/// @param callbackFunction The function selector of callback.
/// @param extraData The contextual data relayed to `callbackFunction`.
function ccipRead(
address target,
bytes memory call,
bytes4 callbackFunction,
bytes memory extraData
) internal view {
// We call the intended function that **could** revert with an `OffchainLookup`
// We destructure the response into an execution status bool and our return bytes
(bool ok, bytes memory v) = _safeCall(
_detectEIP140(target),
target,
call
);
// IF the function reverted with an `OffchainLookup`
if (!ok && bytes4(v) == OffchainLookup.selector) {
// We decode the response error into a tuple
// tuples allow flexibility noting stack too deep constraints
EIP3668.Params memory p = decodeOffchainLookup(v);
if (p.sender == target) {
// We then wrap the error data in an `OffchainLookup` sent/'owned' by this contract
revert OffchainLookup(
address(this),
p.urls,
p.callData,
this.ccipReadCallback.selector,
abi.encode(
Context(
target,
p.callbackFunction,
p.extraData,
callbackFunction,
extraData
)
)
);
}
}
// IF we have gotten here, the 'real' target does not revert with an `OffchainLookup` error
if (ok && callbackFunction != IDENTITY_FUNCTION) {
// The exit point of this architecture is OUR callback in the 'real'
// We pass through the response to that callback
(ok, v) = address(this).staticcall(
abi.encodeWithSelector(callbackFunction, v, extraData)
);
}
// OR the call to the 'real' target reverts with a different error selector
// OR the call to OUR callback reverts with ANY error selector
if (ok) {
assembly {
return(add(v, 32), mload(v))
}
} else {
assembly {
revert(add(v, 32), mload(v))
}
}
}
/// @dev CCIP-Read callback for `ccipRead()`.
/// @param response The response from offchain.
/// @param extraData The contextual data passed from `ccipRead()`.
/// @dev The return type of this function is polymorphic depending on the caller.
function ccipReadCallback(
bytes memory response,
bytes memory extraData
) external view {
Context memory ctx = abi.decode(extraData, (Context));
// Since the callback can revert too (but has the same return structure)
// We can reuse the calling infrastructure to call the callback
ccipRead(
ctx.target,
abi.encodeWithSelector(
ctx.callbackFunction,
response,
ctx.extraData
),
ctx.myCallbackFunction,
ctx.myExtraData
);
}
/// @dev Decode `OffchainLookup` error data into a struct.
/// @param v The error data of the revert.
/// @return p The decoded `OffchainLookup` params.
function decodeOffchainLookup(
bytes memory v
) internal pure returns (EIP3668.Params memory p) {
p = EIP3668.decode(BytesUtils.substring(v, 4, v.length - 4));
}
/// @dev Determine if `target` uses `revert()` instead of `invalid()`.
// Assumption: only newer contracts revert `OffchainLookup`.
/// @param target The contract to test.
/// @return safe True if safe to call.
function _detectEIP140(address target) internal view returns (bool safe) {
if (target == address(this)) return true;
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-140.md
assembly {
let G := 5000
let g := gas()
pop(staticcall(G, target, 0, 0, 0, 0))
safe := lt(sub(g, gas()), G)
}
}
/// @dev Same as `staticcall()` but prevents OOG when not `safe`.
function _safeCall(
bool safe,
address target,
bytes memory call
) internal view returns (bool ok, bytes memory v) {
(ok, v) = target.staticcall{gas: safe ? gasleft() : 50000}(call);
}
}
// ---- ccipRead/EIP3668.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @dev https://eips.ethereum.org/EIPS/eip-3668
/// Error selector: `0x556f1830`
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);
/// @dev Simple library for decoding `OffchainLookup` error data.
/// Avoids "stack too deep" issues as the natural decoding consumes 5 variables.
library EIP3668 {
/// @dev Struct with members matching `OffchainLookup`.
struct Params {
address sender;
string[] urls;
bytes callData;
bytes4 callbackFunction;
bytes extraData;
}
/// @dev Decode an `OffchainLookup` into a struct from the data after the error selector.
function decode(bytes memory v) internal pure returns (Params memory p) {
(p.sender, p.urls, p.callData, p.callbackFunction, p.extraData) = abi
.decode(v, (address, string[], bytes, bytes4, bytes));
}
}
// ---- reverseRegistrar/IReverseRegistrar.sol ----
pragma solidity >=0.8.4;
interface IReverseRegistrar {
function setDefaultResolver(address resolver) external;
function claim(address owner) external returns (bytes32);
function claimForAddr(
address addr,
address owner,
address resolver
) external returns (bytes32);
function claimWithResolver(
address owner,
address resolver
) external returns (bytes32);
function setName(string memory name) external returns (bytes32);
function setNameForAddr(
address addr,
address owner,
address resolver,
string memory name
) external returns (bytes32);
function node(address addr) external pure returns (bytes32);
}
// ---- reverseRegistrar/ReverseClaimer.sol ----
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.17 <0.9.0;
import {ENS} from "../registry/ENS.sol";
import {IReverseRegistrar} from "../reverseRegistrar/IReverseRegistrar.sol";
contract ReverseClaimer {
bytes32 constant ADDR_REVERSE_NODE =
0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2;
constructor(ENS ens, address claimant) {
IReverseRegistrar reverseRegistrar = IReverseRegistrar(
ens.owner(ADDR_REVERSE_NODE)
);
reverseRegistrar.claim(claimant);
}
}
// ---- reverseRegistrar/ReverseRegistrar.sol ----
pragma solidity >=0.8.4;
import "../registry/ENS.sol";
import "./IReverseRegistrar.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../root/Controllable.sol";
abstract contract NameResolver {
function setName(bytes32 node, string memory name) public virtual;
}
bytes32 constant lookup = 0x3031323334353637383961626364656600000000000000000000000000000000;
bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2;
// namehash('addr.reverse')
contract ReverseRegistrar is Ownable, Controllable, IReverseRegistrar {
ENS public immutable ens;
NameResolver public defaultResolver;
event ReverseClaimed(address indexed addr, bytes32 indexed node);
event DefaultResolverChanged(NameResolver indexed resolver);
/// @dev Constructor
/// @param ensAddr The address of the ENS registry.
constructor(ENS ensAddr) {
ens = ensAddr;
// Assign ownership of the reverse record to our deployer
ReverseRegistrar oldRegistrar = ReverseRegistrar(
ensAddr.owner(ADDR_REVERSE_NODE)
);
if (address(oldRegistrar) != address(0x0)) {
oldRegistrar.claim(msg.sender);
}
}
modifier authorised(address addr) {
require(
addr == msg.sender ||
controllers[msg.sender] ||
ens.isApprovedForAll(addr, msg.sender) ||
ownsContract(addr),
"ReverseRegistrar: Caller is not a controller or authorised by address or the address itself"
);
_;
}
function setDefaultResolver(address resolver) public override onlyOwner {
require(
address(resolver) != address(0),
"ReverseRegistrar: Resolver address must not be 0"
);
defaultResolver = NameResolver(resolver);
emit DefaultResolverChanged(NameResolver(resolver));
}
/// @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) public override returns (bytes32) {
return claimForAddr(msg.sender, owner, address(defaultResolver));
}
/// @dev Transfers ownership of the reverse ENS record associated with the
/// calling account.
/// @param addr The reverse record to set
/// @param owner The address to set as the owner of the reverse record in ENS.
/// @param resolver The resolver of the reverse node
/// @return The ENS node hash of the reverse record.
function claimForAddr(
address addr,
address owner,
address resolver
) public override authorised(addr) returns (bytes32) {
bytes32 labelHash = sha3HexAddress(addr);
bytes32 reverseNode = keccak256(
abi.encodePacked(ADDR_REVERSE_NODE, labelHash)
);
emit ReverseClaimed(addr, reverseNode);
ens.setSubnodeRecord(ADDR_REVERSE_NODE, labelHash, owner, resolver, 0);
return reverseNode;
}
/// @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.
/// @param resolver The address of the resolver to set; 0 to leave unchanged.
/// @return The ENS node hash of the reverse record.
function claimWithResolver(
address owner,
address resolver
) public override returns (bytes32) {
return claimForAddr(msg.sender, owner, resolver);
}
/// @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) public override returns (bytes32) {
return
setNameForAddr(
msg.sender,
msg.sender,
address(defaultResolver),
name
);
}
/// @dev Sets the `name()` record for the reverse ENS record associated with
/// the account provided. Updates the resolver to a designated resolver
/// Only callable by controllers and authorised users
/// @param addr The reverse record to set
/// @param owner The owner of the reverse node
/// @param resolver The resolver of the reverse node
/// @param name The name to set for this address.
/// @return The ENS node hash of the reverse record.
function setNameForAddr(
address addr,
address owner,
address resolver,
string memory name
) public override returns (bytes32) {
bytes32 node = claimForAddr(addr, owner, resolver);
NameResolver(resolver).setName(node, name);
return node;
}
/// @dev Returns the node hash for a given account's reverse records.
/// @param addr The address to hash
/// @return The ENS node hash.
function node(address addr) public pure override returns (bytes32) {
return
keccak256(
abi.encodePacked(ADDR_REVERSE_NODE, sha3HexAddress(addr))
);
}
/// @dev An optimised function to compute the sha3 of the lower-case
/// hexadecimal representation of an Ethereum address.
/// @param addr The address to hash
/// @return ret The SHA3 hash of the lower-case hexadecimal encoding of the
/// input address.
function sha3HexAddress(address addr) private pure returns (bytes32 ret) {
assembly {
for {
let i := 40
} gt(i, 0) {} {
i := sub(i, 1)
mstore8(i, byte(and(addr, 0xf), lookup))
addr := div(addr, 0x10)
i := sub(i, 1)
mstore8(i, byte(and(addr, 0xf), lookup))
addr := div(addr, 0x10)
}
ret := keccak256(0, 40)
}
}
function ownsContract(address addr) internal view returns (bool) {
try Ownable(addr).owner() returns (address owner) {
return owner == msg.sender;
} catch {
return false;
}
}
}
// ---- universalResolver/IUniversalResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @notice Interface for the UniversalResolver.
/// @dev Interface selector: `0xcd191b34`
interface IUniversalResolver {
/// @notice A resolver could not be found for the supplied name.
/// @dev Error selector: `0x77209fe8`
error ResolverNotFound(bytes name);
/// @notice The resolver is not a contract.
/// @dev Error selector: `0x1e9535f2`
error ResolverNotContract(bytes name, address resolver);
/// @notice The resolver did not respond.
/// @dev Error selector: `0x7b1c461b`
error UnsupportedResolverProfile(bytes4 selector);
/// @notice The resolver returned an error.
/// @dev Error selector: `0x95c0c752`
error ResolverError(bytes errorData);
/// @notice The resolved address from reverse resolution does not match the supplied address.
/// @dev Error selector: `0xef9c03ce`
error ReverseAddressMismatch(string primary, bytes primaryAddress);
/// @notice An HTTP error occurred on a resolving gateway.
/// @dev Error selector: `0x01800152`
error HttpError(uint16 status, string message);
/// @notice Performs ENS name resolution for the supplied name and resolution data.
/// @notice Callers should enable EIP-3668.
/// @param name The name to resolve, in normalised and DNS-encoded form.
/// @param data The resolution data, as specified in ENSIP-10.
/// For a multicall, the data should be encoded as `(bytes[])`.
/// @return result The result of the resolution.
/// For a multicall, the result is encoded as `(bytes[])`.
/// @return resolver The resolver that was used to resolve the name.
function resolve(
bytes calldata name,
bytes calldata data
) external view returns (bytes memory result, address resolver);
/// @notice Performs ENS reverse resolution for the supplied address and coin type.
/// @notice Callers should enable EIP-3668.
/// @param lookupAddress The address to reverse resolve, in encoded form.
/// @param coinType The coin type to use for the reverse resolution.
/// For ETH, this is 60.
/// For other EVM chains, coinType is calculated as `0x80000000 | chainId`.
/// @return primary The reverse resolution result.
/// @return resolver The resolver that was used to resolve the name.
/// @return reverseResolver The resolver that was used to resolve the reverse name.
function reverse(
bytes calldata lookupAddress,
uint256 coinType
)
external
view
returns (
string memory primary,
address resolver,
address reverseResolver
);
}
// ---- universalResolver/UniversalResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import {AbstractUniversalResolver, NameCoder} from "./AbstractUniversalResolver.sol";
import {ENS} from "../registry/ENS.sol";
contract UniversalResolver is AbstractUniversalResolver {
ENS public immutable registry;
constructor(
ENS ens,
string[] memory gateways
) AbstractUniversalResolver(gateways) {
registry = ens;
}
/// @dev Find the resolver address for `name`.
/// Does not perform any validity checks.
/// @param name The name to search.
function findResolver(
bytes memory name
) public view override returns (address, bytes32, uint256) {
return _findResolver(name, 0);
}
/// @dev Efficiently find the resolver address for `name[offset:]`.
/// @param name The name to search.
/// @param offset The byte-offset into `name` to begin the search.
/// @return resolver The address of the resolver.
/// @return node The namehash of name corresponding to the resolver.
/// @return offset_ The byte-offset into `name` of the name corresponding to the resolver.
function _findResolver(
bytes memory name,
uint256 offset
) internal view returns (address resolver, bytes32 node, uint256 offset_) {
(bytes32 labelHash, uint256 next) = NameCoder.readLabel(name, offset);
if (labelHash != bytes32(0)) {
(
address parentResolver,
bytes32 parentNode,
uint256 parentOffset
) = _findResolver(name, next);
node = keccak256(abi.encodePacked(parentNode, labelHash));
resolver = registry.resolver(node);
return
resolver != address(0)
? (resolver, node, offset)
: (parentResolver, node, parentOffset);
}
}
}
// ---- universalResolver/AbstractUniversalResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import {IUniversalResolver} from "./IUniversalResolver.sol";
import {CCIPBatcher} from "../ccipRead/CCIPBatcher.sol";
import {IExtendedResolver} from "../resolvers/profiles/IExtendedResolver.sol";
import {INameResolver} from "../resolvers/profiles/INameResolver.sol";
import {IAddrResolver} from "../resolvers/profiles/IAddrResolver.sol";
import {IAddressResolver} from "../resolvers/profiles/IAddressResolver.sol";
import {IMulticallable} from "../resolvers/IMulticallable.sol";
import {NameCoder} from "../utils/NameCoder.sol";
import {BytesUtils} from "../utils/BytesUtils.sol";
import {ENSIP19, COIN_TYPE_ETH, EVM_BIT} from "../utils/ENSIP19.sol";
abstract contract AbstractUniversalResolver is
IUniversalResolver,
CCIPBatcher,
Ownable,
ERC165
{
string[] public batchGateways;
constructor(string[] memory gateways) {
batchGateways = gateways;
}
/// @inheritdoc ERC165
function supportsInterface(
bytes4 interfaceID
) public view virtual override(ERC165) returns (bool) {
return
type(IUniversalResolver).interfaceId == interfaceID ||
super.supportsInterface(interfaceID);
}
/// @dev Set the default batch gateways, see: `resolve()` and `reverse()`.
/// @param gateways The list of batch gateway URLs to use as default.
function setBatchGateways(string[] memory gateways) external onlyOwner {
batchGateways = gateways;
}
/// @dev Find the resolver address for `name`.
/// Does not perform any validity checks.
/// @param name The name to search.
/// @return resolver The resolver responsible for this name, or `address(0)` if none.
/// @return node The namehash of name corresponding to the resolver.
/// @return offset The byte-offset into `name` of the name corresponding to the resolver.
function findResolver(
bytes memory name
)
public
view
virtual
returns (address resolver, bytes32 node, uint256 offset);
// @dev A valid resolver and its relevant properties.
struct ResolverInfo {
bytes name; // dns-encoded name (safe to decode)
uint256 offset; // byte offset into name used for resolver
bytes32 node; // namehash(name)
address resolver;
bool extended; // IExtendedResolver
}
/// @dev Returns a valid resolver for `name` or reverts.
/// @param name The name to search.
/// @return info The resolver information.
function requireResolver(
bytes memory name
) public view returns (ResolverInfo memory info) {
// https://docs.ens.domains/ensip/10
(info.resolver, info.node, info.offset) = findResolver(name);
if (info.resolver == address(0)) {
revert ResolverNotFound(name);
} else if (
ERC165Checker.supportsERC165InterfaceUnchecked(
info.resolver,
type(IExtendedResolver).interfaceId
)
) {
info.extended = true;
} else if (info.offset != 0) {
revert ResolverNotFound(name); // immediate resolver requires exact match
} else if (info.resolver.code.length == 0) {
revert ResolverNotContract(name, info.resolver);
}
info.name = name;
}
/// @notice Same as `resolveWithGateways()` but uses default batch gateways.
function resolve(
bytes calldata name,
bytes calldata data
) external view returns (bytes memory /*result*/, address /*resolver*/) {
return resolveWithGateways(name, data, batchGateways);
}
/// @notice Performs ENS name resolution for the supplied name and resolution data.
/// @notice Callers should enable EIP-3668.
/// @dev This function executes over multiple steps (step 1 of 2).
/// @return result The encoded response for the requested call.
/// @return resolver The address of the resolver that supplied `result`.
function resolveWithGateways(
bytes calldata name,
bytes calldata data,
string[] memory gateways
) public view returns (bytes memory /*result*/, address /*resolver*/) {
bool multi = bytes4(data) == IMulticallable.multicall.selector;
_resolveBatch(
requireResolver(name),
multi ? abi.decode(data[4:], (bytes[])) : _oneCall(data),
gateways,
this.resolveCallback.selector,
abi.encode(multi)
);
}
/// @dev CCIP-Read callback for `resolveWithGateways()` (step 2 of 2).
/// @param info The resolver that was called.
/// @param lookups The lookups corresponding to the requested call.
/// @param extraData The contextual data passed from `resolveWithGateways()`.
/// @return result The encoded response for the requested call.
/// @return resolver The address of the resolver that supplied `result`.
function resolveCallback(
ResolverInfo calldata info,
Lookup[] calldata lookups,
bytes calldata extraData
) external pure returns (bytes memory result, address resolver) {
bool multi = abi.decode(extraData, (bool));
if (multi) {
bytes[] memory m = new bytes[](lookups.length);
for (uint256 i; i < lookups.length; i++) {
Lookup memory lu = lookups[i];
if ((lu.flags & FLAG_EMPTY_RESPONSE) == 0) {
m[i] = lookups[i].data;
}
}
result = abi.encode(m);
} else {
result = _requireResponse(lookups[0]);
}
resolver = info.resolver;
}
/// @notice Same as `reverseWithGateways()` but uses default batch gateways.
function reverse(
bytes memory lookupAddress,
uint256 coinType
) external view returns (string memory, address /* resolver */, address) {
return reverseWithGateways(lookupAddress, coinType, batchGateways);
}
struct ReverseArgs {
bytes lookupAddress;
uint256 coinType;
string[] gateways;
}
/// @notice Performs ENS reverse resolution for the supplied address and coin type.
/// @notice Callers should enable EIP-3668.
/// @dev This function executes over multiple steps (step 1 of 3).
/// @param lookupAddress The input address.
/// @param coinType The coin type.
/// @param gateways The list of batch gateway URLs to use.
function reverseWithGateways(
bytes memory lookupAddress,
uint256 coinType,
string[] memory gateways
) public view returns (string memory, address /* resolver */, address) {
// https://docs.ens.domains/ensip/19
ResolverInfo memory info = requireResolver(
NameCoder.encode(ENSIP19.reverseName(lookupAddress, coinType)) // reverts EmptyAddress
);
_resolveBatch(
info,
_oneCall(abi.encodeCall(INameResolver.name, (info.node))),
gateways,
this.reverseNameCallback.selector,
abi.encode(ReverseArgs(lookupAddress, coinType, gateways))
);
}
/// @dev CCIP-Read callback for `reverseWithGateways()` (step 2 of 3).
/// @param infoRev The resolver for the reverse name that was called.
/// @param lookups The lookups corresponding to the calls: `[name()]`.
/// @param extraData The contextual data passed from `reverseWithGateways()`.
function reverseNameCallback(
ResolverInfo calldata infoRev,
Lookup[] calldata lookups,
bytes memory extraData // this cannot be calldata due to "stack too deep"
) external view returns (string memory primary, address, address) {
ReverseArgs memory args = abi.decode(extraData, (ReverseArgs));
primary = abi.decode(_requireResponse(lookups[0]), (string));
if (bytes(primary).length == 0) {
return ("", address(0), infoRev.resolver);
}
ResolverInfo memory info = requireResolver(NameCoder.encode(primary));
_resolveBatch(
info,
_forwardCalls(info.node, args.coinType),
args.gateways,
this.reverseAddressCallback.selector,
abi.encode(args.lookupAddress, primary, infoRev.resolver)
);
}
/// @dev Create forward resolution calls.
/// (Separate function because of stack too deep.)
function _forwardCalls(
bytes32 node,
uint256 coinType
) internal pure returns (bytes[] memory calls) {
bool useFallback = ENSIP19.chainFromCoinType(coinType) > 0;
calls = new bytes[](useFallback ? 2 : 1);
calls[0] = coinType == COIN_TYPE_ETH
? abi.encodeCall(IAddrResolver.addr, (node))
: abi.encodeCall(IAddressResolver.addr, (node, coinType));
if (useFallback) {
calls[1] = abi.encodeCall(IAddressResolver.addr, (node, EVM_BIT));
}
}
/// @dev CCIP-Read callback for `reverseNameCallback()` (step 3 of 3).
/// Reverts `ReverseAddressMismatch`.
/// @param info The resolver for the primary name that was called.
/// @param lookups The lookups corresponding to the calls: `[addr()]`.
/// @param extraData The contextual data passed from `reverseNameCallback()`.
/// @return primary The resolved primary name.
/// @return resolver The resolver address for primary name.
/// @return reverseResolver The resolver address for the reverse name.
function reverseAddressCallback(
ResolverInfo calldata info,
Lookup[] calldata lookups,
bytes calldata extraData
)
external
pure
returns (
string memory primary,
address resolver,
address reverseResolver
)
{
bytes memory reverseAddress;
(reverseAddress, primary, reverseResolver) = abi.decode(
extraData,
(bytes, string, address)
);
bytes memory primaryAddress;
if (lookups.length == 2) {
if (
(lookups[0].flags & FLAGS_ANY_ERROR) == 0 ||
(lookups[1].flags & FLAGS_ANY_ERROR) != 0 // if both fail, revert with first error
) {
primaryAddress = _decodeAddress(lookups[0]);
}
if (primaryAddress.length == 0) {
primaryAddress = _decodeAddress(lookups[1]);
}
} else {
primaryAddress = _decodeAddress(lookups[0]);
}
if (!BytesUtils.equals(reverseAddress, primaryAddress)) {
revert ReverseAddressMismatch(primary, primaryAddress);
}
resolver = info.resolver;
}
/// @dev Decode address (`addr()` or `addr(coinType)`).
/// Ignore `addr() = address(0)`.
function _decodeAddress(
Lookup memory lu
) internal pure returns (bytes memory a) {
bytes memory v = _requireResponse(lu);
bytes4 selector = bytes4(lu.call);
if (selector == IAddrResolver.addr.selector) {
address addr = abi.decode(v, (address));
if (addr != address(0)) {
a = abi.encodePacked(addr);
}
} else if (selector == IAddressResolver.addr.selector) {
a = abi.decode(v, (bytes));
}
}
/// @dev Perform multiple resolver calls in parallel using batch gateway.
/// @param info The resolver to call.
/// @param calls The list of resolver calldata, eg. `[addr(), text()]`.
/// @param gateways The list of batch gateway URLs to use.
/// @param callbackFunction The function selector to call after resolution.
/// @param extraData The contextual data passed to `callbackFunction`.
/// @dev The return type of this function is polymorphic depending on the caller.
function _resolveBatch(
ResolverInfo memory info,
bytes[] memory calls,
string[] memory gateways,
bytes4 callbackFunction,
bytes memory extraData
) internal view {
Batch memory batch = Batch(new Lookup[](calls.length), gateways);
for (uint256 i; i < calls.length; i++) {
Lookup memory lu = batch.lookups[i];
lu.target = info.resolver;
lu.call = info.extended
? abi.encodeCall(
IExtendedResolver.resolve,
(info.name, calls[i])
)
: calls[i];
}
ccipRead(
address(this),
abi.encodeCall(this.ccipBatch, (batch)),
this.resolveBatchCallback.selector,
abi.encode(info, callbackFunction, extraData)
);
}
/// @dev CCIP-Read callback for `_resolveBatch()`.
/// @param response The response data from `CCIPBatcher`.
/// @param extraData The contextual data from `_resolveBatch()`.
function resolveBatchCallback(
bytes calldata response,
bytes calldata extraData
) external view {
Batch memory batch = abi.decode(response, (Batch));
(
ResolverInfo memory info,
bytes4 callbackFunction_,
bytes memory extraData_
) = abi.decode(extraData, (ResolverInfo, bytes4, bytes));
if (info.extended) {
for (uint256 i; i < batch.lookups.length; i++) {
Lookup memory lu = batch.lookups[i];
lu.call = _unwrapResolve(lu.call);
if ((lu.flags & FLAGS_ANY_ERROR) == 0) {
lu.data = abi.decode(lu.data, (bytes));
}
}
}
ccipRead(
address(this),
abi.encodeWithSelector(
callbackFunction_,
info,
batch.lookups,
extraData_
)
);
}
/// @dev Extract `data` from `resolve(bytes, bytes data)` calldata.
/// @param v The `resolve(bytes, bytes data)` calldata.
/// @return data The inner `bytes data` argument.
function _unwrapResolve(
bytes memory v
) internal pure returns (bytes memory data) {
// resolve(bytes name, bytes data): | <== offset starts here
// => uint256(length) + bytes4(selector) | offset(name) + offset(data)
// 32 + 4 | 32
assembly {
data := add(v, 36) // location of offset start
data := add(data, mload(add(data, 32))) // += offset(data)
}
}
/// @dev Extract `data` from a lookup or revert an appropriate error.
/// Reverts if the `data` is not a successful response.
/// @param lu The lookup to extract from.
/// @return v The successful response (always 32+ bytes).
function _requireResponse(
Lookup memory lu
) internal pure returns (bytes memory v) {
v = lu.data;
if ((lu.flags & FLAG_BATCH_ERROR) != 0) {
assembly {
revert(add(v, 32), mload(v)) // HttpError or Error
}
} else if ((lu.flags & FLAG_CALL_ERROR) != 0) {
if (bytes4(v) == UnsupportedResolverProfile.selector) {
assembly {
revert(add(v, 32), mload(v))
}
}
revert ResolverError(v); // any error from Resolver
} else if ((lu.flags & FLAG_EMPTY_RESPONSE) != 0) {
revert UnsupportedResolverProfile(bytes4(v)); // initial call or callback was unimplemented
}
}
/// @dev Create an array with one `call`.
/// @param call The single calldata.
/// @return calls The one-element calldata array, eg. `[call]`.
function _oneCall(
bytes memory call
) internal pure returns (bytes[] memory calls) {
calls = new bytes[](1);
calls[0] = call;
}
}
// ---- universalResolver/mocks/DummyShapeshiftResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {IExtendedResolver} from "../../resolvers/profiles/IExtendedResolver.sol";
import {OffchainLookup} from "../../ccipRead/EIP3668.sol";
//import {IResolveMulticall} from "../../resolvers/IResolveMulticall.sol";
// this resolver can perform all resolver permutations
// when this contract triggers OffchainLookup(), it uses a data-url, so no server is required
// the actual response is set using `setResponse()`
// https://github.com/ensdomains/ensips/pull/18
error UnsupportedResolverProfile(bytes4 call);
contract DummyShapeshiftResolver is IExtendedResolver, IERC165 {
mapping(bytes => bytes) public responses;
bool public isERC165 = true; // default
bool public isExtended;
bool public isOffchain;
bool public revertUnsupported;
bool public revertEmpty;
//bool public isWrapperSafe;
//bool public isResolveMulticallable;
function setResponse(bytes memory req, bytes memory res) external {
responses[req] = res;
}
function setOld() external {
isERC165 = false;
isExtended = false;
}
function setExtended(bool x) external {
isERC165 = true;
isExtended = x;
}
function setOffchain(bool x) external {
isOffchain = x;
}
function setRevertUnsupportedResolverProfile(bool x) external {
revertUnsupported = x;
}
function setRevertEmpty(bool x) external {
revertEmpty = x;
}
fallback() external {
if (isExtended) return;
bytes memory v = responses[msg.data];
if (v.length == 0) {
if (revertEmpty) {
assembly {
revert(0, 0)
}
}
return;
}
if (isOffchain) _revertOffchain(v);
_revertIfError(v);
assembly {
return(add(v, 32), mload(v))
}
}
function supportsInterface(bytes4 x) external view returns (bool) {
if (!isERC165) {
assembly {
return(0, 0)
}
}
return
type(IERC165).interfaceId == x ||
(type(IExtendedResolver).interfaceId == x && isExtended);
}
function resolve(
bytes memory,
bytes memory call
) external view returns (bytes memory) {
bytes memory v = responses[call];
if (v.length == 0 && revertUnsupported) {
revert UnsupportedResolverProfile(bytes4(call));
}
if (isOffchain) _revertOffchain(v);
_revertIfError(v);
return v;
}
function _revertOffchain(bytes memory v) internal view {
string[] memory urls = new string[](1);
urls[0] = 'data:application/json,{"data":"0x"}';
revert OffchainLookup(
address(this),
urls,
"",
this.callback.selector,
v
);
}
function callback(
bytes memory,
bytes memory v
) external view returns (bytes memory) {
_revertIfError(v);
if (isExtended) return v;
assembly {
return(add(v, 32), mload(v))
}
}
function _revertIfError(bytes memory v) internal pure {
if ((v.length & 31) != 0) {
assembly {
revert(add(v, 32), mload(v))
}
}
}
// function enableMulticall(bytes[] memory calls) external {
// bytes[] memory m = new bytes[](calls.length);
// for (uint256 i; i < calls.length; i++) {
// m[i] = responses[calls[i]];
// }
// setResponse(
// abi.encodeCall(IResolveMulticall.multicall, (calls)),
// abi.encode(m)
// );
// }
}
// ---- registry/TestRegistrar.sol ----
pragma solidity >=0.8.4;
import "./ENS.sol";
/// A registrar that allocates subdomains to the first person to claim them, but
/// expires registrations a fixed period after they're initially claimed.
contract TestRegistrar {
uint256 constant registrationPeriod = 4 weeks;
ENS public immutable ens;
bytes32 public immutable rootNode;
mapping(bytes32 => uint256) public expiryTimes;
/// Constructor.
/// @param ensAddr The address of the ENS registry.
/// @param node The node that this registrar administers.
constructor(ENS ensAddr, bytes32 node) {
ens = ensAddr;
rootNode = node;
}
/// Register a name that's not currently registered
/// @param label The hash of the label to register.
/// @param owner The address of the new owner.
function register(bytes32 label, address owner) public {
require(expiryTimes[label] < block.timestamp);
expiryTimes[label] = block.timestamp + registrationPeriod;
ens.setSubnodeOwner(rootNode, label, owner);
}
}
// ---- registry/FIFSRegistrar.sol ----
pragma solidity >=0.8.4;
import "./ENS.sol";
/// A registrar that allocates subdomains to the first person to claim them.
contract FIFSRegistrar {
ENS ens;
bytes32 rootNode;
modifier only_owner(bytes32 label) {
address currentOwner = ens.owner(
keccak256(abi.encodePacked(rootNode, label))
);
require(currentOwner == address(0x0) || currentOwner == msg.sender);
_;
}
/// Constructor.
/// @param ensAddr The address of the ENS registry.
/// @param node The node that this registrar administers.
constructor(ENS ensAddr, bytes32 node) public {
ens = ensAddr;
rootNode = node;
}
/// Register a name, or change the owner of an existing registration.
/// @param label The hash of the label to register.
/// @param owner The address of the new owner.
function register(bytes32 label, address owner) public only_owner(label) {
ens.setSubnodeOwner(rootNode, label, owner);
}
}
// ---- registry/ENSRegistry.sol ----
pragma solidity >=0.8.4;
import "./ENS.sol";
/// The ENS registry contract.
contract ENSRegistry is ENS {
struct Record {
address owner;
address resolver;
uint64 ttl;
}
mapping(bytes32 => Record) records;
mapping(address => mapping(address => bool)) operators;
// Permits modifications only by the owner of the specified node.
modifier authorised(bytes32 node) {
address owner = records[node].owner;
require(owner == msg.sender || operators[owner][msg.sender]);
_;
}
/// @dev Constructs a new ENS registry.
constructor() public {
records[0x0].owner = msg.sender;
}
/// @dev Sets the record for a node.
/// @param node The node to update.
/// @param owner The address of the new owner.
/// @param resolver The address of the resolver.
/// @param ttl The TTL in seconds.
function setRecord(
bytes32 node,
address owner,
address resolver,
uint64 ttl
) external virtual override {
setOwner(node, owner);
_setResolverAndTTL(node, resolver, ttl);
}
/// @dev Sets the record for a subnode.
/// @param node The parent node.
/// @param label The hash of the label specifying the subnode.
/// @param owner The address of the new owner.
/// @param resolver The address of the resolver.
/// @param ttl The TTL in seconds.
function setSubnodeRecord(
bytes32 node,
bytes32 label,
address owner,
address resolver,
uint64 ttl
) external virtual override {
bytes32 subnode = setSubnodeOwner(node, label, owner);
_setResolverAndTTL(subnode, resolver, ttl);
}
/// @dev Transfers ownership of a node to a new address. May only be called by the current owner of the node.
/// @param node The node to transfer ownership of.
/// @param owner The address of the new owner.
function setOwner(
bytes32 node,
address owner
) public virtual override authorised(node) {
_setOwner(node, owner);
emit Transfer(node, owner);
}
/// @dev Transfers ownership of a subnode keccak256(node, label) to a new address. May only be called by the owner of the parent node.
/// @param node The parent node.
/// @param label The hash of the label specifying the subnode.
/// @param owner The address of the new owner.
function setSubnodeOwner(
bytes32 node,
bytes32 label,
address owner
) public virtual override authorised(node) returns (bytes32) {
bytes32 subnode = keccak256(abi.encodePacked(node, label));
_setOwner(subnode, owner);
emit NewOwner(node, label, owner);
return subnode;
}
/// @dev Sets the resolver address for the specified node.
/// @param node The node to update.
/// @param resolver The address of the resolver.
function setResolver(
bytes32 node,
address resolver
) public virtual override authorised(node) {
emit NewResolver(node, resolver);
records[node].resolver = resolver;
}
/// @dev Sets the TTL for the specified node.
/// @param node The node to update.
/// @param ttl The TTL in seconds.
function setTTL(
bytes32 node,
uint64 ttl
) public virtual override authorised(node) {
emit NewTTL(node, ttl);
records[node].ttl = ttl;
}
/// @dev Enable or disable approval for a third party ("operator") to manage
/// all of `msg.sender`'s ENS records. Emits the ApprovalForAll event.
/// @param operator Address to add to the set of authorized operators.
/// @param approved True if the operator is approved, false to revoke approval.
function setApprovalForAll(
address operator,
bool approved
) external virtual override {
operators[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
/// @dev Returns the address that owns the specified node.
/// @param node The specified node.
/// @return address of the owner.
function owner(
bytes32 node
) public view virtual override returns (address) {
address addr = records[node].owner;
if (addr == address(this)) {
return address(0x0);
}
return addr;
}
/// @dev Returns the address of the resolver for the specified node.
/// @param node The specified node.
/// @return address of the resolver.
function resolver(
bytes32 node
) public view virtual override returns (address) {
return records[node].resolver;
}
/// @dev Returns the TTL of a node, and any records associated with it.
/// @param node The specified node.
/// @return ttl of the node.
function ttl(bytes32 node) public view virtual override returns (uint64) {
return records[node].ttl;
}
/// @dev Returns whether a record has been imported to the registry.
/// @param node The specified node.
/// @return Bool if record exists
function recordExists(
bytes32 node
) public view virtual override returns (bool) {
return records[node].owner != address(0x0);
}
/// @dev Query if an address is an authorized operator for another address.
/// @param owner The address that owns the records.
/// @param operator The address that acts on behalf of the owner.
/// @return True if `operator` is an approved operator for `owner`, false otherwise.
function isApprovedForAll(
address owner,
address operator
) external view virtual override returns (bool) {
return operators[owner][operator];
}
function _setOwner(bytes32 node, address owner) internal virtual {
records[node].owner = owner;
}
function _setResolverAndTTL(
bytes32 node,
address resolver,
uint64 ttl
) internal {
if (resolver != records[node].resolver) {
records[node].resolver = resolver;
emit NewResolver(node, resolver);
}
if (ttl != records[node].ttl) {
records[node].ttl = ttl;
emit NewTTL(node, ttl);
}
}
}
// ---- registry/ENSRegistryWithFallback.sol ----
pragma solidity >=0.8.4;
import "./ENS.sol";
import "./ENSRegistry.sol";
/// The ENS registry contract.
contract ENSRegistryWithFallback is ENSRegistry {
ENS public old;
/// @dev Constructs a new ENS registrar.
constructor(ENS _old) public ENSRegistry() {
old = _old;
}
/// @dev Returns the address of the resolver for the specified node.
/// @param node The specified node.
/// @return address of the resolver.
function resolver(bytes32 node) public view override returns (address) {
if (!recordExists(node)) {
return old.resolver(node);
}
return super.resolver(node);
}
/// @dev Returns the address that owns the specified node.
/// @param node The specified node.
/// @return address of the owner.
function owner(bytes32 node) public view override returns (address) {
if (!recordExists(node)) {
return old.owner(node);
}
return super.owner(node);
}
/// @dev Returns the TTL of a node, and any records associated with it.
/// @param node The specified node.
/// @return ttl of the node.
function ttl(bytes32 node) public view override returns (uint64) {
if (!recordExists(node)) {
return old.ttl(node);
}
return super.ttl(node);
}
function _setOwner(bytes32 node, address owner) internal override {
address addr = owner;
if (addr == address(0x0)) {
addr = address(this);
}
super._setOwner(node, addr);
}
}
// ---- registry/ENS.sol ----
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface ENS {
// Logged when the owner of a node assigns a new owner to a subnode.
event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner);
// Logged when the owner of a node transfers ownership to a new account.
event Transfer(bytes32 indexed node, address owner);
// Logged when the resolver for a node changes.
event NewResolver(bytes32 indexed node, address resolver);
// Logged when the TTL of a node changes
event NewTTL(bytes32 indexed node, uint64 ttl);
// Logged when an operator is added or removed.
event ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
);
function setRecord(
bytes32 node,
address owner,
address resolver,
uint64 ttl
) external;
function setSubnodeRecord(
bytes32 node,
bytes32 label,
address owner,
address resolver,
uint64 ttl
) external;
function setSubnodeOwner(
bytes32 node,
bytes32 label,
address owner
) external returns (bytes32);
function setResolver(bytes32 node, address resolver) external;
function setOwner(bytes32 node, address owner) external;
function setTTL(bytes32 node, uint64 ttl) external;
function setApprovalForAll(address operator, bool approved) external;
function owner(bytes32 node) external view returns (address);
function resolver(bytes32 node) external view returns (address);
function ttl(bytes32 node) external view returns (uint64);
function recordExists(bytes32 node) external view returns (bool);
function isApprovedForAll(
address owner,
address operator
) external view returns (bool);
}
// ---- dnssec-oracle/DNSSEC.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
pragma experimental ABIEncoderV2;
abstract contract DNSSEC {
bytes public anchors;
struct RRSetWithSignature {
bytes rrset;
bytes sig;
}
event AlgorithmUpdated(uint8 id, address addr);
event DigestUpdated(uint8 id, address addr);
function verifyRRSet(
RRSetWithSignature[] memory input
) external view virtual returns (bytes memory rrs, uint32 inception);
function verifyRRSet(
RRSetWithSignature[] memory input,
uint256 now
) public view virtual returns (bytes memory rrs, uint32 inception);
}
// ---- dnssec-oracle/Owned.sol ----
pragma solidity ^0.8.4;
/// @dev Contract mixin for 'owned' contracts.
contract Owned {
address public owner;
modifier owner_only() {
require(msg.sender == owner);
_;
}
constructor() public {
owner = msg.sender;
}
function setOwner(address newOwner) public owner_only {
owner = newOwner;
}
}
// ---- dnssec-oracle/SHA1.sol ----
pragma solidity >=0.8.4;
library SHA1 {
event Debug(bytes32 x);
function sha1(bytes memory data) internal pure returns (bytes20 ret) {
assembly {
// Get a safe scratch location
let scratch := mload(0x40)
// Get the data length, and point data at the first byte
let len := mload(data)
data := add(data, 32)
// Find the length after padding
let totallen := add(and(add(len, 1), 0xFFFFFFFFFFFFFFC0), 64)
switch lt(sub(totallen, len), 9)
case 1 {
totallen := add(totallen, 64)
}
let h := 0x6745230100EFCDAB890098BADCFE001032547600C3D2E1F0
function readword(ptr, off, count) -> result {
result := 0
if lt(off, count) {
result := mload(add(ptr, off))
count := sub(count, off)
if lt(count, 32) {
let mask := not(sub(exp(256, sub(32, count)), 1))
result := and(result, mask)
}
}
}
for {
let i := 0
} lt(i, totallen) {
i := add(i, 64)
} {
mstore(scratch, readword(data, i, len))
mstore(add(scratch, 32), readword(data, add(i, 32), len))
// If we loaded the last byte, store the terminator byte
switch lt(sub(len, i), 64)
case 1 {
mstore8(add(scratch, sub(len, i)), 0x80)
}
// If this is the last block, store the length
switch eq(i, sub(totallen, 64))
case 1 {
mstore(
add(scratch, 32),
or(mload(add(scratch, 32)), mul(len, 8))
)
}
// Expand the 16 32-bit words into 80
for {
let j := 64
} lt(j, 128) {
j := add(j, 12)
} {
let temp := xor(
xor(
mload(add(scratch, sub(j, 12))),
mload(add(scratch, sub(j, 32)))
),
xor(
mload(add(scratch, sub(j, 56))),
mload(add(scratch, sub(j, 64)))
)
)
temp := or(
and(
mul(temp, 2),
0xFFFFFFFEFFFFFFFEFFFFFFFEFFFFFFFEFFFFFFFEFFFFFFFEFFFFFFFEFFFFFFFE
),
and(
div(temp, 0x80000000),
0x0000000100000001000000010000000100000001000000010000000100000001
)
)
mstore(add(scratch, j), temp)
}
for {
let j := 128
} lt(j, 320) {
j := add(j, 24)
} {
let temp := xor(
xor(
mload(add(scratch, sub(j, 24))),
mload(add(scratch, sub(j, 64)))
),
xor(
mload(add(scratch, sub(j, 112))),
mload(add(scratch, sub(j, 128)))
)
)
temp := or(
and(
mul(temp, 4),
0xFFFFFFFCFFFFFFFCFFFFFFFCFFFFFFFCFFFFFFFCFFFFFFFCFFFFFFFCFFFFFFFC
),
and(
div(temp, 0x40000000),
0x0000000300000003000000030000000300000003000000030000000300000003
)
)
mstore(add(scratch, j), temp)
}
let x := h
let f := 0
let k := 0
for {
let j := 0
} lt(j, 80) {
j := add(j, 1)
} {
switch div(j, 20)
case 0 {
// f = d xor (b and (c xor d))
f := xor(
div(x, 0x100000000000000000000),
div(x, 0x10000000000)
)
f := and(div(x, 0x1000000000000000000000000000000), f)
f := xor(div(x, 0x10000000000), f)
k := 0x5A827999
}
case 1 {
// f = b xor c xor d
f := xor(
div(x, 0x1000000000000000000000000000000),
div(x, 0x100000000000000000000)
)
f := xor(div(x, 0x10000000000), f)
k := 0x6ED9EBA1
}
case 2 {
// f = (b and c) or (d and (b or c))
f := or(
div(x, 0x1000000000000000000000000000000),
div(x, 0x100000000000000000000)
)
f := and(div(x, 0x10000000000), f)
f := or(
and(
div(x, 0x1000000000000000000000000000000),
div(x, 0x100000000000000000000)
),
f
)
k := 0x8F1BBCDC
}
case 3 {
// f = b xor c xor d
f := xor(
div(x, 0x1000000000000000000000000000000),
div(x, 0x100000000000000000000)
)
f := xor(div(x, 0x10000000000), f)
k := 0xCA62C1D6
}
// temp = (a leftrotate 5) + f + e + k + w[i]
let temp := and(
div(
x,
0x80000000000000000000000000000000000000000000000
),
0x1F
)
temp := or(
and(
div(x, 0x800000000000000000000000000000000000000),
0xFFFFFFE0
),
temp
)
temp := add(f, temp)
temp := add(and(x, 0xFFFFFFFF), temp)
temp := add(k, temp)
temp := add(
div(
mload(add(scratch, mul(j, 4))),
0x100000000000000000000000000000000000000000000000000000000
),
temp
)
x := or(
div(x, 0x10000000000),
mul(temp, 0x10000000000000000000000000000000000000000)
)
x := or(
and(
x,
0xFFFFFFFF00FFFFFFFF000000000000FFFFFFFF00FFFFFFFF
),
mul(
or(
and(div(x, 0x4000000000000), 0xC0000000),
and(div(x, 0x400000000000000000000), 0x3FFFFFFF)
),
0x100000000000000000000
)
)
}
h := and(
add(h, x),
0xFFFFFFFF00FFFFFFFF00FFFFFFFF00FFFFFFFF00FFFFFFFF
)
}
ret := mul(
or(
or(
or(
or(
and(
div(h, 0x100000000),
0xFFFFFFFF00000000000000000000000000000000
),
and(
div(h, 0x1000000),
0xFFFFFFFF000000000000000000000000
)
),
and(div(h, 0x10000), 0xFFFFFFFF0000000000000000)
),
and(div(h, 0x100), 0xFFFFFFFF00000000)
),
and(h, 0xFFFFFFFF)
),
0x1000000000000000000000000
)
}
}
}
// ---- dnssec-oracle/digests/SHA256Digest.sol ----
pragma solidity ^0.8.4;
import "./Digest.sol";
import "../../utils/BytesUtils.sol";
/// @dev Implements the DNSSEC SHA256 digest.
contract SHA256Digest is Digest {
using BytesUtils for *;
function verify(
bytes calldata data,
bytes calldata hash
) external pure override returns (bool) {
require(hash.length == 32, "Invalid sha256 hash length");
return sha256(data) == hash.readBytes32(0);
}
}
// ---- dnssec-oracle/digests/Digest.sol ----
pragma solidity ^0.8.4;
/// @dev An interface for contracts implementing a DNSSEC digest.
interface Digest {
/// @dev Verifies a cryptographic hash.
/// @param data The data to hash.
/// @param hash The hash to compare to.
/// @return True iff the hashed data matches the provided hash value.
function verify(
bytes calldata data,
bytes calldata hash
) external pure virtual returns (bool);
}
// ---- dnssec-oracle/digests/DummyDigest.sol ----
pragma solidity ^0.8.4;
import "./Digest.sol";
/// @dev Implements a dummy DNSSEC digest that approves all hashes, for testing.
contract DummyDigest is Digest {
function verify(
bytes calldata,
bytes calldata
) external pure override returns (bool) {
return true;
}
}
// ---- dnssec-oracle/digests/SHA1Digest.sol ----
pragma solidity ^0.8.4;
import "./Digest.sol";
import "../../utils/BytesUtils.sol";
import "@ensdomains/solsha1/contracts/SHA1.sol";
/// @dev Implements the DNSSEC SHA1 digest.
contract SHA1Digest is Digest {
using BytesUtils for *;
function verify(
bytes calldata data,
bytes calldata hash
) external pure override returns (bool) {
require(hash.length == 20, "Invalid sha1 hash length");
bytes32 expected = hash.readBytes20(0);
bytes20 computed = SHA1.sha1(data);
return expected == computed;
}
}
// ---- dnssec-oracle/algorithms/RSASHA256Algorithm.sol ----
pragma solidity ^0.8.4;
import "./Algorithm.sol";
import "./RSAVerify.sol";
import "../../utils/BytesUtils.sol";
/// @dev Implements the DNSSEC RSASHA256 algorithm.
contract RSASHA256Algorithm is Algorithm {
using BytesUtils for *;
function verify(
bytes calldata key,
bytes calldata data,
bytes calldata sig
) external view override returns (bool) {
bytes memory exponent;
bytes memory modulus;
uint16 exponentLen = uint16(key.readUint8(4));
if (exponentLen != 0) {
exponent = key.substring(5, exponentLen);
modulus = key.substring(
exponentLen + 5,
key.length - exponentLen - 5
);
} else {
exponentLen = key.readUint16(5);
exponent = key.substring(7, exponentLen);
modulus = key.substring(
exponentLen + 7,
key.length - exponentLen - 7
);
}
// Recover the message from the signature
bool ok;
bytes memory result;
(ok, result) = RSAVerify.rsarecover(modulus, exponent, sig);
// Verify it ends with the hash of our data
return ok && sha256(data) == result.readBytes32(result.length - 32);
}
}
// ---- dnssec-oracle/algorithms/P256SHA256Algorithm.sol ----
pragma solidity ^0.8.4;
import "./Algorithm.sol";
import "./EllipticCurve.sol";
import "../../utils/BytesUtils.sol";
contract P256SHA256Algorithm is Algorithm, EllipticCurve {
using BytesUtils for *;
/// @dev Verifies a signature.
/// @param key The public key to verify with.
/// @param data The signed data to verify.
/// @param signature The signature to verify.
/// @return True iff the signature is valid.
function verify(
bytes calldata key,
bytes calldata data,
bytes calldata signature
) external view override returns (bool) {
return
validateSignature(
sha256(data),
parseSignature(signature),
parseKey(key)
);
}
function parseSignature(
bytes memory data
) internal pure returns (uint256[2] memory) {
require(data.length == 64, "Invalid p256 signature length");
return [uint256(data.readBytes32(0)), uint256(data.readBytes32(32))];
}
function parseKey(
bytes memory data
) internal pure returns (uint256[2] memory) {
require(data.length == 68, "Invalid p256 key length");
return [uint256(data.readBytes32(4)), uint256(data.readBytes32(36))];
}
}
// ---- dnssec-oracle/algorithms/ModexpPrecompile.sol ----
pragma solidity ^0.8.4;
library ModexpPrecompile {
/// @dev Computes (base ^ exponent) % modulus over big numbers.
function modexp(
bytes memory base,
bytes memory exponent,
bytes memory modulus
) internal view returns (bool success, bytes memory output) {
bytes memory input = abi.encodePacked(
uint256(base.length),
uint256(exponent.length),
uint256(modulus.length),
base,
exponent,
modulus
);
output = new bytes(modulus.length);
assembly {
success := staticcall(
gas(),
5,
add(input, 32),
mload(input),
add(output, 32),
mload(modulus)
)
}
}
}
// ---- dnssec-oracle/algorithms/EllipticCurve.sol ----
pragma solidity ^0.8.4;
/// @title EllipticCurve
/// @author Tilman Drerup;
/// @notice Implements elliptic curve math; Parametrized for SECP256R1.
/// Includes components of code by Andreas Olofsson, Alexander Vlasov
/// (https://github.com/BANKEX/CurveArithmetics), and Avi Asayag
/// (https://github.com/orbs-network/elliptic-curve-solidity)
/// Source: https://github.com/tdrerup/elliptic-curve-solidity
/// @dev NOTE: To disambiguate public keys when verifying signatures, activate
/// condition 'rs[1] > lowSmax' in validateSignature().
contract EllipticCurve {
// Set parameters for curve.
uint256 constant a =
0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC;
uint256 constant b =
0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B;
uint256 constant gx =
0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296;
uint256 constant gy =
0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5;
uint256 constant p =
0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF;
uint256 constant n =
0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551;
uint256 constant lowSmax =
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0;
/// @dev Inverse of u in the field of modulo m.
function inverseMod(uint256 u, uint256 m) internal pure returns (uint256) {
unchecked {
if (u == 0 || u == m || m == 0) return 0;
if (u > m) u = u % m;
int256 t1;
int256 t2 = 1;
uint256 r1 = m;
uint256 r2 = u;
uint256 q;
while (r2 != 0) {
q = r1 / r2;
(t1, t2, r1, r2) = (t2, t1 - int256(q) * t2, r2, r1 - q * r2);
}
if (t1 < 0) return (m - uint256(-t1));
return uint256(t1);
}
}
/// @dev Transform affine coordinates into projective coordinates.
function toProjectivePoint(
uint256 x0,
uint256 y0
) internal pure returns (uint256[3] memory P) {
P[2] = addmod(0, 1, p);
P[0] = mulmod(x0, P[2], p);
P[1] = mulmod(y0, P[2], p);
}
/// @dev Add two points in affine coordinates and return projective point.
function addAndReturnProjectivePoint(
uint256 x1,
uint256 y1,
uint256 x2,
uint256 y2
) internal pure returns (uint256[3] memory P) {
uint256 x;
uint256 y;
(x, y) = add(x1, y1, x2, y2);
P = toProjectivePoint(x, y);
}
/// @dev Transform from projective to affine coordinates.
function toAffinePoint(
uint256 x0,
uint256 y0,
uint256 z0
) internal pure returns (uint256 x1, uint256 y1) {
uint256 z0Inv;
z0Inv = inverseMod(z0, p);
x1 = mulmod(x0, z0Inv, p);
y1 = mulmod(y0, z0Inv, p);
}
/// @dev Return the zero curve in projective coordinates.
function zeroProj()
internal
pure
returns (uint256 x, uint256 y, uint256 z)
{
return (0, 1, 0);
}
/// @dev Return the zero curve in affine coordinates.
function zeroAffine() internal pure returns (uint256 x, uint256 y) {
return (0, 0);
}
/// @dev Check if the curve is the zero curve.
function isZeroCurve(
uint256 x0,
uint256 y0
) internal pure returns (bool isZero) {
if (x0 == 0 && y0 == 0) {
return true;
}
return false;
}
/// @dev Check if a point in affine coordinates is on the curve.
function isOnCurve(uint256 x, uint256 y) internal pure returns (bool) {
if (0 == x || x == p || 0 == y || y == p) {
return false;
}
uint256 LHS = mulmod(y, y, p); // y^2
uint256 RHS = mulmod(mulmod(x, x, p), x, p); // x^3
if (a != 0) {
RHS = addmod(RHS, mulmod(x, a, p), p); // x^3 + a*x
}
if (b != 0) {
RHS = addmod(RHS, b, p); // x^3 + a*x + b
}
return LHS == RHS;
}
/// @dev Double an elliptic curve point in projective coordinates. See
/// https://www.nayuki.io/page/elliptic-curve-point-addition-in-projective-coordinates
function twiceProj(
uint256 x0,
uint256 y0,
uint256 z0
) internal pure returns (uint256 x1, uint256 y1, uint256 z1) {
uint256 t;
uint256 u;
uint256 v;
uint256 w;
if (isZeroCurve(x0, y0)) {
return zeroProj();
}
u = mulmod(y0, z0, p);
u = mulmod(u, 2, p);
v = mulmod(u, x0, p);
v = mulmod(v, y0, p);
v = mulmod(v, 2, p);
x0 = mulmod(x0, x0, p);
t = mulmod(x0, 3, p);
z0 = mulmod(z0, z0, p);
z0 = mulmod(z0, a, p);
t = addmod(t, z0, p);
w = mulmod(t, t, p);
x0 = mulmod(2, v, p);
w = addmod(w, p - x0, p);
x0 = addmod(v, p - w, p);
x0 = mulmod(t, x0, p);
y0 = mulmod(y0, u, p);
y0 = mulmod(y0, y0, p);
y0 = mulmod(2, y0, p);
y1 = addmod(x0, p - y0, p);
x1 = mulmod(u, w, p);
z1 = mulmod(u, u, p);
z1 = mulmod(z1, u, p);
}
/// @dev Add two elliptic curve points in projective coordinates. See
/// https://www.nayuki.io/page/elliptic-curve-point-addition-in-projective-coordinates
function addProj(
uint256 x0,
uint256 y0,
uint256 z0,
uint256 x1,
uint256 y1,
uint256 z1
) internal pure returns (uint256 x2, uint256 y2, uint256 z2) {
uint256 t0;
uint256 t1;
uint256 u0;
uint256 u1;
if (isZeroCurve(x0, y0)) {
return (x1, y1, z1);
} else if (isZeroCurve(x1, y1)) {
return (x0, y0, z0);
}
t0 = mulmod(y0, z1, p);
t1 = mulmod(y1, z0, p);
u0 = mulmod(x0, z1, p);
u1 = mulmod(x1, z0, p);
if (u0 == u1) {
if (t0 == t1) {
return twiceProj(x0, y0, z0);
} else {
return zeroProj();
}
}
(x2, y2, z2) = addProj2(mulmod(z0, z1, p), u0, u1, t1, t0);
}
/// @dev Helper function that splits addProj to avoid too many local variables.
function addProj2(
uint256 v,
uint256 u0,
uint256 u1,
uint256 t1,
uint256 t0
) private pure returns (uint256 x2, uint256 y2, uint256 z2) {
uint256 u;
uint256 u2;
uint256 u3;
uint256 w;
uint256 t;
t = addmod(t0, p - t1, p);
u = addmod(u0, p - u1, p);
u2 = mulmod(u, u, p);
w = mulmod(t, t, p);
w = mulmod(w, v, p);
u1 = addmod(u1, u0, p);
u1 = mulmod(u1, u2, p);
w = addmod(w, p - u1, p);
x2 = mulmod(u, w, p);
u3 = mulmod(u2, u, p);
u0 = mulmod(u0, u2, p);
u0 = addmod(u0, p - w, p);
t = mulmod(t, u0, p);
t0 = mulmod(t0, u3, p);
y2 = addmod(t, p - t0, p);
z2 = mulmod(u3, v, p);
}
/// @dev Add two elliptic curve points in affine coordinates.
function add(
uint256 x0,
uint256 y0,
uint256 x1,
uint256 y1
) internal pure returns (uint256, uint256) {
uint256 z0;
(x0, y0, z0) = addProj(x0, y0, 1, x1, y1, 1);
return toAffinePoint(x0, y0, z0);
}
/// @dev Double an elliptic curve point in affine coordinates.
function twice(
uint256 x0,
uint256 y0
) internal pure returns (uint256, uint256) {
uint256 z0;
(x0, y0, z0) = twiceProj(x0, y0, 1);
return toAffinePoint(x0, y0, z0);
}
/// @dev Multiply an elliptic curve point by a 2 power base (i.e., (2^exp)*P)).
function multiplyPowerBase2(
uint256 x0,
uint256 y0,
uint256 exp
) internal pure returns (uint256, uint256) {
uint256 base2X = x0;
uint256 base2Y = y0;
uint256 base2Z = 1;
for (uint256 i = 0; i < exp; i++) {
(base2X, base2Y, base2Z) = twiceProj(base2X, base2Y, base2Z);
}
return toAffinePoint(base2X, base2Y, base2Z);
}
/// @dev Multiply an elliptic curve point by a scalar.
function multiplyScalar(
uint256 x0,
uint256 y0,
uint256 scalar
) internal pure returns (uint256 x1, uint256 y1) {
if (scalar == 0) {
return zeroAffine();
} else if (scalar == 1) {
return (x0, y0);
} else if (scalar == 2) {
return twice(x0, y0);
}
uint256 base2X = x0;
uint256 base2Y = y0;
uint256 base2Z = 1;
uint256 z1 = 1;
x1 = x0;
y1 = y0;
if (scalar % 2 == 0) {
x1 = y1 = 0;
}
scalar = scalar >> 1;
while (scalar > 0) {
(base2X, base2Y, base2Z) = twiceProj(base2X, base2Y, base2Z);
if (scalar % 2 == 1) {
(x1, y1, z1) = addProj(base2X, base2Y, base2Z, x1, y1, z1);
}
scalar = scalar >> 1;
}
return toAffinePoint(x1, y1, z1);
}
/// @dev Multiply the curve's generator point by a scalar.
function multipleGeneratorByScalar(
uint256 scalar
) internal pure returns (uint256, uint256) {
return multiplyScalar(gx, gy, scalar);
}
/// @dev Validate combination of message, signature, and public key.
function validateSignature(
bytes32 message,
uint256[2] memory rs,
uint256[2] memory Q
) internal pure returns (bool) {
// To disambiguate between public key solutions, include comment below.
if (rs[0] == 0 || rs[0] >= n || rs[1] == 0) {
// || rs[1] > lowSmax)
return false;
}
if (!isOnCurve(Q[0], Q[1])) {
return false;
}
uint256 x1;
uint256 x2;
uint256 y1;
uint256 y2;
uint256 sInv = inverseMod(rs[1], n);
(x1, y1) = multiplyScalar(gx, gy, mulmod(uint256(message), sInv, n));
(x2, y2) = multiplyScalar(Q[0], Q[1], mulmod(rs[0], sInv, n));
uint256[3] memory P = addAndReturnProjectivePoint(x1, y1, x2, y2);
if (P[2] == 0) {
return false;
}
uint256 Px = inverseMod(P[2], p);
Px = mulmod(P[0], mulmod(Px, Px, p), p);
return Px % n == rs[0];
}
}
// ---- dnssec-oracle/algorithms/RSASHA1Algorithm.sol ----
pragma solidity ^0.8.4;
import "./Algorithm.sol";
import "./RSAVerify.sol";
import "../../utils/BytesUtils.sol";
import "@ensdomains/solsha1/contracts/SHA1.sol";
/// @dev Implements the DNSSEC RSASHA1 algorithm.
contract RSASHA1Algorithm is Algorithm {
using BytesUtils for *;
function verify(
bytes calldata key,
bytes calldata data,
bytes calldata sig
) external view override returns (bool) {
bytes memory exponent;
bytes memory modulus;
uint16 exponentLen = uint16(key.readUint8(4));
if (exponentLen != 0) {
exponent = key.substring(5, exponentLen);
modulus = key.substring(
exponentLen + 5,
key.length - exponentLen - 5
);
} else {
exponentLen = key.readUint16(5);
exponent = key.substring(7, exponentLen);
modulus = key.substring(
exponentLen + 7,
key.length - exponentLen - 7
);
}
// Recover the message from the signature
bool ok;
bytes memory result;
(ok, result) = RSAVerify.rsarecover(modulus, exponent, sig);
// Verify it ends with the hash of our data
return ok && SHA1.sha1(data) == result.readBytes20(result.length - 20);
}
}
// ---- dnssec-oracle/algorithms/DummyAlgorithm.sol ----
pragma solidity ^0.8.4;
import "./Algorithm.sol";
/// @dev Implements a dummy DNSSEC (signing) algorithm that approves all
/// signatures, for testing.
contract DummyAlgorithm is Algorithm {
function verify(
bytes calldata,
bytes calldata,
bytes calldata
) external view override returns (bool) {
return true;
}
}
// ---- dnssec-oracle/algorithms/RSAVerify.sol ----
pragma solidity ^0.8.4;
import "./ModexpPrecompile.sol";
import "../../utils/BytesUtils.sol";
library RSAVerify {
/// @dev Recovers the input data from an RSA signature, returning the result in S.
/// @param N The RSA public modulus.
/// @param E The RSA public exponent.
/// @param S The signature to recover.
/// @return True if the recovery succeeded.
function rsarecover(
bytes memory N,
bytes memory E,
bytes memory S
) internal view returns (bool, bytes memory) {
return ModexpPrecompile.modexp(S, E, N);
}
}
// ---- dnssec-oracle/algorithms/Algorithm.sol ----
pragma solidity ^0.8.4;
/// @dev An interface for contracts implementing a DNSSEC (signing) algorithm.
interface Algorithm {
/// @dev Verifies a signature.
/// @param key The public key to verify with.
/// @param data The signed data to verify.
/// @param signature The signature to verify.
/// @return True iff the signature is valid.
function verify(
bytes calldata key,
bytes calldata data,
bytes calldata signature
) external view virtual returns (bool);
}
// ---- dnssec-oracle/RRUtils.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "../utils/BytesUtils.sol";
import "@ensdomains/buffer/contracts/Buffer.sol";
/// @dev RRUtils is a library that provides utilities for parsing DNS resource records.
library RRUtils {
using BytesUtils for *;
using Buffer for *;
/// @dev Returns the number of bytes in the DNS name at 'offset' in 'self'.
/// @param self The byte array to read a name from.
/// @param offset The offset to start reading at.
/// @return The length of the DNS name at 'offset', in bytes.
function nameLength(
bytes memory self,
uint256 offset
) internal pure returns (uint256) {
uint256 idx = offset;
while (true) {
assert(idx < self.length);
uint256 labelLen = self.readUint8(idx);
idx += labelLen + 1;
if (labelLen == 0) {
break;
}
}
return idx - offset;
}
/// @dev Returns a DNS format name at the specified offset of self.
/// @param self The byte array to read a name from.
/// @param offset The offset to start reading at.
/// @return ret The name.
function readName(
bytes memory self,
uint256 offset
) internal pure returns (bytes memory ret) {
uint256 len = nameLength(self, offset);
return self.substring(offset, len);
}
/// @dev Returns the number of labels in the DNS name at 'offset' in 'self'.
/// @param self The byte array to read a name from.
/// @param offset The offset to start reading at.
/// @return The number of labels in the DNS name at 'offset', in bytes.
function labelCount(
bytes memory self,
uint256 offset
) internal pure returns (uint256) {
uint256 count = 0;
while (true) {
assert(offset < self.length);
uint256 labelLen = self.readUint8(offset);
offset += labelLen + 1;
if (labelLen == 0) {
break;
}
count += 1;
}
return count;
}
uint256 constant RRSIG_TYPE = 0;
uint256 constant RRSIG_ALGORITHM = 2;
uint256 constant RRSIG_LABELS = 3;
uint256 constant RRSIG_TTL = 4;
uint256 constant RRSIG_EXPIRATION = 8;
uint256 constant RRSIG_INCEPTION = 12;
uint256 constant RRSIG_KEY_TAG = 16;
uint256 constant RRSIG_SIGNER_NAME = 18;
struct SignedSet {
uint16 typeCovered;
uint8 algorithm;
uint8 labels;
uint32 ttl;
uint32 expiration;
uint32 inception;
uint16 keytag;
bytes signerName;
bytes data;
bytes name;
}
function readSignedSet(
bytes memory data
) internal pure returns (SignedSet memory self) {
self.typeCovered = data.readUint16(RRSIG_TYPE);
self.algorithm = data.readUint8(RRSIG_ALGORITHM);
self.labels = data.readUint8(RRSIG_LABELS);
self.ttl = data.readUint32(RRSIG_TTL);
self.expiration = data.readUint32(RRSIG_EXPIRATION);
self.inception = data.readUint32(RRSIG_INCEPTION);
self.keytag = data.readUint16(RRSIG_KEY_TAG);
self.signerName = readName(data, RRSIG_SIGNER_NAME);
self.data = data.substring(
RRSIG_SIGNER_NAME + self.signerName.length,
data.length - RRSIG_SIGNER_NAME - self.signerName.length
);
}
function rrs(
SignedSet memory rrset
) internal pure returns (RRIterator memory) {
return iterateRRs(rrset.data, 0);
}
/// @dev An iterator over resource records.
struct RRIterator {
bytes data;
uint256 offset;
uint16 dnstype;
uint16 class;
uint32 ttl;
uint256 rdataOffset;
uint256 nextOffset;
}
/// @dev Begins iterating over resource records.
/// @param self The byte string to read from.
/// @param offset The offset to start reading at.
/// @return ret An iterator object.
function iterateRRs(
bytes memory self,
uint256 offset
) internal pure returns (RRIterator memory ret) {
ret.data = self;
ret.nextOffset = offset;
next(ret);
}
/// @dev Returns true iff there are more RRs to iterate.
/// @param iter The iterator to check.
/// @return True iff the iterator has finished.
function done(RRIterator memory iter) internal pure returns (bool) {
return iter.offset >= iter.data.length;
}
/// @dev Moves the iterator to the next resource record.
/// @param iter The iterator to advance.
function next(RRIterator memory iter) internal pure {
iter.offset = iter.nextOffset;
if (iter.offset >= iter.data.length) {
return;
}
// Skip the name
uint256 off = iter.offset + nameLength(iter.data, iter.offset);
// Read type, class, and ttl
iter.dnstype = iter.data.readUint16(off);
off += 2;
iter.class = iter.data.readUint16(off);
off += 2;
iter.ttl = iter.data.readUint32(off);
off += 4;
// Read the rdata
uint256 rdataLength = iter.data.readUint16(off);
off += 2;
iter.rdataOffset = off;
iter.nextOffset = off + rdataLength;
}
/// @dev Returns the name of the current record.
/// @param iter The iterator.
/// @return A new bytes object containing the owner name from the RR.
function name(RRIterator memory iter) internal pure returns (bytes memory) {
return
iter.data.substring(
iter.offset,
nameLength(iter.data, iter.offset)
);
}
/// @dev Returns the rdata portion of the current record.
/// @param iter The iterator.
/// @return A new bytes object containing the RR's RDATA.
function rdata(
RRIterator memory iter
) internal pure returns (bytes memory) {
return
iter.data.substring(
iter.rdataOffset,
iter.nextOffset - iter.rdataOffset
);
}
uint256 constant DNSKEY_FLAGS = 0;
uint256 constant DNSKEY_PROTOCOL = 2;
uint256 constant DNSKEY_ALGORITHM = 3;
uint256 constant DNSKEY_PUBKEY = 4;
struct DNSKEY {
uint16 flags;
uint8 protocol;
uint8 algorithm;
bytes publicKey;
}
function readDNSKEY(
bytes memory data,
uint256 offset,
uint256 length
) internal pure returns (DNSKEY memory self) {
self.flags = data.readUint16(offset + DNSKEY_FLAGS);
self.protocol = data.readUint8(offset + DNSKEY_PROTOCOL);
self.algorithm = data.readUint8(offset + DNSKEY_ALGORITHM);
self.publicKey = data.substring(
offset + DNSKEY_PUBKEY,
length - DNSKEY_PUBKEY
);
}
uint256 constant DS_KEY_TAG = 0;
uint256 constant DS_ALGORITHM = 2;
uint256 constant DS_DIGEST_TYPE = 3;
uint256 constant DS_DIGEST = 4;
struct DS {
uint16 keytag;
uint8 algorithm;
uint8 digestType;
bytes digest;
}
function readDS(
bytes memory data,
uint256 offset,
uint256 length
) internal pure returns (DS memory self) {
self.keytag = data.readUint16(offset + DS_KEY_TAG);
self.algorithm = data.readUint8(offset + DS_ALGORITHM);
self.digestType = data.readUint8(offset + DS_DIGEST_TYPE);
self.digest = data.substring(offset + DS_DIGEST, length - DS_DIGEST);
}
function isSubdomainOf(
bytes memory self,
bytes memory other
) internal pure returns (bool) {
uint256 off = 0;
uint256 counts = labelCount(self, 0);
uint256 othercounts = labelCount(other, 0);
while (counts > othercounts) {
off = progress(self, off);
counts--;
}
return self.equals(off, other, 0);
}
function compareNames(
bytes memory self,
bytes memory other
) internal pure returns (int256) {
if (self.equals(other)) {
return 0;
}
uint256 off;
uint256 otheroff;
uint256 prevoff;
uint256 otherprevoff;
uint256 counts = labelCount(self, 0);
uint256 othercounts = labelCount(other, 0);
// Keep removing labels from the front of the name until both names are equal length
while (counts > othercounts) {
prevoff = off;
off = progress(self, off);
counts--;
}
while (othercounts > counts) {
otherprevoff = otheroff;
otheroff = progress(other, otheroff);
othercounts--;
}
// Compare the last nonequal labels to each other
while (counts > 0 && !self.equals(off, other, otheroff)) {
prevoff = off;
off = progress(self, off);
otherprevoff = otheroff;
otheroff = progress(other, otheroff);
counts -= 1;
}
if (off == 0) {
return -1;
}
if (otheroff == 0) {
return 1;
}
return
self.compare(
prevoff + 1,
self.readUint8(prevoff),
other,
otherprevoff + 1,
other.readUint8(otherprevoff)
);
}
/// @dev Compares two serial numbers using RFC1982 serial number math.
function serialNumberGte(
uint32 i1,
uint32 i2
) internal pure returns (bool) {
unchecked {
return int32(i1) - int32(i2) >= 0;
}
}
function progress(
bytes memory body,
uint256 off
) internal pure returns (uint256) {
return off + 1 + body.readUint8(off);
}
/// @dev Computes the keytag for a chunk of data.
/// @param data The data to compute a keytag for.
/// @return The computed key tag.
function computeKeytag(bytes memory data) internal pure returns (uint16) {
/* This function probably deserves some explanation.
* The DNSSEC keytag function is a checksum that relies on summing up individual bytes
* from the input string, with some mild bitshifting. Here's a Naive solidity implementation:
*
* function computeKeytag(bytes memory data) internal pure returns (uint16) {
* uint ac;
* for (uint i = 0; i < data.length; i++) {
* ac += i & 1 == 0 ? uint16(data.readUint8(i)) << 8 : data.readUint8(i);
* }
* return uint16(ac + (ac >> 16));
* }
*
* The EVM, with its 256 bit words, is exceedingly inefficient at doing byte-by-byte operations;
* the code above, on reasonable length inputs, consumes over 100k gas. But we can make the EVM's
* large words work in our favour.
*
* The code below works by treating the input as a series of 256 bit words. It first masks out
* even and odd bytes from each input word, adding them to two separate accumulators `ac1` and `ac2`.
* The bytes are separated by empty bytes, so as long as no individual sum exceeds 2^16-1, we're
* effectively summing 16 different numbers with each EVM ADD opcode.
*
* Once it's added up all the inputs, it has to add all the 16 bit values in `ac1` and `ac2` together.
* It does this using the same trick - mask out every other value, shift to align them, add them together.
* After the first addition on both accumulators, there's enough room to add the two accumulators together,
* and the remaining sums can be done just on ac1.
*/
unchecked {
require(data.length <= 8192, "Long keys not permitted");
uint256 ac1;
uint256 ac2;
for (uint256 i = 0; i < data.length + 31; i += 32) {
uint256 word;
assembly {
word := mload(add(add(data, 32), i))
}
if (i + 32 > data.length) {
uint256 unused = 256 - (data.length - i) * 8;
word = (word >> unused) << unused;
}
ac1 +=
(word &
0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >>
8;
ac2 += (word &
0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF);
}
ac1 =
(ac1 &
0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) +
((ac1 &
0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >>
16);
ac2 =
(ac2 &
0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) +
((ac2 &
0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >>
16);
ac1 = (ac1 << 8) + ac2;
ac1 =
(ac1 &
0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) +
((ac1 &
0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >>
32);
ac1 =
(ac1 &
0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) +
((ac1 &
0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >>
64);
ac1 =
(ac1 &
0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) +
(ac1 >> 128);
ac1 += (ac1 >> 16) & 0xFFFF;
return uint16(ac1);
}
}
}
// ---- dnssec-oracle/DNSSECImpl.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
pragma experimental ABIEncoderV2;
import "./Owned.sol";
import "./RRUtils.sol";
import "./DNSSEC.sol";
import "./algorithms/Algorithm.sol";
import "./digests/Digest.sol";
import "../utils/BytesUtils.sol";
import "@ensdomains/buffer/contracts/Buffer.sol";
/*
* @dev An oracle contract that verifies and stores DNSSEC-validated DNS records.
* @note This differs from the DNSSEC spec defined in RFC4034 and RFC4035 in some key regards:
* - NSEC & NSEC3 are not supported; only positive proofs are allowed.
* - Proofs involving wildcard names will not validate.
* - TTLs on records are ignored, as data is not stored persistently.
* - Canonical form of names is not checked; in ENS this is done on the frontend, so submitting
* proofs with non-canonical names will only result in registering unresolvable ENS names.
*/
contract DNSSECImpl is DNSSEC, Owned {
using Buffer for Buffer.buffer;
using BytesUtils for bytes;
using RRUtils for *;
uint16 constant DNSCLASS_IN = 1;
uint16 constant DNSTYPE_DS = 43;
uint16 constant DNSTYPE_DNSKEY = 48;
uint256 constant DNSKEY_FLAG_ZONEKEY = 0x100;
error InvalidLabelCount(bytes name, uint256 labelsExpected);
error SignatureNotValidYet(uint32 inception, uint32 now);
error SignatureExpired(uint32 expiration, uint32 now);
error InvalidClass(uint16 class);
error InvalidRRSet();
error SignatureTypeMismatch(uint16 rrsetType, uint16 sigType);
error InvalidSignerName(bytes rrsetName, bytes signerName);
error InvalidProofType(uint16 proofType);
error ProofNameMismatch(bytes signerName, bytes proofName);
error NoMatchingProof(bytes signerName);
mapping(uint8 => Algorithm) public algorithms;
mapping(uint8 => Digest) public digests;
/// @dev Constructor.
/// @param _anchors The binary format RR entries for the root DS records.
constructor(bytes memory _anchors) {
// Insert the 'trust anchors' - the key hashes that start the chain
// of trust for all other records.
anchors = _anchors;
}
/// @dev Sets the contract address for a signature verification algorithm.
/// Callable only by the owner.
/// @param id The algorithm ID
/// @param algo The address of the algorithm contract.
function setAlgorithm(uint8 id, Algorithm algo) public owner_only {
algorithms[id] = algo;
emit AlgorithmUpdated(id, address(algo));
}
/// @dev Sets the contract address for a digest verification algorithm.
/// Callable only by the owner.
/// @param id The digest ID
/// @param digest The address of the digest contract.
function setDigest(uint8 id, Digest digest) public owner_only {
digests[id] = digest;
emit DigestUpdated(id, address(digest));
}
/// @dev Takes a chain of signed DNS records, verifies them, and returns the data from the last record set in the chain.
/// Reverts if the records do not form an unbroken chain of trust to the DNSSEC anchor records.
/// @param input A list of signed RRSets.
/// @return rrs The RRData from the last RRSet in the chain.
/// @return inception The inception time of the signed record set.
function verifyRRSet(
RRSetWithSignature[] memory input
)
external
view
virtual
override
returns (bytes memory rrs, uint32 inception)
{
return verifyRRSet(input, block.timestamp);
}
/// @dev Takes a chain of signed DNS records, verifies them, and returns the data from the last record set in the chain.
/// Reverts if the records do not form an unbroken chain of trust to the DNSSEC anchor records.
/// @param input A list of signed RRSets.
/// @param now The Unix timestamp to validate the records at.
/// @return rrs The RRData from the last RRSet in the chain.
/// @return inception The inception time of the signed record set.
function verifyRRSet(
RRSetWithSignature[] memory input,
uint256 now
)
public
view
virtual
override
returns (bytes memory rrs, uint32 inception)
{
bytes memory proof = anchors;
for (uint256 i = 0; i < input.length; i++) {
RRUtils.SignedSet memory rrset = validateSignedSet(
input[i],
proof,
now
);
proof = rrset.data;
inception = rrset.inception;
}
return (proof, inception);
}
/// @dev Validates an RRSet against the already trusted RR provided in `proof`.
///
/// @param input The signed RR set. This is in the format described in section
/// 5.3.2 of RFC4035: The RRDATA section from the RRSIG without the signature
/// data, followed by a series of canonicalised RR records that the signature
/// applies to.
/// @param proof The DNSKEY or DS to validate the signature against.
/// @param now The current timestamp.
function validateSignedSet(
RRSetWithSignature memory input,
bytes memory proof,
uint256 now
) internal view returns (RRUtils.SignedSet memory rrset) {
rrset = input.rrset.readSignedSet();
// Do some basic checks on the RRs and extract the name
bytes memory name = validateRRs(rrset, rrset.typeCovered);
if (name.labelCount(0) != rrset.labels) {
revert InvalidLabelCount(name, rrset.labels);
}
rrset.name = name;
// All comparisons involving the Signature Expiration and
// Inception fields MUST use "serial number arithmetic", as
// defined in RFC 1982
// o The validator's notion of the current time MUST be less than or
// equal to the time listed in the RRSIG RR's Expiration field.
if (!RRUtils.serialNumberGte(rrset.expiration, uint32(now))) {
revert SignatureExpired(rrset.expiration, uint32(now));
}
// o The validator's notion of the current time MUST be greater than or
// equal to the time listed in the RRSIG RR's Inception field.
if (!RRUtils.serialNumberGte(uint32(now), rrset.inception)) {
revert SignatureNotValidYet(rrset.inception, uint32(now));
}
// Validate the signature
verifySignature(name, rrset, input, proof);
return rrset;
}
/// @dev Validates a set of RRs.
/// @param rrset The RR set.
/// @param typecovered The type covered by the RRSIG record.
function validateRRs(
RRUtils.SignedSet memory rrset,
uint16 typecovered
) internal pure returns (bytes memory name) {
// Iterate over all the RRs
for (
RRUtils.RRIterator memory iter = rrset.rrs();
!iter.done();
iter.next()
) {
// We only support class IN (Internet)
if (iter.class != DNSCLASS_IN) {
revert InvalidClass(iter.class);
}
if (name.length == 0) {
name = iter.name();
} else {
// Name must be the same on all RRs. We do things this way to avoid copying the name
// repeatedly.
if (
name.length != iter.data.nameLength(iter.offset) ||
!name.equals(0, iter.data, iter.offset, name.length)
) {
revert InvalidRRSet();
}
}
// o The RRSIG RR's Type Covered field MUST equal the RRset's type.
if (iter.dnstype != typecovered) {
revert SignatureTypeMismatch(iter.dnstype, typecovered);
}
}
}
/// @dev Performs signature verification.
///
/// Throws or reverts if unable to verify the record.
///
/// @param name The name of the RRSIG record, in DNS label-sequence format.
/// @param data The original data to verify.
/// @param proof A DS or DNSKEY record that's already verified by the oracle.
function verifySignature(
bytes memory name,
RRUtils.SignedSet memory rrset,
RRSetWithSignature memory data,
bytes memory proof
) internal view {
// o The RRSIG RR's Signer's Name field MUST be the name of the zone
// that contains the RRset.
if (!name.isSubdomainOf(rrset.signerName)) {
revert InvalidSignerName(name, rrset.signerName);
}
RRUtils.RRIterator memory proofRR = proof.iterateRRs(0);
// Check the proof
if (proofRR.dnstype == DNSTYPE_DS) {
verifyWithDS(rrset, data, proofRR);
} else if (proofRR.dnstype == DNSTYPE_DNSKEY) {
verifyWithKnownKey(rrset, data, proofRR);
} else {
revert InvalidProofType(proofRR.dnstype);
}
}
/// @dev Attempts to verify a signed RRSET against an already known public key.
/// @param rrset The signed set to verify.
/// @param data The original data the signed set was read from.
/// @param proof The serialized DS or DNSKEY record to use as proof.
function verifyWithKnownKey(
RRUtils.SignedSet memory rrset,
RRSetWithSignature memory data,
RRUtils.RRIterator memory proof
) internal view {
// Check the DNSKEY's owner name matches the signer name on the RRSIG
for (; !proof.done(); proof.next()) {
bytes memory proofName = proof.name();
if (!proofName.equals(rrset.signerName)) {
revert ProofNameMismatch(rrset.signerName, proofName);
}
bytes memory keyrdata = proof.rdata();
RRUtils.DNSKEY memory dnskey = keyrdata.readDNSKEY(
0,
keyrdata.length
);
if (verifySignatureWithKey(dnskey, keyrdata, rrset, data)) {
return;
}
}
revert NoMatchingProof(rrset.signerName);
}
/// @dev Attempts to verify some data using a provided key and a signature.
/// @param dnskey The dns key record to verify the signature with.
/// @param rrset The signed RRSET being verified.
/// @param data The original data `rrset` was decoded from.
/// @return True iff the key verifies the signature.
function verifySignatureWithKey(
RRUtils.DNSKEY memory dnskey,
bytes memory keyrdata,
RRUtils.SignedSet memory rrset,
RRSetWithSignature memory data
) internal view returns (bool) {
// TODO: Check key isn't expired, unless updating key itself
// The Protocol Field MUST have value 3 (RFC4034 2.1.2)
if (dnskey.protocol != 3) {
return false;
}
// o The RRSIG RR's Signer's Name, Algorithm, and Key Tag fields MUST
// match the owner name, algorithm, and key tag for some DNSKEY RR in
// the zone's apex DNSKEY RRset.
if (dnskey.algorithm != rrset.algorithm) {
return false;
}
uint16 computedkeytag = keyrdata.computeKeytag();
if (computedkeytag != rrset.keytag) {
return false;
}
// o The matching DNSKEY RR MUST be present in the zone's apex DNSKEY
// RRset, and MUST have the Zone Flag bit (DNSKEY RDATA Flag bit 7)
// set.
if (dnskey.flags & DNSKEY_FLAG_ZONEKEY == 0) {
return false;
}
Algorithm algorithm = algorithms[dnskey.algorithm];
if (address(algorithm) == address(0)) {
return false;
}
return algorithm.verify(keyrdata, data.rrset, data.sig);
}
/// @dev Attempts to verify a signed RRSET against an already known hash. This function assumes
/// that the record
/// @param rrset The signed set to verify.
/// @param data The original data the signed set was read from.
/// @param proof The serialized DS or DNSKEY record to use as proof.
function verifyWithDS(
RRUtils.SignedSet memory rrset,
RRSetWithSignature memory data,
RRUtils.RRIterator memory proof
) internal view {
uint256 proofOffset = proof.offset;
for (
RRUtils.RRIterator memory iter = rrset.rrs();
!iter.done();
iter.next()
) {
if (iter.dnstype != DNSTYPE_DNSKEY) {
revert InvalidProofType(iter.dnstype);
}
bytes memory keyrdata = iter.rdata();
RRUtils.DNSKEY memory dnskey = keyrdata.readDNSKEY(
0,
keyrdata.length
);
if (verifySignatureWithKey(dnskey, keyrdata, rrset, data)) {
// It's self-signed - look for a DS record to verify it.
if (
verifyKeyWithDS(rrset.signerName, proof, dnskey, keyrdata)
) {
return;
}
// Rewind proof iterator to the start for the next loop iteration.
proof.nextOffset = proofOffset;
proof.next();
}
}
revert NoMatchingProof(rrset.signerName);
}
/// @dev Attempts to verify a key using DS records.
/// @param keyname The DNS name of the key, in DNS label-sequence format.
/// @param dsrrs The DS records to use in verification.
/// @param dnskey The dnskey to verify.
/// @param keyrdata The RDATA section of the key.
/// @return True if a DS record verifies this key.
function verifyKeyWithDS(
bytes memory keyname,
RRUtils.RRIterator memory dsrrs,
RRUtils.DNSKEY memory dnskey,
bytes memory keyrdata
) internal view returns (bool) {
uint16 keytag = keyrdata.computeKeytag();
for (; !dsrrs.done(); dsrrs.next()) {
bytes memory proofName = dsrrs.name();
if (!proofName.equals(keyname)) {
revert ProofNameMismatch(keyname, proofName);
}
RRUtils.DS memory ds = dsrrs.data.readDS(
dsrrs.rdataOffset,
dsrrs.nextOffset - dsrrs.rdataOffset
);
if (ds.keytag != keytag) {
continue;
}
if (ds.algorithm != dnskey.algorithm) {
continue;
}
Buffer.buffer memory buf;
buf.init(keyname.length + keyrdata.length);
buf.append(keyname);
buf.append(keyrdata);
if (verifyDSHash(ds.digestType, buf.buf, ds.digest)) {
return true;
}
}
return false;
}
/// @dev Attempts to verify a DS record's hash value against some data.
/// @param digesttype The digest ID from the DS record.
/// @param data The data to digest.
/// @param digest The digest data to check against.
/// @return True if the digest matches.
function verifyDSHash(
uint8 digesttype,
bytes memory data,
bytes memory digest
) internal view returns (bool) {
if (address(digests[digesttype]) == address(0)) {
return false;
}
return digests[digesttype].verify(data, digest);
}
}
// ---- test/TestRRUtils.sol ----
pragma solidity ^0.8.4;
import "../../contracts/dnssec-oracle/RRUtils.sol";
import "../../contracts/utils/BytesUtils.sol";
contract TestRRUtils {
using BytesUtils for *;
using RRUtils for *;
uint16 constant DNSTYPE_A = 1;
uint16 constant DNSTYPE_CNAME = 5;
uint16 constant DNSTYPE_MX = 15;
uint16 constant DNSTYPE_TEXT = 16;
uint16 constant DNSTYPE_RRSIG = 46;
uint16 constant DNSTYPE_TYPE1234 = 1234;
function testNameLength() public pure {
require(hex"00".nameLength(0) == 1, "nameLength('.') == 1");
require(hex"0361626300".nameLength(4) == 1, "nameLength('.') == 1");
require(hex"0361626300".nameLength(0) == 5, "nameLength('abc.') == 5");
}
function testLabelCount() public pure {
require(hex"00".labelCount(0) == 0, "labelCount('.') == 0");
require(hex"016100".labelCount(0) == 1, "labelCount('a.') == 1");
require(
hex"016201610000".labelCount(0) == 2,
"labelCount('b.a.') == 2"
);
require(
hex"066574686c61620378797a00".labelCount(6 + 1) == 1,
"nameLength('(bthlab).xyz.') == 6"
);
}
function testIterateRRs() public pure {
// a. IN A 3600 127.0.0.1
// b.a. IN A 3600 192.168.1.1
bytes
memory rrs = hex"0161000001000100000e1000047400000101620161000001000100000e100004c0a80101";
bytes[2] memory names = [bytes(hex"016100"), bytes(hex"0162016100")];
bytes[2] memory rdatas = [bytes(hex"74000001"), bytes(hex"c0a80101")];
uint i = 0;
for (
RRUtils.RRIterator memory iter = rrs.iterateRRs(0);
!iter.done();
iter.next()
) {
require(uint(iter.dnstype) == 1, "Type matches");
require(uint(iter.class) == 1, "Class matches");
require(uint(iter.ttl) == 3600, "TTL matches");
require(
keccak256(iter.name()) == keccak256(names[i]),
"Name matches"
);
require(
keccak256(iter.rdata()) == keccak256(rdatas[i]),
"Rdata matches"
);
i++;
}
require(i == 2, "Expected 2 records");
}
// Canonical ordering https://tools.ietf.org/html/rfc4034#section-6.1
function testCompareNames() public pure {
bytes memory bthLabXyz = hex"066274686c61620378797a00";
bytes memory ethLabXyz = hex"066574686c61620378797a00";
bytes memory xyz = hex"0378797a00";
bytes memory a_b_c = hex"01610162016300";
bytes memory b_b_c = hex"01620162016300";
bytes memory c = hex"016300";
bytes memory d = hex"016400";
bytes memory a_d_c = hex"01610164016300";
bytes memory b_a_c = hex"01620161016300";
bytes memory ab_c_d = hex"0261620163016400";
bytes memory a_c_d = hex"01610163016400";
bytes
memory verylong1_eth = hex"223031323334353637383930313233343536373839303132333435363738393031613031303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334353637380365746800";
bytes
memory verylong2_eth = hex"2130313233343536373839303132333435363738393031323334353637383930316131303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334353637380365746800";
require(
hex"0301616100".compareNames(hex"0302616200") < 0,
"label lengths are correctly checked"
);
require(
a_b_c.compareNames(c) > 0,
"one name has a difference of >1 label to with the same root name"
);
require(
a_b_c.compareNames(d) < 0,
"one name has a difference of >1 label to with different root name"
);
require(
a_b_c.compareNames(a_d_c) < 0,
"two names start the same but have differences in later labels"
);
require(
a_b_c.compareNames(b_a_c) > 0,
"the first label sorts later, but the first label sorts earlier"
);
require(
ab_c_d.compareNames(a_c_d) > 0,
"two names where the first label on one is a prefix of the first label on the other"
);
require(
a_b_c.compareNames(b_b_c) < 0,
"two names where the first label on one is a prefix of the first label on the other"
);
require(xyz.compareNames(ethLabXyz) < 0, "xyz comes before ethLab.xyz");
require(
bthLabXyz.compareNames(ethLabXyz) < 0,
"bthLab.xyz comes before ethLab.xyz"
);
require(
bthLabXyz.compareNames(bthLabXyz) == 0,
"bthLab.xyz and bthLab.xyz are the same"
);
require(
ethLabXyz.compareNames(bthLabXyz) > 0,
"ethLab.xyz comes after bethLab.xyz"
);
require(bthLabXyz.compareNames(xyz) > 0, "bthLab.xyz comes after xyz");
require(
verylong1_eth.compareNames(verylong2_eth) > 0,
"longa.vlong.eth comes after long.vlong.eth"
);
}
function testSerialNumberGt() public pure {
require(RRUtils.serialNumberGte(1, 0), "1 >= 0");
require(!RRUtils.serialNumberGte(0, 1), "!(0 <= 1)");
require(RRUtils.serialNumberGte(0, 0xFFFFFFFF), "0 >= 0xFFFFFFFF");
require(!RRUtils.serialNumberGte(0xFFFFFFFF, 0), "!(0 <= 0xFFFFFFFF)");
require(
RRUtils.serialNumberGte(0x11111111, 0xAAAAAAAA),
"0x11111111 >= 0xAAAAAAAA"
);
require(RRUtils.serialNumberGte(1, 1), "1 >= 1");
}
function testKeyTag() public view {
require(
hex"0101030803010001a80020a95566ba42e886bb804cda84e47ef56dbd7aec612615552cec906d2116d0ef207028c51554144dfeafe7c7cb8f005dd18234133ac0710a81182ce1fd14ad2283bc83435f9df2f6313251931a176df0da51e54f42e604860dfb359580250f559cc543c4ffd51cbe3de8cfd06719237f9fc47ee729da06835fa452e825e9a18ebc2ecbcf563474652c33cf56a9033bcdf5d973121797ec8089041b6e03a1b72d0a735b984e03687309332324f27c2dba85e9db15e83a0143382e974b0621c18e625ecec907577d9e7bade95241a81ebbe8a901d4d3276e40b114c0a2e6fc38d19c2e6aab02644b2813f575fc21601e0dee49cd9ee96a43103e524d62873d"
.computeKeytag() == 19036,
"Invalid keytag"
);
require(
hex"010003050440000003ba2fa05a75e173bede89eb71831ab14035f2408ad09df4d8dc8f8f72e8f13506feaddf7b04cb14958b82966e3420562302c4002bc4fd088432e160519bb14dae82443850c1423e06085710b5caf070d46b7ba7e481414f6a5fe225fdca984c959091645d0cf1c9a1a313d7e7fb7ba60b967b71a65f8cef2c3768e11b081c8fcf"
.computeKeytag() == 21693,
"Invalid keytag (2)"
);
require(
hex"0100030503010001bfa54c38d909fabb0f937d70d775ba0df4c0badb09707d995249406950407a621c794c68b186b15dbf8f9f9ea231e9f96414ccda4eceb50b17a9ac6c4bd4b95da04849e96ee791578b703bc9ae184fb1794bac792a0787f693a40f19f523ee6dbd3599dbaaa9a50437926ecf6438845d1d49448962524f2a1a7a36b3a0a1eca3"
.computeKeytag() == 33630
);
require(
hex"0101030803010001acffb409bcc939f831f7a1e5ec88f7a59255ec53040be432027390a4ce896d6f9086f3c5e177fbfe118163aaec7af1462c47945944c4e2c026be5e98bbcded25978272e1e3e079c5094d573f0e83c92f02b32d3513b1550b826929c80dd0f92cac966d17769fd5867b647c3f38029abdc48152eb8f207159ecc5d232c7c1537c79f4b7ac28ff11682f21681bf6d6aba555032bf6f9f036beb2aaa5b3778d6eebfba6bf9ea191be4ab0caea759e2f773a1f9029c73ecb8d5735b9321db085f1b8e2d8038fe2941992548cee0d67dd4547e11dd63af9c9fc1c5466fb684cf009d7197c2cf79e792ab501e6a8a1ca519af2cb9b5f6367e94c0d47502451357be1b5"
.computeKeytag() == 20326,
"Invalid keytag (3)"
);
}
}
// ---- test/TestBytesUtils.sol ----
pragma solidity ^0.8.4;
import "../../contracts/dnssec-oracle/RRUtils.sol";
import "../../contracts/utils/BytesUtils.sol";
contract TestBytesUtils {
using BytesUtils for *;
function testKeccak() public pure {
require(
"".keccak(0, 0) ==
bytes32(
0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
),
"Incorrect hash of empty string"
);
require(
"foo".keccak(0, 3) ==
bytes32(
0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d
),
"Incorrect hash of 'foo'"
);
require(
"foo".keccak(0, 0) ==
bytes32(
0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
),
"Incorrect hash of empty string"
);
}
function testEquals() public pure {
require("hello".equals("hello") == true, "String equality");
require("hello".equals("goodbye") == false, "String inequality");
require(
"hello".equals(1, "ello") == true,
"Substring to string equality"
);
require(
"hello".equals(1, "jello", 1, 4) == true,
"Substring to substring equality"
);
require(
"zhello".equals(1, "abchello", 3) == true,
"Compare different value with multiple length"
);
require(
"0x0102030000".equals(0, "0x010203") == false,
"Compare with offset and trailing bytes"
);
}
function testComparePartial() public pure {
require(
"xax".compare(1, 1, "xxbxx", 2, 1) < 0 == true,
"Compare same length"
);
require(
"xax".compare(1, 1, "xxabxx", 2, 2) < 0 == true,
"Compare different length"
);
require(
"xax".compare(1, 1, "xxaxx", 2, 1) == 0 == true,
"Compare same with different offset"
);
require(
"01234567890123450123456789012345ab".compare(
0,
33,
"01234567890123450123456789012345aa",
0,
33
) ==
0 ==
true,
"Compare different long strings same length smaller partial length which must be equal"
);
require(
"01234567890123450123456789012345ab".compare(
0,
33,
"01234567890123450123456789012345aa",
0,
34
) <
0 ==
true,
"Compare long strings same length different partial length"
);
require(
"0123456789012345012345678901234a".compare(
0,
32,
"0123456789012345012345678901234b",
0,
32
) <
0 ==
true,
"Compare strings exactly 32 characters long"
);
}
function testCompare() public pure {
require("a".compare("a") == 0 == true, "Compare equal");
require(
"a".compare("b") < 0 == true,
"Compare different value with same length"
);
require(
"b".compare("a") > 0 == true,
"Compare different value with same length"
);
require(
"aa".compare("ab") < 0 == true,
"Compare different value with multiple length"
);
require(
"a".compare("aa") < 0 == true,
"Compare different value with different length"
);
require(
"aa".compare("a") > 0 == true,
"Compare different value with different length"
);
bytes memory longChar = "1234567890123456789012345678901234";
require(
longChar.compare(longChar) == 0 == true,
"Compares more than 32 bytes char"
);
bytes memory otherLongChar = "2234567890123456789012345678901234";
require(
longChar.compare(otherLongChar) < 0 == true,
"Compare long char with difference at start"
);
require(
abi.encodePacked(type(int256).min).compare(
abi.encodePacked(type(int256).max)
) > 0,
"Compare maximum difference"
);
}
function testSubstring() public pure {
require(
keccak256(bytes("hello".substring(0, 0))) == keccak256(bytes("")),
"Copy 0 bytes"
);
require(
keccak256(bytes("hello".substring(0, 4))) ==
keccak256(bytes("hell")),
"Copy substring"
);
require(
keccak256(bytes("hello".substring(1, 4))) ==
keccak256(bytes("ello")),
"Copy substring"
);
require(
keccak256(bytes("hello".substring(0, 5))) ==
keccak256(bytes("hello")),
"Copy whole string"
);
}
function testReadUint8() public pure {
require(uint("a".readUint8(0)) == 0x61, "a == 0x61");
require(uint("ba".readUint8(1)) == 0x61, "a == 0x61");
}
function testReadUint16() public pure {
require(uint("abc".readUint16(1)) == 0x6263, "Read uint 16");
}
function testReadUint32() public pure {
require(uint("abcde".readUint32(1)) == 0x62636465, "Read uint 32");
}
function testReadBytes20() public pure {
require(
bytes32("abcdefghijklmnopqrstuv".readBytes20(1)) ==
bytes32(
0x62636465666768696a6b6c6d6e6f707172737475000000000000000000000000
),
"readBytes20"
);
}
function testReadBytes32() public pure {
require(
"0123456789abcdef0123456789abcdef".readBytes32(0) ==
bytes32(
0x3031323334353637383961626364656630313233343536373839616263646566
),
"readBytes32"
);
}
function testBase32HexDecodeWord() public pure {
require(
"C4".base32HexDecodeWord(0, 2) == bytes32(bytes1("a")),
"Decode 'a'"
);
require(
"C5GG".base32HexDecodeWord(0, 4) == bytes32(bytes2("aa")),
"Decode 'aa'"
);
require(
"C5GM2".base32HexDecodeWord(0, 5) == bytes32(bytes3("aaa")),
"Decode 'aaa'"
);
require(
"C5GM2O8".base32HexDecodeWord(0, 7) == bytes32(bytes4("aaaa")),
"Decode 'aaaa'"
);
require(
"C5GM2OB1".base32HexDecodeWord(0, 8) == bytes32(bytes5("aaaaa")),
"Decode 'aaaaa'"
);
require(
"c5gm2Ob1".base32HexDecodeWord(0, 8) == bytes32(bytes5("aaaaa")),
"Decode 'aaaaa' lowercase"
);
require(
"C5H66P35CPJMGQBADDM6QRJFE1ON4SRKELR7EU3PF8".base32HexDecodeWord(
0,
42
) == bytes32(bytes26("abcdefghijklmnopqrstuvwxyz")),
"Decode alphabet"
);
require(
"c5h66p35cpjmgqbaddm6qrjfe1on4srkelr7eu3pf8".base32HexDecodeWord(
0,
42
) == bytes32(bytes26("abcdefghijklmnopqrstuvwxyz")),
"Decode alphabet lowercase"
);
require(
"C5GM2OB1C5GM2OB1C5GM2OB1C5GM2OB1C5GM2OB1C5GM2OB1C5GG"
.base32HexDecodeWord(0, 52) ==
bytes32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
"Decode 32*'a'"
);
require(
" bst4hlje7r0o8c8p4o8q582lm0ejmiqt\x07matoken\x03xyz\x00"
.base32HexDecodeWord(1, 32) ==
bytes32(hex"5f3a48d66e3ec18431192611a2a055b01d3b4b5d"),
"Decode real bytes32hex"
);
}
}
// ---- test/mocks/MockReverseClaimerImplementer.sol ----
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.17 <0.9.0;
import {ENS} from "../../../contracts/registry/ENS.sol";
import {ReverseClaimer} from "../../../contracts/reverseRegistrar/ReverseClaimer.sol";
contract MockReverseClaimerImplementer is ReverseClaimer {
constructor(ENS ens, address claimant) ReverseClaimer(ens, claimant) {}
}
// ---- test/mocks/MockOffchainResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "../../../contracts/resolvers/profiles/IExtendedResolver.sol";
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);
contract MockOffchainResolver is IExtendedResolver, ERC165 {
function supportsInterface(
bytes4 interfaceId
) public view virtual override returns (bool) {
return
interfaceId == type(IExtendedResolver).interfaceId ||
super.supportsInterface(interfaceId);
}
function resolve(
bytes calldata /* name */,
bytes calldata data
) external view returns (bytes memory) {
string[] memory urls = new string[](1);
urls[0] = "https://example.com/";
revert OffchainLookup(
address(this),
urls,
data,
MockOffchainResolver.resolveCallback.selector,
data
);
}
function addr(bytes32) external pure returns (bytes memory) {
return abi.encode("onchain");
}
function resolveCallback(
bytes calldata response,
bytes calldata extraData
) external view returns (bytes memory) {
(, bytes memory callData, ) = abi.decode(
extraData,
(bytes, bytes, bytes4)
);
if (bytes4(callData) == bytes4(keccak256("addr(bytes32)"))) {
(bytes memory result, , ) = abi.decode(
response,
(bytes, uint64, bytes)
);
return result;
}
return abi.encode(address(this));
}
}
// ---- test/mocks/DummyOffchainResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "../../../contracts/resolvers/profiles/ITextResolver.sol";
import "../../../contracts/resolvers/profiles/IExtendedResolver.sol";
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);
contract DummyOffchainResolver is IExtendedResolver, ERC165 {
function supportsInterface(
bytes4 interfaceId
) public view virtual override returns (bool) {
return
interfaceId == type(IExtendedResolver).interfaceId ||
super.supportsInterface(interfaceId);
}
function resolve(
bytes calldata /* name */,
bytes calldata data
) external view returns (bytes memory) {
string[] memory urls = new string[](1);
urls[0] = "https://example.com/";
if (bytes4(data) == bytes4(0x12345678)) {
return abi.encode("foo");
}
revert OffchainLookup(
address(this),
urls,
data,
DummyOffchainResolver.resolveCallback.selector,
data
);
}
function addr(bytes32) external pure returns (address) {
return 0x69420f05A11f617B4B74fFe2E04B2D300dFA556F;
}
function resolveCallback(
bytes calldata response,
bytes calldata extraData
) external view returns (bytes memory) {
require(
keccak256(response) == keccak256(extraData),
"Response data error"
);
if (bytes4(extraData) == bytes4(keccak256("name(bytes32)"))) {
return abi.encode("offchain.test.eth");
}
return abi.encode(address(this));
}
}
// ---- test/mocks/LegacyResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract LegacyResolver {
function addr(bytes32 /* node */) public view returns (address) {
return address(this);
}
}
// ---- test/mocks/MockERC20.sol ----
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.17 <0.9.0;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor(
string memory name,
string memory symbol,
address[] memory addresses
) ERC20(name, symbol) {
_mint(msg.sender, 100 * 10 ** uint256(decimals()));
for (uint256 i = 0; i < addresses.length; i++) {
_mint(addresses[i], 100 * 10 ** uint256(decimals()));
}
}
}
// ---- test/mocks/StringUtilsTest.sol ----
// SPDX-License-Identifier: MIT
import "../../../contracts/utils/StringUtils.sol";
library StringUtilsTest {
function testEscape(
string calldata testStr
) public pure returns (string memory) {
return StringUtils.escape(testStr);
}
}
// ---- wrapper/NameWrapper.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import {ERC1155Fuse, IERC165, IERC1155MetadataURI} from "./ERC1155Fuse.sol";
import {Controllable} from "./Controllable.sol";
import {INameWrapper, CANNOT_UNWRAP, CANNOT_BURN_FUSES, CANNOT_TRANSFER, CANNOT_SET_RESOLVER, CANNOT_SET_TTL, CANNOT_CREATE_SUBDOMAIN, CANNOT_APPROVE, PARENT_CANNOT_CONTROL, CAN_DO_EVERYTHING, IS_DOT_ETH, CAN_EXTEND_EXPIRY, PARENT_CONTROLLED_FUSES, USER_SETTABLE_FUSES} from "./INameWrapper.sol";
import {INameWrapperUpgrade} from "./INameWrapperUpgrade.sol";
import {IMetadataService} from "./IMetadataService.sol";
import {ENS} from "../registry/ENS.sol";
import {IReverseRegistrar} from "../reverseRegistrar/IReverseRegistrar.sol";
import {ReverseClaimer} from "../reverseRegistrar/ReverseClaimer.sol";
import {IBaseRegistrar} from "../ethregistrar/IBaseRegistrar.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {BytesUtils_LEGACY} from "../utils/BytesUtils_LEGACY.sol";
import {ERC20Recoverable} from "../utils/ERC20Recoverable.sol";
error Unauthorised(bytes32 node, address addr);
error IncompatibleParent();
error IncorrectTokenType();
error LabelMismatch(bytes32 labelHash, bytes32 expectedLabelhash);
error LabelTooShort();
error LabelTooLong(string label);
error IncorrectTargetOwner(address owner);
error CannotUpgrade();
error OperationProhibited(bytes32 node);
error NameIsNotWrapped();
error NameIsStillExpired();
contract NameWrapper is
Ownable,
ERC1155Fuse,
INameWrapper,
Controllable,
IERC721Receiver,
ERC20Recoverable,
ReverseClaimer
{
using BytesUtils_LEGACY for bytes;
ENS public immutable ens;
IBaseRegistrar public immutable registrar;
IMetadataService public metadataService;
mapping(bytes32 => bytes) public names;
string public constant name = "NameWrapper";
uint64 private constant GRACE_PERIOD = 90 days;
bytes32 private constant ETH_NODE =
0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae;
bytes32 private constant ETH_LABELHASH =
0x4f5b812789fc606be1b3b16908db13fc7a9adf7ca72641f84d75b47069d3d7f0;
bytes32 private constant ROOT_NODE =
0x0000000000000000000000000000000000000000000000000000000000000000;
INameWrapperUpgrade public upgradeContract;
uint64 private constant MAX_EXPIRY = type(uint64).max;
constructor(
ENS _ens,
IBaseRegistrar _registrar,
IMetadataService _metadataService
) ReverseClaimer(_ens, msg.sender) {
ens = _ens;
registrar = _registrar;
metadataService = _metadataService;
/* Burn PARENT_CANNOT_CONTROL and CANNOT_UNWRAP fuses for ROOT_NODE and ETH_NODE and set expiry to max */
_setData(
uint256(ETH_NODE),
address(0),
uint32(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP),
MAX_EXPIRY
);
_setData(
uint256(ROOT_NODE),
address(0),
uint32(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP),
MAX_EXPIRY
);
names[ROOT_NODE] = "\x00";
names[ETH_NODE] = "\x03eth\x00";
}
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC1155Fuse, INameWrapper) returns (bool) {
return
interfaceId == type(INameWrapper).interfaceId ||
interfaceId == type(IERC721Receiver).interfaceId ||
super.supportsInterface(interfaceId);
}
/* ERC1155 Fuse */
/// @notice Gets the owner of a name
/// @param id Label as a string of the .eth domain to wrap
/// @return owner The owner of the name
function ownerOf(
uint256 id
) public view override(ERC1155Fuse, INameWrapper) returns (address owner) {
return super.ownerOf(id);
}
/// @notice Gets the owner of a name
/// @param id Namehash of the name
/// @return operator Approved operator of a name
function getApproved(
uint256 id
)
public
view
override(ERC1155Fuse, INameWrapper)
returns (address operator)
{
address owner = ownerOf(id);
if (owner == address(0)) {
return address(0);
}
return super.getApproved(id);
}
/// @notice Approves an address for a name
/// @param to address to approve
/// @param tokenId name to approve
function approve(
address to,
uint256 tokenId
) public override(ERC1155Fuse, INameWrapper) {
(, uint32 fuses, ) = getData(tokenId);
if (fuses & CANNOT_APPROVE == CANNOT_APPROVE) {
revert OperationProhibited(bytes32(tokenId));
}
super.approve(to, tokenId);
}
/// @notice Gets the data for a name
/// @param id Namehash of the name
/// @return owner Owner of the name
/// @return fuses Fuses of the name
/// @return expiry Expiry of the name
function getData(
uint256 id
)
public
view
override(ERC1155Fuse, INameWrapper)
returns (address owner, uint32 fuses, uint64 expiry)
{
(owner, fuses, expiry) = super.getData(id);
(owner, fuses) = _clearOwnerAndFuses(owner, fuses, expiry);
}
/* Metadata service */
/// @notice Set the metadata service. Only the owner can do this
/// @param _metadataService The new metadata service
function setMetadataService(
IMetadataService _metadataService
) public onlyOwner {
metadataService = _metadataService;
}
/// @notice Get the metadata uri
/// @param tokenId The id of the token
/// @return string uri of the metadata service
function uri(
uint256 tokenId
)
public
view
override(INameWrapper, IERC1155MetadataURI)
returns (string memory)
{
return metadataService.uri(tokenId);
}
/// @notice Set the address of the upgradeContract of the contract. only admin can do this
/// @dev The default value of upgradeContract is the 0 address. Use the 0 address at any time
/// to make the contract not upgradable.
/// @param _upgradeAddress address of an upgraded contract
function setUpgradeContract(
INameWrapperUpgrade _upgradeAddress
) public onlyOwner {
if (address(upgradeContract) != address(0)) {
registrar.setApprovalForAll(address(upgradeContract), false);
ens.setApprovalForAll(address(upgradeContract), false);
}
upgradeContract = _upgradeAddress;
if (address(upgradeContract) != address(0)) {
registrar.setApprovalForAll(address(upgradeContract), true);
ens.setApprovalForAll(address(upgradeContract), true);
}
}
/// @notice Checks if msg.sender is the owner or operator of the owner of a name
/// @param node namehash of the name to check
modifier onlyTokenOwner(bytes32 node) {
if (!canModifyName(node, msg.sender)) {
revert Unauthorised(node, msg.sender);
}
_;
}
/// @notice Checks if owner or operator of the owner
/// @param node namehash of the name to check
/// @param addr which address to check permissions for
/// @return whether or not is owner or operator
function canModifyName(
bytes32 node,
address addr
) public view returns (bool) {
(address owner, uint32 fuses, uint64 expiry) = getData(uint256(node));
return
(owner == addr || isApprovedForAll(owner, addr)) &&
!_isETH2LDInGracePeriod(fuses, expiry);
}
/// @notice Checks if owner/operator or approved by owner
/// @param node namehash of the name to check
/// @param addr which address to check permissions for
/// @return whether or not is owner/operator or approved
function canExtendSubnames(
bytes32 node,
address addr
) public view returns (bool) {
(address owner, uint32 fuses, uint64 expiry) = getData(uint256(node));
return
(owner == addr ||
isApprovedForAll(owner, addr) ||
getApproved(uint256(node)) == addr) &&
!_isETH2LDInGracePeriod(fuses, expiry);
}
/// @notice Wraps a .eth domain, creating a new token and sending the original ERC721 token to this contract
/// @dev Can be called by the owner of the name on the .eth registrar or an authorised caller on the registrar
/// @param label Label as a string of the .eth domain to wrap
/// @param wrappedOwner Owner of the name in this contract
/// @param ownerControlledFuses Initial owner-controlled fuses to set
/// @param resolver Resolver contract address
function wrapETH2LD(
string calldata label,
address wrappedOwner,
uint16 ownerControlledFuses,
address resolver
) public returns (uint64 expiry) {
uint256 tokenId = uint256(keccak256(bytes(label)));
address registrant = registrar.ownerOf(tokenId);
if (
registrant != msg.sender &&
!registrar.isApprovedForAll(registrant, msg.sender)
) {
revert Unauthorised(
_makeNode(ETH_NODE, bytes32(tokenId)),
msg.sender
);
}
// transfer the token from the user to this contract
registrar.transferFrom(registrant, address(this), tokenId);
// transfer the ens record back to the new owner (this contract)
registrar.reclaim(tokenId, address(this));
expiry = uint64(registrar.nameExpires(tokenId)) + GRACE_PERIOD;
_wrapETH2LD(
label,
wrappedOwner,
ownerControlledFuses,
expiry,
resolver
);
}
/// @dev Registers a new .eth second-level domain and wraps it.
/// Only callable by authorised controllers.
/// @param label The label to register (Eg, 'foo' for 'foo.eth').
/// @param wrappedOwner The owner of the wrapped name.
/// @param duration The duration, in seconds, to register the name for.
/// @param resolver The resolver address to set on the ENS registry (optional).
/// @param ownerControlledFuses Initial owner-controlled fuses to set
/// @return registrarExpiry The expiry date of the new name on the .eth registrar, in seconds since the Unix epoch.
function registerAndWrapETH2LD(
string calldata label,
address wrappedOwner,
uint256 duration,
address resolver,
uint16 ownerControlledFuses
) external onlyController returns (uint256 registrarExpiry) {
uint256 tokenId = uint256(keccak256(bytes(label)));
registrarExpiry = registrar.register(tokenId, address(this), duration);
_wrapETH2LD(
label,
wrappedOwner,
ownerControlledFuses,
uint64(registrarExpiry) + GRACE_PERIOD,
resolver
);
}
/// @notice Renews a .eth second-level domain.
/// @dev Only callable by authorised controllers.
/// @param tokenId The hash of the label to register (eg, `keccak256('foo')`, for 'foo.eth').
/// @param duration The number of seconds to renew the name for.
/// @return expires The expiry date of the name on the .eth registrar, in seconds since the Unix epoch.
function renew(
uint256 tokenId,
uint256 duration
) external onlyController returns (uint256 expires) {
bytes32 node = _makeNode(ETH_NODE, bytes32(tokenId));
uint256 registrarExpiry = registrar.renew(tokenId, duration);
// Do not set anything in wrapper if name is not wrapped
try registrar.ownerOf(tokenId) returns (address registrarOwner) {
if (
registrarOwner != address(this) ||
ens.owner(node) != address(this)
) {
return registrarExpiry;
}
} catch {
return registrarExpiry;
}
// Set expiry in Wrapper
uint64 expiry = uint64(registrarExpiry) + GRACE_PERIOD;
// Use super to allow names expired on the wrapper, but not expired on the registrar to renew()
(address owner, uint32 fuses, ) = super.getData(uint256(node));
_setData(node, owner, fuses, expiry);
return registrarExpiry;
}
/// @notice Wraps a non .eth domain, of any kind. Could be a DNSSEC name vitalik.xyz or a subdomain
/// @dev Can be called by the owner in the registry or an authorised caller in the registry
/// @param name The name to wrap, in DNS format
/// @param wrappedOwner Owner of the name in this contract
/// @param resolver Resolver contract
function wrap(
bytes calldata name,
address wrappedOwner,
address resolver
) public {
(bytes32 labelhash, uint256 offset) = name.readLabel(0);
bytes32 parentNode = name.namehash(offset);
bytes32 node = _makeNode(parentNode, labelhash);
names[node] = name;
if (parentNode == ETH_NODE) {
revert IncompatibleParent();
}
address owner = ens.owner(node);
if (owner != msg.sender && !ens.isApprovedForAll(owner, msg.sender)) {
revert Unauthorised(node, msg.sender);
}
if (resolver != address(0)) {
ens.setResolver(node, resolver);
}
ens.setOwner(node, address(this));
_wrap(node, name, wrappedOwner, 0, 0);
}
/// @notice Unwraps a .eth domain. e.g. vitalik.eth
/// @dev Can be called by the owner in the wrapper or an authorised caller in the wrapper
/// @param labelhash Labelhash of the .eth domain
/// @param registrant Sets the owner in the .eth registrar to this address
/// @param controller Sets the owner in the registry to this address
function unwrapETH2LD(
bytes32 labelhash,
address registrant,
address controller
) public onlyTokenOwner(_makeNode(ETH_NODE, labelhash)) {
if (registrant == address(this)) {
revert IncorrectTargetOwner(registrant);
}
_unwrap(_makeNode(ETH_NODE, labelhash), controller);
registrar.safeTransferFrom(
address(this),
registrant,
uint256(labelhash)
);
}
/// @notice Unwraps a non .eth domain, of any kind. Could be a DNSSEC name vitalik.xyz or a subdomain
/// @dev Can be called by the owner in the wrapper or an authorised caller in the wrapper
/// @param parentNode Parent namehash of the name e.g. vitalik.xyz would be namehash('xyz')
/// @param labelhash Labelhash of the name, e.g. vitalik.xyz would be keccak256('vitalik')
/// @param controller Sets the owner in the registry to this address
function unwrap(
bytes32 parentNode,
bytes32 labelhash,
address controller
) public onlyTokenOwner(_makeNode(parentNode, labelhash)) {
if (parentNode == ETH_NODE) {
revert IncompatibleParent();
}
if (controller == address(0x0) || controller == address(this)) {
revert IncorrectTargetOwner(controller);
}
_unwrap(_makeNode(parentNode, labelhash), controller);
}
/// @notice Sets fuses of a name
/// @param node Namehash of the name
/// @param ownerControlledFuses Owner-controlled fuses to burn
/// @return Old fuses
function setFuses(
bytes32 node,
uint16 ownerControlledFuses
)
public
onlyTokenOwner(node)
operationAllowed(node, CANNOT_BURN_FUSES)
returns (uint32)
{
// owner protected by onlyTokenOwner
(address owner, uint32 oldFuses, uint64 expiry) = getData(
uint256(node)
);
_setFuses(node, owner, ownerControlledFuses | oldFuses, expiry, expiry);
return oldFuses;
}
/// @notice Extends expiry for a name
/// @param parentNode Parent namehash of the name e.g. vitalik.xyz would be namehash('xyz')
/// @param labelhash Labelhash of the name, e.g. vitalik.xyz would be keccak256('vitalik')
/// @param expiry When the name will expire in seconds since the Unix epoch
/// @return New expiry
function extendExpiry(
bytes32 parentNode,
bytes32 labelhash,
uint64 expiry
) public returns (uint64) {
bytes32 node = _makeNode(parentNode, labelhash);
if (!_isWrapped(node)) {
revert NameIsNotWrapped();
}
// this flag is used later, when checking fuses
bool canExtendSubname = canExtendSubnames(parentNode, msg.sender);
// only allow the owner of the name or owner of the parent name
if (!canExtendSubname && !canModifyName(node, msg.sender)) {
revert Unauthorised(node, msg.sender);
}
(address owner, uint32 fuses, uint64 oldExpiry) = getData(
uint256(node)
);
// Either CAN_EXTEND_EXPIRY must be set, or the caller must have permission to modify the parent name
if (!canExtendSubname && fuses & CAN_EXTEND_EXPIRY == 0) {
revert OperationProhibited(node);
}
// Max expiry is set to the expiry of the parent
(, , uint64 maxExpiry) = getData(uint256(parentNode));
expiry = _normaliseExpiry(expiry, oldExpiry, maxExpiry);
_setData(node, owner, fuses, expiry);
emit ExpiryExtended(node, expiry);
return expiry;
}
/// @notice Upgrades a domain of any kind. Could be a .eth name vitalik.eth, a DNSSEC name vitalik.xyz, or a subdomain
/// @dev Can be called by the owner or an authorised caller
/// @param name The name to upgrade, in DNS format
/// @param extraData Extra data to pass to the upgrade contract
function upgrade(bytes calldata name, bytes calldata extraData) public {
bytes32 node = name.namehash(0);
if (address(upgradeContract) == address(0)) {
revert CannotUpgrade();
}
if (!canModifyName(node, msg.sender)) {
revert Unauthorised(node, msg.sender);
}
(address currentOwner, uint32 fuses, uint64 expiry) = getData(
uint256(node)
);
address approved = getApproved(uint256(node));
_burn(uint256(node));
upgradeContract.wrapFromUpgrade(
name,
currentOwner,
fuses,
expiry,
approved,
extraData
);
}
/// /* @notice Sets fuses of a name that you own the parent of
/// @param parentNode Parent namehash of the name e.g. vitalik.xyz would be namehash('xyz')
/// @param labelhash Labelhash of the name, e.g. vitalik.xyz would be keccak256('vitalik')
/// @param fuses Fuses to burn
/// @param expiry When the name will expire in seconds since the Unix epoch
function setChildFuses(
bytes32 parentNode,
bytes32 labelhash,
uint32 fuses,
uint64 expiry
) public {
bytes32 node = _makeNode(parentNode, labelhash);
_checkFusesAreSettable(node, fuses);
(address owner, uint32 oldFuses, uint64 oldExpiry) = getData(
uint256(node)
);
if (owner == address(0) || ens.owner(node) != address(this)) {
revert NameIsNotWrapped();
}
// max expiry is set to the expiry of the parent
(, uint32 parentFuses, uint64 maxExpiry) = getData(uint256(parentNode));
if (parentNode == ROOT_NODE) {
if (!canModifyName(node, msg.sender)) {
revert Unauthorised(node, msg.sender);
}
} else {
if (!canModifyName(parentNode, msg.sender)) {
revert Unauthorised(parentNode, msg.sender);
}
}
_checkParentFuses(node, fuses, parentFuses);
expiry = _normaliseExpiry(expiry, oldExpiry, maxExpiry);
// if PARENT_CANNOT_CONTROL has been burned and fuses have changed
if (
oldFuses & PARENT_CANNOT_CONTROL != 0 &&
oldFuses | fuses != oldFuses
) {
revert OperationProhibited(node);
}
fuses |= oldFuses;
_setFuses(node, owner, fuses, oldExpiry, expiry);
}
/// @notice Sets the subdomain owner in the registry and then wraps the subdomain
/// @param parentNode Parent namehash of the subdomain
/// @param label Label of the subdomain as a string
/// @param owner New owner in the wrapper
/// @param fuses Initial fuses for the wrapped subdomain
/// @param expiry When the name will expire in seconds since the Unix epoch
/// @return node Namehash of the subdomain
function setSubnodeOwner(
bytes32 parentNode,
string calldata label,
address owner,
uint32 fuses,
uint64 expiry
) public onlyTokenOwner(parentNode) returns (bytes32 node) {
bytes32 labelhash = keccak256(bytes(label));
node = _makeNode(parentNode, labelhash);
_checkCanCallSetSubnodeOwner(parentNode, node);
_checkFusesAreSettable(node, fuses);
bytes memory name = _saveLabel(parentNode, node, label);
expiry = _checkParentFusesAndExpiry(parentNode, node, fuses, expiry);
if (!_isWrapped(node)) {
ens.setSubnodeOwner(parentNode, labelhash, address(this));
_wrap(node, name, owner, fuses, expiry);
} else {
_updateName(parentNode, node, label, owner, fuses, expiry);
}
}
/// @notice Sets the subdomain owner in the registry with records and then wraps the subdomain
/// @param parentNode parent namehash of the subdomain
/// @param label label of the subdomain as a string
/// @param owner new owner in the wrapper
/// @param resolver resolver contract in the registry
/// @param ttl ttl in the registry
/// @param fuses initial fuses for the wrapped subdomain
/// @param expiry When the name will expire in seconds since the Unix epoch
/// @return node Namehash of the subdomain
function setSubnodeRecord(
bytes32 parentNode,
string memory label,
address owner,
address resolver,
uint64 ttl,
uint32 fuses,
uint64 expiry
) public onlyTokenOwner(parentNode) returns (bytes32 node) {
bytes32 labelhash = keccak256(bytes(label));
node = _makeNode(parentNode, labelhash);
_checkCanCallSetSubnodeOwner(parentNode, node);
_checkFusesAreSettable(node, fuses);
_saveLabel(parentNode, node, label);
expiry = _checkParentFusesAndExpiry(parentNode, node, fuses, expiry);
if (!_isWrapped(node)) {
ens.setSubnodeRecord(
parentNode,
labelhash,
address(this),
resolver,
ttl
);
_storeNameAndWrap(parentNode, node, label, owner, fuses, expiry);
} else {
ens.setSubnodeRecord(
parentNode,
labelhash,
address(this),
resolver,
ttl
);
_updateName(parentNode, node, label, owner, fuses, expiry);
}
}
/// @notice Sets records for the name in the ENS Registry
/// @param node Namehash of the name to set a record for
/// @param owner New owner in the registry
/// @param resolver Resolver contract
/// @param ttl Time to live in the registry
function setRecord(
bytes32 node,
address owner,
address resolver,
uint64 ttl
)
public
onlyTokenOwner(node)
operationAllowed(
node,
CANNOT_TRANSFER | CANNOT_SET_RESOLVER | CANNOT_SET_TTL
)
{
ens.setRecord(node, address(this), resolver, ttl);
if (owner == address(0)) {
(, uint32 fuses, ) = getData(uint256(node));
if (fuses & IS_DOT_ETH == IS_DOT_ETH) {
revert IncorrectTargetOwner(owner);
}
_unwrap(node, address(0));
} else {
address oldOwner = ownerOf(uint256(node));
_transfer(oldOwner, owner, uint256(node), 1, "");
}
}
/// @notice Sets resolver contract in the registry
/// @param node namehash of the name
/// @param resolver the resolver contract
function setResolver(
bytes32 node,
address resolver
) public onlyTokenOwner(node) operationAllowed(node, CANNOT_SET_RESOLVER) {
ens.setResolver(node, resolver);
}
/// @notice Sets TTL in the registry
/// @param node Namehash of the name
/// @param ttl TTL in the registry
function setTTL(
bytes32 node,
uint64 ttl
) public onlyTokenOwner(node) operationAllowed(node, CANNOT_SET_TTL) {
ens.setTTL(node, ttl);
}
/// @dev Allows an operation only if none of the specified fuses are burned.
/// @param node The namehash of the name to check fuses on.
/// @param fuseMask A bitmask of fuses that must not be burned.
modifier operationAllowed(bytes32 node, uint32 fuseMask) {
(, uint32 fuses, ) = getData(uint256(node));
if (fuses & fuseMask != 0) {
revert OperationProhibited(node);
}
_;
}
/// @notice Check whether a name can call setSubnodeOwner/setSubnodeRecord
/// @dev Checks both CANNOT_CREATE_SUBDOMAIN and PARENT_CANNOT_CONTROL and whether not they have been burnt
/// and checks whether the owner of the subdomain is 0x0 for creating or already exists for
/// replacing a subdomain. If either conditions are true, then it is possible to call
/// setSubnodeOwner
/// @param parentNode Namehash of the parent name to check
/// @param subnode Namehash of the subname to check
function _checkCanCallSetSubnodeOwner(
bytes32 parentNode,
bytes32 subnode
) internal view {
(
address subnodeOwner,
uint32 subnodeFuses,
uint64 subnodeExpiry
) = getData(uint256(subnode));
// check if the registry owner is 0 and expired
// check if the wrapper owner is 0 and expired
// If either, then check parent fuses for CANNOT_CREATE_SUBDOMAIN
bool expired = subnodeExpiry < block.timestamp;
if (
expired &&
// protects a name that has been unwrapped with PCC and doesn't allow the parent to take control by recreating it if unexpired
(subnodeOwner == address(0) ||
// protects a name that has been burnt and doesn't allow the parent to take control by recreating it if unexpired
ens.owner(subnode) == address(0))
) {
(, uint32 parentFuses, ) = getData(uint256(parentNode));
if (parentFuses & CANNOT_CREATE_SUBDOMAIN != 0) {
revert OperationProhibited(subnode);
}
} else {
if (subnodeFuses & PARENT_CANNOT_CONTROL != 0) {
revert OperationProhibited(subnode);
}
}
}
/// @notice Checks all Fuses in the mask are burned for the node
/// @param node Namehash of the name
/// @param fuseMask The fuses you want to check
/// @return Boolean of whether or not all the selected fuses are burned
function allFusesBurned(
bytes32 node,
uint32 fuseMask
) public view returns (bool) {
(, uint32 fuses, ) = getData(uint256(node));
return fuses & fuseMask == fuseMask;
}
/// @notice Checks if a name is wrapped
/// @param node Namehash of the name
/// @return Boolean of whether or not the name is wrapped
function isWrapped(bytes32 node) public view returns (bool) {
bytes memory name = names[node];
if (name.length == 0) {
return false;
}
(bytes32 labelhash, uint256 offset) = name.readLabel(0);
bytes32 parentNode = name.namehash(offset);
return isWrapped(parentNode, labelhash);
}
/// @notice Checks if a name is wrapped in a more gas efficient way
/// @param parentNode Namehash of the name
/// @param labelhash Namehash of the name
/// @return Boolean of whether or not the name is wrapped
function isWrapped(
bytes32 parentNode,
bytes32 labelhash
) public view returns (bool) {
bytes32 node = _makeNode(parentNode, labelhash);
bool wrapped = _isWrapped(node);
if (parentNode != ETH_NODE) {
return wrapped;
}
try registrar.ownerOf(uint256(labelhash)) returns (address owner) {
return owner == address(this);
} catch {
return false;
}
}
function onERC721Received(
address to,
address,
uint256 tokenId,
bytes calldata data
) public returns (bytes4) {
//check if it's the eth registrar ERC721
if (msg.sender != address(registrar)) {
revert IncorrectTokenType();
}
(
string memory label,
address owner,
uint16 ownerControlledFuses,
address resolver
) = abi.decode(data, (string, address, uint16, address));
bytes32 labelhash = bytes32(tokenId);
bytes32 labelhashFromData = keccak256(bytes(label));
if (labelhashFromData != labelhash) {
revert LabelMismatch(labelhashFromData, labelhash);
}
// transfer the ens record back to the new owner (this contract)
registrar.reclaim(uint256(labelhash), address(this));
uint64 expiry = uint64(registrar.nameExpires(tokenId)) + GRACE_PERIOD;
_wrapETH2LD(label, owner, ownerControlledFuses, expiry, resolver);
return IERC721Receiver(to).onERC721Received.selector;
}
/***** Internal functions */
function _beforeTransfer(
uint256 id,
uint32 fuses,
uint64 expiry
) internal override {
// For this check, treat .eth 2LDs as expiring at the start of the grace period.
if (fuses & IS_DOT_ETH == IS_DOT_ETH) {
expiry -= GRACE_PERIOD;
}
if (expiry < block.timestamp) {
// Transferable if the name was not emancipated
if (fuses & PARENT_CANNOT_CONTROL != 0) {
revert("ERC1155: insufficient balance for transfer");
}
} else {
// Transferable if CANNOT_TRANSFER is unburned
if (fuses & CANNOT_TRANSFER != 0) {
revert OperationProhibited(bytes32(id));
}
}
// delete token approval if CANNOT_APPROVE has not been burnt
if (fuses & CANNOT_APPROVE == 0) {
delete _tokenApprovals[id];
}
}
function _clearOwnerAndFuses(
address owner,
uint32 fuses,
uint64 expiry
) internal view override returns (address, uint32) {
if (expiry < block.timestamp) {
if (fuses & PARENT_CANNOT_CONTROL == PARENT_CANNOT_CONTROL) {
owner = address(0);
}
fuses = 0;
}
return (owner, fuses);
}
function _makeNode(
bytes32 node,
bytes32 labelhash
) private pure returns (bytes32) {
return keccak256(abi.encodePacked(node, labelhash));
}
function _addLabel(
string memory label,
bytes memory name
) internal pure returns (bytes memory ret) {
if (bytes(label).length < 1) {
revert LabelTooShort();
}
if (bytes(label).length > 255) {
revert LabelTooLong(label);
}
return abi.encodePacked(uint8(bytes(label).length), label, name);
}
function _mint(
bytes32 node,
address owner,
uint32 fuses,
uint64 expiry
) internal override {
_canFusesBeBurned(node, fuses);
(address oldOwner, , ) = super.getData(uint256(node));
if (oldOwner != address(0)) {
// burn and unwrap old token of old owner
_burn(uint256(node));
emit NameUnwrapped(node, address(0));
}
super._mint(node, owner, fuses, expiry);
}
function _wrap(
bytes32 node,
bytes memory name,
address wrappedOwner,
uint32 fuses,
uint64 expiry
) internal {
_mint(node, wrappedOwner, fuses, expiry);
emit NameWrapped(node, name, wrappedOwner, fuses, expiry);
}
function _storeNameAndWrap(
bytes32 parentNode,
bytes32 node,
string memory label,
address owner,
uint32 fuses,
uint64 expiry
) internal {
bytes memory name = _addLabel(label, names[parentNode]);
_wrap(node, name, owner, fuses, expiry);
}
function _saveLabel(
bytes32 parentNode,
bytes32 node,
string memory label
) internal returns (bytes memory) {
bytes memory name = _addLabel(label, names[parentNode]);
names[node] = name;
return name;
}
function _updateName(
bytes32 parentNode,
bytes32 node,
string memory label,
address owner,
uint32 fuses,
uint64 expiry
) internal {
(address oldOwner, uint32 oldFuses, uint64 oldExpiry) = getData(
uint256(node)
);
bytes memory name = _addLabel(label, names[parentNode]);
if (names[node].length == 0) {
names[node] = name;
}
_setFuses(node, oldOwner, oldFuses | fuses, oldExpiry, expiry);
if (owner == address(0)) {
_unwrap(node, address(0));
} else {
_transfer(oldOwner, owner, uint256(node), 1, "");
}
}
// wrapper function for stack limit
function _checkParentFusesAndExpiry(
bytes32 parentNode,
bytes32 node,
uint32 fuses,
uint64 expiry
) internal view returns (uint64) {
(, , uint64 oldExpiry) = getData(uint256(node));
(, uint32 parentFuses, uint64 maxExpiry) = getData(uint256(parentNode));
_checkParentFuses(node, fuses, parentFuses);
return _normaliseExpiry(expiry, oldExpiry, maxExpiry);
}
function _checkParentFuses(
bytes32 node,
uint32 fuses,
uint32 parentFuses
) internal pure {
bool isBurningParentControlledFuses = fuses & PARENT_CONTROLLED_FUSES !=
0;
bool parentHasNotBurnedCU = parentFuses & CANNOT_UNWRAP == 0;
if (isBurningParentControlledFuses && parentHasNotBurnedCU) {
revert OperationProhibited(node);
}
}
function _normaliseExpiry(
uint64 expiry,
uint64 oldExpiry,
uint64 maxExpiry
) private pure returns (uint64) {
// Expiry cannot be more than maximum allowed
// .eth names will check registrar, non .eth check parent
if (expiry > maxExpiry) {
expiry = maxExpiry;
}
// Expiry cannot be less than old expiry
if (expiry < oldExpiry) {
expiry = oldExpiry;
}
return expiry;
}
function _wrapETH2LD(
string memory label,
address wrappedOwner,
uint32 fuses,
uint64 expiry,
address resolver
) private {
bytes32 labelhash = keccak256(bytes(label));
bytes32 node = _makeNode(ETH_NODE, labelhash);
// hardcode dns-encoded eth string for gas savings
bytes memory name = _addLabel(label, "\x03eth\x00");
names[node] = name;
_wrap(
node,
name,
wrappedOwner,
fuses | PARENT_CANNOT_CONTROL | IS_DOT_ETH,
expiry
);
if (resolver != address(0)) {
ens.setResolver(node, resolver);
}
}
function _unwrap(bytes32 node, address owner) private {
if (allFusesBurned(node, CANNOT_UNWRAP)) {
revert OperationProhibited(node);
}
// Burn token and fuse data
_burn(uint256(node));
ens.setOwner(node, owner);
emit NameUnwrapped(node, owner);
}
function _setFuses(
bytes32 node,
address owner,
uint32 fuses,
uint64 oldExpiry,
uint64 expiry
) internal {
_setData(node, owner, fuses, expiry);
emit FusesSet(node, fuses);
if (expiry > oldExpiry) {
emit ExpiryExtended(node, expiry);
}
}
function _setData(
bytes32 node,
address owner,
uint32 fuses,
uint64 expiry
) internal {
_canFusesBeBurned(node, fuses);
super._setData(uint256(node), owner, fuses, expiry);
}
function _canFusesBeBurned(bytes32 node, uint32 fuses) internal pure {
// If a non-parent controlled fuse is being burned, check PCC and CU are burnt
if (
fuses & ~PARENT_CONTROLLED_FUSES != 0 &&
fuses & (PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) !=
(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP)
) {
revert OperationProhibited(node);
}
}
function _checkFusesAreSettable(bytes32 node, uint32 fuses) internal pure {
if (fuses | USER_SETTABLE_FUSES != USER_SETTABLE_FUSES) {
// Cannot directly burn other non-user settable fuses
revert OperationProhibited(node);
}
}
function _isWrapped(bytes32 node) internal view returns (bool) {
return
ownerOf(uint256(node)) != address(0) &&
ens.owner(node) == address(this);
}
function _isETH2LDInGracePeriod(
uint32 fuses,
uint64 expiry
) internal view returns (bool) {
return
fuses & IS_DOT_ETH == IS_DOT_ETH &&
expiry - GRACE_PERIOD < block.timestamp;
}
}
// ---- wrapper/INameWrapperUpgrade.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
interface INameWrapperUpgrade {
function wrapFromUpgrade(
bytes calldata name,
address wrappedOwner,
uint32 fuses,
uint64 expiry,
address approved,
bytes calldata extraData
) external;
}
// ---- wrapper/test/NameGriefer.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import {INameWrapper} from "../INameWrapper.sol";
import {ENS} from "../../registry/ENS.sol";
import {NameCoder} from "../../utils/NameCoder.sol";
import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
contract NameGriefer is IERC1155Receiver {
ENS public immutable ens;
INameWrapper public immutable wrapper;
constructor(INameWrapper _wrapper) {
wrapper = _wrapper;
ENS _ens = _wrapper.ens();
ens = _ens;
_ens.setApprovalForAll(address(_wrapper), true);
}
function destroy(bytes calldata name) public {
wrapper.wrap(name, address(this), address(0));
}
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256,
bytes calldata
) external override returns (bytes4) {
require(operator == address(this), "Operator must be us");
require(from == address(0), "Token must be new");
// Unwrap the name
bytes memory name = wrapper.names(bytes32(id));
(bytes32 labelhash, uint256 offset) = NameCoder.readLabel(name, 0);
bytes32 parentNode = NameCoder.namehash(name, offset);
wrapper.unwrap(parentNode, labelhash, address(this));
// Here we can do something with the name before it's permanently burned, like
// set the resolver or create subdomains.
return NameGriefer.onERC1155Received.selector;
}
function onERC1155BatchReceived(
address,
address,
uint256[] calldata,
uint256[] calldata,
bytes calldata
) external override returns (bytes4) {
return NameGriefer.onERC1155BatchReceived.selector;
}
function supportsInterface(
bytes4 interfaceID
) external view override returns (bool) {
return
interfaceID == 0x01ffc9a7 || // ERC-165 support (i.e. `bytes4(keccak256('supportsInterface(bytes4)'))`).
interfaceID == 0x4e2312e0; // ERC-1155 `ERC1155TokenReceiver` support (i.e. `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`).
}
}
// ---- wrapper/test/TestNameWrapperReentrancy.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "../INameWrapper.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
contract TestNameWrapperReentrancy is ERC165, IERC1155Receiver {
INameWrapper nameWrapper;
address owner;
bytes32 parentNode;
bytes32 labelHash;
uint256 tokenId;
constructor(
address _owner,
INameWrapper _nameWrapper,
bytes32 _parentNode,
bytes32 _labelHash
) {
owner = _owner;
nameWrapper = _nameWrapper;
parentNode = _parentNode;
labelHash = _labelHash;
}
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IERC1155Receiver).interfaceId ||
super.supportsInterface(interfaceId);
}
function onERC1155Received(
address,
address,
uint256 _id,
uint256,
bytes calldata
) public override returns (bytes4) {
tokenId = _id;
nameWrapper.unwrap(parentNode, labelHash, owner);
return this.onERC1155Received.selector;
}
function onERC1155BatchReceived(
address,
address,
uint256[] memory,
uint256[] memory,
bytes memory
) public virtual override returns (bytes4) {
return this.onERC1155BatchReceived.selector;
}
function claimToOwner() public {
nameWrapper.safeTransferFrom(address(this), owner, tokenId, 1, "");
}
}
// ---- wrapper/IMetadataService.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
interface IMetadataService {
function uri(uint256) external view returns (string memory);
}
// ---- wrapper/StaticMetadataService.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
contract StaticMetadataService {
string private _uri;
constructor(string memory _metaDataUri) {
_uri = _metaDataUri;
}
function uri(uint256) public view returns (string memory) {
return _uri;
}
}
// ---- wrapper/INameWrapper.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "../registry/ENS.sol";
import "../ethregistrar/IBaseRegistrar.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "./IMetadataService.sol";
import "./INameWrapperUpgrade.sol";
uint32 constant CANNOT_UNWRAP = 1;
uint32 constant CANNOT_BURN_FUSES = 2;
uint32 constant CANNOT_TRANSFER = 4;
uint32 constant CANNOT_SET_RESOLVER = 8;
uint32 constant CANNOT_SET_TTL = 16;
uint32 constant CANNOT_CREATE_SUBDOMAIN = 32;
uint32 constant CANNOT_APPROVE = 64;
//uint16 reserved for parent controlled fuses from bit 17 to bit 32
uint32 constant PARENT_CANNOT_CONTROL = 1 << 16;
uint32 constant IS_DOT_ETH = 1 << 17;
uint32 constant CAN_EXTEND_EXPIRY = 1 << 18;
uint32 constant CAN_DO_EVERYTHING = 0;
uint32 constant PARENT_CONTROLLED_FUSES = 0xFFFF0000;
// all fuses apart from IS_DOT_ETH
uint32 constant USER_SETTABLE_FUSES = 0xFFFDFFFF;
interface INameWrapper is IERC1155 {
event NameWrapped(
bytes32 indexed node,
bytes name,
address owner,
uint32 fuses,
uint64 expiry
);
event NameUnwrapped(bytes32 indexed node, address owner);
event FusesSet(bytes32 indexed node, uint32 fuses);
event ExpiryExtended(bytes32 indexed node, uint64 expiry);
function ens() external view returns (ENS);
function registrar() external view returns (IBaseRegistrar);
function metadataService() external view returns (IMetadataService);
function names(bytes32) external view returns (bytes memory);
function name() external view returns (string memory);
function upgradeContract() external view returns (INameWrapperUpgrade);
function supportsInterface(bytes4 interfaceID) external view returns (bool);
function wrap(
bytes calldata name,
address wrappedOwner,
address resolver
) external;
function wrapETH2LD(
string calldata label,
address wrappedOwner,
uint16 ownerControlledFuses,
address resolver
) external returns (uint64 expires);
function registerAndWrapETH2LD(
string calldata label,
address wrappedOwner,
uint256 duration,
address resolver,
uint16 ownerControlledFuses
) external returns (uint256 registrarExpiry);
function renew(
uint256 labelHash,
uint256 duration
) external returns (uint256 expires);
function unwrap(bytes32 node, bytes32 label, address owner) external;
function unwrapETH2LD(
bytes32 label,
address newRegistrant,
address newController
) external;
function upgrade(bytes calldata name, bytes calldata extraData) external;
function setFuses(
bytes32 node,
uint16 ownerControlledFuses
) external returns (uint32 newFuses);
function setChildFuses(
bytes32 parentNode,
bytes32 labelhash,
uint32 fuses,
uint64 expiry
) external;
function setSubnodeRecord(
bytes32 node,
string calldata label,
address owner,
address resolver,
uint64 ttl,
uint32 fuses,
uint64 expiry
) external returns (bytes32);
function setRecord(
bytes32 node,
address owner,
address resolver,
uint64 ttl
) external;
function setSubnodeOwner(
bytes32 node,
string calldata label,
address newOwner,
uint32 fuses,
uint64 expiry
) external returns (bytes32);
function extendExpiry(
bytes32 node,
bytes32 labelhash,
uint64 expiry
) external returns (uint64);
function canModifyName(
bytes32 node,
address addr
) external view returns (bool);
function setResolver(bytes32 node, address resolver) external;
function setTTL(bytes32 node, uint64 ttl) external;
function ownerOf(uint256 id) external view returns (address owner);
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address);
function getData(
uint256 id
) external view returns (address, uint32, uint64);
function setMetadataService(IMetadataService _metadataService) external;
function uri(uint256 tokenId) external view returns (string memory);
function setUpgradeContract(INameWrapperUpgrade _upgradeAddress) external;
function allFusesBurned(
bytes32 node,
uint32 fuseMask
) external view returns (bool);
function isWrapped(bytes32) external view returns (bool);
function isWrapped(bytes32, bytes32) external view returns (bool);
}
// ---- wrapper/ERC1155Fuse.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol";
import "@openzeppelin/contracts/utils/Address.sol";
/* This contract is a variation on ERC1155 with the additions of _setData, getData and _beforeTransfer and ownerOf. _setData and getData allows the use of the other 96 bits next to the address of the owner for extra data. We use this to store 'fuses' that control permissions that can be burnt. 32 bits are used for the fuses themselves and 64 bits are used for the expiry of the name. When a name has expired, its fuses will be be set back to 0 */
abstract contract ERC1155Fuse is ERC165, IERC1155, IERC1155MetadataURI {
using Address for address;
/// @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
event Approval(
address indexed owner,
address indexed approved,
uint256 indexed tokenId
);
mapping(uint256 => uint256) public _tokens;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;
// Mapping from token ID to approved address
mapping(uint256 => address) internal _tokenApprovals;
/**************************************************************************
* ERC721 methods
*************************************************************************/
function ownerOf(uint256 id) public view virtual returns (address) {
(address owner, , ) = getData(id);
return owner;
}
/// @dev See {IERC721-approve}.
function approve(address to, uint256 tokenId) public virtual {
address owner = ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
msg.sender == owner || isApprovedForAll(owner, msg.sender),
"ERC721: approve caller is not token owner or approved for all"
);
_approve(to, tokenId);
}
/// @dev See {IERC721-getApproved}.
function getApproved(
uint256 tokenId
) public view virtual returns (address) {
return _tokenApprovals[tokenId];
}
/// @dev See {IERC165-supportsInterface}.
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IERC1155).interfaceId ||
interfaceId == type(IERC1155MetadataURI).interfaceId ||
super.supportsInterface(interfaceId);
}
/// @dev See {IERC1155-balanceOf}.
/// Requirements:
/// - `account` cannot be the zero address.
function balanceOf(
address account,
uint256 id
) public view virtual override returns (uint256) {
require(
account != address(0),
"ERC1155: balance query for the zero address"
);
address owner = ownerOf(id);
if (owner == account) {
return 1;
}
return 0;
}
/// @dev See {IERC1155-balanceOfBatch}.
/// Requirements:
/// - `accounts` and `ids` must have the same length.
function balanceOfBatch(
address[] memory accounts,
uint256[] memory ids
) public view virtual override returns (uint256[] memory) {
require(
accounts.length == ids.length,
"ERC1155: accounts and ids length mismatch"
);
uint256[] memory batchBalances = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; ++i) {
batchBalances[i] = balanceOf(accounts[i], ids[i]);
}
return batchBalances;
}
/// @dev See {IERC1155-setApprovalForAll}.
function setApprovalForAll(
address operator,
bool approved
) public virtual override {
require(
msg.sender != operator,
"ERC1155: setting approval status for self"
);
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
/// @dev See {IERC1155-isApprovedForAll}.
function isApprovedForAll(
address account,
address operator
) public view virtual override returns (bool) {
return _operatorApprovals[account][operator];
}
/// @dev Returns the Name's owner address and fuses
function getData(
uint256 tokenId
) public view virtual returns (address owner, uint32 fuses, uint64 expiry) {
uint256 t = _tokens[tokenId];
owner = address(uint160(t));
expiry = uint64(t >> 192);
fuses = uint32(t >> 160);
}
/// @dev See {IERC1155-safeTransferFrom}.
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) public virtual override {
require(to != address(0), "ERC1155: transfer to the zero address");
require(
from == msg.sender || isApprovedForAll(from, msg.sender),
"ERC1155: caller is not owner nor approved"
);
_transfer(from, to, id, amount, data);
}
/// @dev See {IERC1155-safeBatchTransferFrom}.
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public virtual override {
require(
ids.length == amounts.length,
"ERC1155: ids and amounts length mismatch"
);
require(to != address(0), "ERC1155: transfer to the zero address");
require(
from == msg.sender || isApprovedForAll(from, msg.sender),
"ERC1155: transfer caller is not owner nor approved"
);
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids[i];
uint256 amount = amounts[i];
(address oldOwner, uint32 fuses, uint64 expiry) = getData(id);
_beforeTransfer(id, fuses, expiry);
require(
amount == 1 && oldOwner == from,
"ERC1155: insufficient balance for transfer"
);
_setData(id, to, fuses, expiry);
}
emit TransferBatch(msg.sender, from, to, ids, amounts);
_doSafeBatchTransferAcceptanceCheck(
msg.sender,
from,
to,
ids,
amounts,
data
);
}
/**************************************************************************
* Internal/private methods
*************************************************************************/
/// @dev Sets the Name's owner address and fuses
function _setData(
uint256 tokenId,
address owner,
uint32 fuses,
uint64 expiry
) internal virtual {
_tokens[tokenId] =
uint256(uint160(owner)) |
(uint256(fuses) << 160) |
(uint256(expiry) << 192);
}
function _beforeTransfer(
uint256 id,
uint32 fuses,
uint64 expiry
) internal virtual;
function _clearOwnerAndFuses(
address owner,
uint32 fuses,
uint64 expiry
) internal virtual returns (address, uint32);
function _mint(
bytes32 node,
address owner,
uint32 fuses,
uint64 expiry
) internal virtual {
uint256 tokenId = uint256(node);
(address oldOwner, uint32 oldFuses, uint64 oldExpiry) = getData(
uint256(node)
);
uint32 parentControlledFuses = (uint32(type(uint16).max) << 16) &
oldFuses;
if (oldExpiry > expiry) {
expiry = oldExpiry;
}
if (oldExpiry >= block.timestamp) {
fuses = fuses | parentControlledFuses;
}
require(oldOwner == address(0), "ERC1155: mint of existing token");
require(owner != address(0), "ERC1155: mint to the zero address");
require(
owner != address(this),
"ERC1155: newOwner cannot be the NameWrapper contract"
);
_setData(tokenId, owner, fuses, expiry);
emit TransferSingle(msg.sender, address(0x0), owner, tokenId, 1);
_doSafeTransferAcceptanceCheck(
msg.sender,
address(0),
owner,
tokenId,
1,
""
);
}
function _burn(uint256 tokenId) internal virtual {
(address oldOwner, uint32 fuses, uint64 expiry) = ERC1155Fuse.getData(
tokenId
);
(, fuses) = _clearOwnerAndFuses(oldOwner, fuses, expiry);
// Clear approvals
delete _tokenApprovals[tokenId];
// Fuses and expiry are kept on burn
_setData(tokenId, address(0x0), fuses, expiry);
emit TransferSingle(msg.sender, oldOwner, address(0x0), tokenId, 1);
}
function _transfer(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) internal {
(address oldOwner, uint32 fuses, uint64 expiry) = getData(id);
_beforeTransfer(id, fuses, expiry);
require(
amount == 1 && oldOwner == from,
"ERC1155: insufficient balance for transfer"
);
if (oldOwner == to) {
return;
}
_setData(id, to, fuses, expiry);
emit TransferSingle(msg.sender, from, to, id, amount);
_doSafeTransferAcceptanceCheck(msg.sender, from, to, id, amount, data);
}
function _doSafeTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) private {
if (to.isContract()) {
try
IERC1155Receiver(to).onERC1155Received(
operator,
from,
id,
amount,
data
)
returns (bytes4 response) {
if (
response != IERC1155Receiver(to).onERC1155Received.selector
) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non ERC1155Receiver implementer");
}
}
}
function _doSafeBatchTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) private {
if (to.isContract()) {
try
IERC1155Receiver(to).onERC1155BatchReceived(
operator,
from,
ids,
amounts,
data
)
returns (bytes4 response) {
if (
response !=
IERC1155Receiver(to).onERC1155BatchReceived.selector
) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non ERC1155Receiver implementer");
}
}
}
/* ERC721 internal functions */
/// @dev Approve `to` to operate on `tokenId`
/// Emits an {Approval} event.
function _approve(address to, uint256 tokenId) internal virtual {
_tokenApprovals[tokenId] = to;
emit Approval(ownerOf(tokenId), to, tokenId);
}
}
// ---- wrapper/mocks/ERC1155ReceiverMock.sol ----
// Based on https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.1.0/test/token/ERC1155/ERC1155.behaviour.js
// Copyright (c) 2016-2020 zOS Global Limited
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
contract ERC1155ReceiverMock is IERC1155Receiver, ERC165 {
bytes4 private _recRetval;
bool private _recReverts;
bytes4 private _batRetval;
bool private _batReverts;
event Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes data
);
event BatchReceived(
address operator,
address from,
uint256[] ids,
uint256[] values,
bytes data
);
constructor(
bytes4 recRetval,
bool recReverts,
bytes4 batRetval,
bool batReverts
) {
_recRetval = recRetval;
_recReverts = recReverts;
_batRetval = batRetval;
_batReverts = batReverts;
}
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
) external override returns (bytes4) {
require(!_recReverts, "ERC1155ReceiverMock: reverting on receive");
emit Received(operator, from, id, value, data);
return _recRetval;
}
function onERC1155BatchReceived(
address operator,
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external override returns (bytes4) {
require(
!_batReverts,
"ERC1155ReceiverMock: reverting on batch receive"
);
emit BatchReceived(operator, from, ids, values, data);
return _batRetval;
}
}
// ---- wrapper/mocks/TestUnwrap.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "../../registry/ENS.sol";
import "../../ethregistrar/IBaseRegistrar.sol";
import {NameCoder} from "../../utils/NameCoder.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract TestUnwrap is Ownable {
bytes32 private constant ETH_NODE =
0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae;
ENS public immutable ens;
IBaseRegistrar public immutable registrar;
mapping(address => bool) public approvedWrapper;
constructor(ENS _ens, IBaseRegistrar _registrar) {
ens = _ens;
registrar = _registrar;
}
function setWrapperApproval(
address wrapper,
bool approved
) public onlyOwner {
approvedWrapper[wrapper] = approved;
}
function wrapETH2LD(
string calldata label,
address wrappedOwner,
uint32 fuses,
uint64 expiry,
address resolver
) public {
_unwrapETH2LD(keccak256(bytes(label)), wrappedOwner, msg.sender);
}
function setSubnodeRecord(
bytes32 parentNode,
string memory label,
address newOwner,
address resolver,
uint64 ttl,
uint32 fuses,
uint64 expiry
) public {
bytes32 node = _makeNode(parentNode, keccak256(bytes(label)));
_unwrapSubnode(node, newOwner, msg.sender);
}
function wrapFromUpgrade(
bytes calldata name,
address wrappedOwner,
uint32 fuses,
uint64 expiry,
address approved,
bytes calldata extraData
) public {
(bytes32 labelhash, uint256 offset) = NameCoder.readLabel(name, 0);
bytes32 parentNode = NameCoder.namehash(name, offset);
bytes32 node = _makeNode(parentNode, labelhash);
if (parentNode == ETH_NODE) {
_unwrapETH2LD(labelhash, wrappedOwner, msg.sender);
} else {
_unwrapSubnode(node, wrappedOwner, msg.sender);
}
}
function _unwrapETH2LD(
bytes32 labelhash,
address wrappedOwner,
address sender
) private {
uint256 tokenId = uint256(labelhash);
address registrant = registrar.ownerOf(tokenId);
require(
approvedWrapper[sender] &&
sender == registrant &&
registrar.isApprovedForAll(registrant, address(this)),
"Unauthorised"
);
registrar.reclaim(tokenId, wrappedOwner);
registrar.transferFrom(registrant, wrappedOwner, tokenId);
}
function _unwrapSubnode(
bytes32 node,
address newOwner,
address sender
) private {
address owner = ens.owner(node);
require(
approvedWrapper[sender] &&
owner == sender &&
ens.isApprovedForAll(owner, address(this)),
"Unauthorised"
);
ens.setOwner(node, newOwner);
}
function _makeNode(
bytes32 node,
bytes32 labelhash
) private pure returns (bytes32) {
return keccak256(abi.encodePacked(node, labelhash));
}
}
// ---- wrapper/mocks/UpgradedNameWrapperMock.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import {INameWrapperUpgrade} from "../INameWrapperUpgrade.sol";
import "../../registry/ENS.sol";
import "../../ethregistrar/IBaseRegistrar.sol";
import {NameCoder} from "../../utils/NameCoder.sol";
contract UpgradedNameWrapperMock is INameWrapperUpgrade {
bytes32 private constant ETH_NODE =
0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae;
ENS public immutable ens;
IBaseRegistrar public immutable registrar;
constructor(ENS _ens, IBaseRegistrar _registrar) {
ens = _ens;
registrar = _registrar;
}
event NameUpgraded(
bytes name,
address wrappedOwner,
uint32 fuses,
uint64 expiry,
address approved,
bytes extraData
);
function wrapFromUpgrade(
bytes calldata name,
address wrappedOwner,
uint32 fuses,
uint64 expiry,
address approved,
bytes calldata extraData
) public {
(bytes32 labelhash, uint256 offset) = NameCoder.readLabel(name, 0);
bytes32 parentNode = NameCoder.namehash(name, offset);
bytes32 node = _makeNode(parentNode, labelhash);
if (parentNode == ETH_NODE) {
address registrant = registrar.ownerOf(uint256(labelhash));
require(
msg.sender == registrant &&
registrar.isApprovedForAll(registrant, address(this)),
"No approval for registrar"
);
} else {
address owner = ens.owner(node);
require(
msg.sender == owner &&
ens.isApprovedForAll(owner, address(this)),
"No approval for registry"
);
}
emit NameUpgraded(
name,
wrappedOwner,
fuses,
expiry,
approved,
extraData
);
}
function _makeNode(
bytes32 node,
bytes32 labelhash
) private pure returns (bytes32) {
return keccak256(abi.encodePacked(node, labelhash));
}
}
// ---- wrapper/Controllable.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "@openzeppelin/contracts/access/Ownable.sol";
contract Controllable is Ownable {
mapping(address => bool) public controllers;
event ControllerChanged(address indexed controller, bool active);
function setController(address controller, bool active) public onlyOwner {
controllers[controller] = active;
emit ControllerChanged(controller, active);
}
modifier onlyController() {
require(
controllers[msg.sender],
"Controllable: Caller is not a controller"
);
_;
}
}
// ---- root/Ownable.sol ----
pragma solidity ^0.8.4;
contract Ownable {
address public owner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
modifier onlyOwner() {
require(isOwner(msg.sender));
_;
}
constructor() public {
owner = msg.sender;
}
function transferOwnership(address newOwner) public onlyOwner {
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
function isOwner(address addr) public view returns (bool) {
return owner == addr;
}
}
// ---- root/Root.sol ----
pragma solidity ^0.8.4;
import "../registry/ENS.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./Controllable.sol";
contract Root is Ownable, Controllable {
bytes32 private constant ROOT_NODE = bytes32(0);
bytes4 private constant INTERFACE_META_ID =
bytes4(keccak256("supportsInterface(bytes4)"));
event TLDLocked(bytes32 indexed label);
ENS public ens;
mapping(bytes32 => bool) public locked;
constructor(ENS _ens) public {
ens = _ens;
}
function setSubnodeOwner(
bytes32 label,
address owner
) external onlyController {
require(!locked[label]);
ens.setSubnodeOwner(ROOT_NODE, label, owner);
}
function setResolver(address resolver) external onlyOwner {
ens.setResolver(ROOT_NODE, resolver);
}
function lock(bytes32 label) external onlyOwner {
emit TLDLocked(label);
locked[label] = true;
}
function supportsInterface(
bytes4 interfaceID
) external pure returns (bool) {
return interfaceID == INTERFACE_META_ID;
}
}
// ---- root/Controllable.sol ----
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/access/Ownable.sol";
contract Controllable is Ownable {
mapping(address => bool) public controllers;
event ControllerChanged(address indexed controller, bool enabled);
modifier onlyController() {
require(
controllers[msg.sender],
"Controllable: Caller is not a controller"
);
_;
}
function setController(address controller, bool enabled) public onlyOwner {
controllers[controller] = enabled;
emit ControllerChanged(controller, enabled);
}
}
// ---- resolvers/IMulticallable.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
interface IMulticallable {
function multicall(
bytes[] calldata data
) external returns (bytes[] memory results);
function multicallWithNodeCheck(
bytes32,
bytes[] calldata data
) external returns (bytes[] memory results);
}
// ---- resolvers/OwnedResolver.sol ----
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./profiles/ABIResolver.sol";
import "./profiles/AddrResolver.sol";
import "./profiles/ContentHashResolver.sol";
import "./profiles/DNSResolver.sol";
import "./profiles/InterfaceResolver.sol";
import "./profiles/NameResolver.sol";
import "./profiles/PubkeyResolver.sol";
import "./profiles/TextResolver.sol";
import "./profiles/ExtendedResolver.sol";
/// A simple resolver anyone can use; only allows the owner of a node to set its
/// address.
contract OwnedResolver is
Ownable,
ABIResolver,
AddrResolver,
ContentHashResolver,
DNSResolver,
InterfaceResolver,
NameResolver,
PubkeyResolver,
TextResolver,
ExtendedResolver
{
function isAuthorised(bytes32) internal view override returns (bool) {
return msg.sender == owner();
}
function supportsInterface(
bytes4 interfaceID
)
public
view
virtual
override(
ABIResolver,
AddrResolver,
ContentHashResolver,
DNSResolver,
InterfaceResolver,
NameResolver,
PubkeyResolver,
TextResolver
)
returns (bool)
{
return super.supportsInterface(interfaceID);
}
}
// ---- resolvers/ResolverBase.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "./profiles/IVersionableResolver.sol";
abstract contract ResolverBase is ERC165, IVersionableResolver {
mapping(bytes32 => uint64) public recordVersions;
function isAuthorised(bytes32 node) internal view virtual returns (bool);
modifier authorised(bytes32 node) {
require(isAuthorised(node));
_;
}
/// Increments the record version associated with an ENS node.
/// May only be called by the owner of that node in the ENS registry.
/// @param node The node to update.
function clearRecords(bytes32 node) public virtual authorised(node) {
recordVersions[node]++;
emit VersionChanged(node, recordVersions[node]);
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
interfaceID == type(IVersionableResolver).interfaceId ||
super.supportsInterface(interfaceID);
}
}
// ---- resolvers/profiles/IExtendedDNSResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
interface IExtendedDNSResolver {
function resolve(
bytes memory name,
bytes memory data,
bytes memory context
) external view returns (bytes memory);
}
// ---- resolvers/profiles/IContentHashResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface IContentHashResolver {
event ContenthashChanged(bytes32 indexed node, bytes hash);
/// Returns the contenthash associated with an ENS node.
/// @param node The ENS node to query.
/// @return The associated contenthash.
function contenthash(bytes32 node) external view returns (bytes memory);
}
// ---- resolvers/profiles/PubkeyResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "../ResolverBase.sol";
import "./IPubkeyResolver.sol";
abstract contract PubkeyResolver is IPubkeyResolver, ResolverBase {
struct PublicKey {
bytes32 x;
bytes32 y;
}
mapping(uint64 => mapping(bytes32 => PublicKey)) versionable_pubkeys;
/// Sets the SECP256k1 public key associated with an ENS node.
/// @param node The ENS node to query
/// @param x the X coordinate of the curve point for the public key.
/// @param y the Y coordinate of the curve point for the public key.
function setPubkey(
bytes32 node,
bytes32 x,
bytes32 y
) external virtual authorised(node) {
versionable_pubkeys[recordVersions[node]][node] = PublicKey(x, y);
emit PubkeyChanged(node, x, y);
}
/// Returns the SECP256k1 public key associated with an ENS node.
/// Defined in EIP 619.
/// @param node The ENS node to query
/// @return x The X coordinate of the curve point for the public key.
/// @return y The Y coordinate of the curve point for the public key.
function pubkey(
bytes32 node
) external view virtual override returns (bytes32 x, bytes32 y) {
uint64 currentRecordVersion = recordVersions[node];
return (
versionable_pubkeys[currentRecordVersion][node].x,
versionable_pubkeys[currentRecordVersion][node].y
);
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
interfaceID == type(IPubkeyResolver).interfaceId ||
super.supportsInterface(interfaceID);
}
}
// ---- resolvers/profiles/ABIResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "./IABIResolver.sol";
import "../ResolverBase.sol";
abstract contract ABIResolver is IABIResolver, ResolverBase {
mapping(uint64 => mapping(bytes32 => mapping(uint256 => bytes))) versionable_abis;
/// Sets the ABI associated with an ENS node.
/// Nodes may have one ABI of each content type. To remove an ABI, set it to
/// the empty string.
/// @param node The node to update.
/// @param contentType The content type of the ABI
/// @param data The ABI data.
function setABI(
bytes32 node,
uint256 contentType,
bytes calldata data
) external virtual authorised(node) {
// Content types must be powers of 2
require(((contentType - 1) & contentType) == 0);
versionable_abis[recordVersions[node]][node][contentType] = data;
emit ABIChanged(node, contentType);
}
/// Returns the ABI associated with an ENS node.
/// Defined in EIP205.
/// @param node The ENS node to query
/// @param contentTypes A bitwise OR of the ABI formats accepted by the caller.
/// @return contentType The content type of the return value
/// @return data The ABI data
function ABI(
bytes32 node,
uint256 contentTypes
) external view virtual override returns (uint256, bytes memory) {
mapping(uint256 => bytes) storage abiset = versionable_abis[
recordVersions[node]
][node];
for (
uint256 contentType = 1;
contentType <= contentTypes;
contentType <<= 1
) {
if (
(contentType & contentTypes) != 0 &&
abiset[contentType].length > 0
) {
return (contentType, abiset[contentType]);
}
}
return (0, bytes(""));
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
interfaceID == type(IABIResolver).interfaceId ||
super.supportsInterface(interfaceID);
}
}
// ---- resolvers/profiles/IExtendedResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
interface IExtendedResolver {
function resolve(
bytes memory name,
bytes memory data
) external view returns (bytes memory);
}
// ---- resolvers/profiles/AddrResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "../ResolverBase.sol";
import "./IAddrResolver.sol";
import "./IAddressResolver.sol";
abstract contract AddrResolver is
IAddrResolver,
IAddressResolver,
ResolverBase
{
uint256 private constant COIN_TYPE_ETH = 60;
mapping(uint64 => mapping(bytes32 => mapping(uint256 => bytes))) versionable_addresses;
/// Sets the address associated with an ENS node.
/// May only be called by the owner of that node in the ENS registry.
/// @param node The node to update.
/// @param a The address to set.
function setAddr(
bytes32 node,
address a
) external virtual authorised(node) {
setAddr(node, COIN_TYPE_ETH, addressToBytes(a));
}
/// Returns the address associated with an ENS node.
/// @param node The ENS node to query.
/// @return The associated address.
function addr(
bytes32 node
) public view virtual override returns (address payable) {
bytes memory a = addr(node, COIN_TYPE_ETH);
if (a.length == 0) {
return payable(0);
}
return bytesToAddress(a);
}
function setAddr(
bytes32 node,
uint256 coinType,
bytes memory a
) public virtual authorised(node) {
emit AddressChanged(node, coinType, a);
if (coinType == COIN_TYPE_ETH) {
emit AddrChanged(node, bytesToAddress(a));
}
versionable_addresses[recordVersions[node]][node][coinType] = a;
}
function addr(
bytes32 node,
uint256 coinType
) public view virtual override returns (bytes memory) {
return versionable_addresses[recordVersions[node]][node][coinType];
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
interfaceID == type(IAddrResolver).interfaceId ||
interfaceID == type(IAddressResolver).interfaceId ||
super.supportsInterface(interfaceID);
}
function bytesToAddress(
bytes memory b
) internal pure returns (address payable a) {
require(b.length == 20);
assembly {
a := div(mload(add(b, 32)), exp(256, 12))
}
}
function addressToBytes(address a) internal pure returns (bytes memory b) {
b = new bytes(20);
assembly {
mstore(add(b, 32), mul(a, exp(256, 12)))
}
}
}
// ---- resolvers/profiles/IVersionableResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface IVersionableResolver {
event VersionChanged(bytes32 indexed node, uint64 newVersion);
function recordVersions(bytes32 node) external view returns (uint64);
}
// ---- resolvers/profiles/IPubkeyResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface IPubkeyResolver {
event PubkeyChanged(bytes32 indexed node, bytes32 x, bytes32 y);
/// Returns the SECP256k1 public key associated with an ENS node.
/// Defined in EIP 619.
/// @param node The ENS node to query
/// @return x The X coordinate of the curve point for the public key.
/// @return y The Y coordinate of the curve point for the public key.
function pubkey(bytes32 node) external view returns (bytes32 x, bytes32 y);
}
// ---- resolvers/profiles/ContentHashResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "../ResolverBase.sol";
import "./IContentHashResolver.sol";
abstract contract ContentHashResolver is IContentHashResolver, ResolverBase {
mapping(uint64 => mapping(bytes32 => bytes)) versionable_hashes;
/// Sets the contenthash associated with an ENS node.
/// May only be called by the owner of that node in the ENS registry.
/// @param node The node to update.
/// @param hash The contenthash to set
function setContenthash(
bytes32 node,
bytes calldata hash
) external virtual authorised(node) {
versionable_hashes[recordVersions[node]][node] = hash;
emit ContenthashChanged(node, hash);
}
/// Returns the contenthash associated with an ENS node.
/// @param node The ENS node to query.
/// @return The associated contenthash.
function contenthash(
bytes32 node
) external view virtual override returns (bytes memory) {
return versionable_hashes[recordVersions[node]][node];
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
interfaceID == type(IContentHashResolver).interfaceId ||
super.supportsInterface(interfaceID);
}
}
// ---- resolvers/profiles/INameResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface INameResolver {
event NameChanged(bytes32 indexed node, string name);
/// Returns the name associated with an ENS node, for reverse records.
/// Defined in EIP181.
/// @param node The ENS node to query.
/// @return The associated name.
function name(bytes32 node) external view returns (string memory);
}
// ---- resolvers/profiles/IAddressResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
/// Interface for the new (multicoin) addr function.
interface IAddressResolver {
event AddressChanged(
bytes32 indexed node,
uint256 coinType,
bytes newAddress
);
function addr(
bytes32 node,
uint256 coinType
) external view returns (bytes memory);
}
// ---- resolvers/profiles/NameResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "../ResolverBase.sol";
import "./INameResolver.sol";
abstract contract NameResolver is INameResolver, ResolverBase {
mapping(uint64 => mapping(bytes32 => string)) versionable_names;
/// Sets the name associated with an ENS node, for reverse records.
/// May only be called by the owner of that node in the ENS registry.
/// @param node The node to update.
function setName(
bytes32 node,
string calldata newName
) external virtual authorised(node) {
versionable_names[recordVersions[node]][node] = newName;
emit NameChanged(node, newName);
}
/// Returns the name associated with an ENS node, for reverse records.
/// Defined in EIP181.
/// @param node The ENS node to query.
/// @return The associated name.
function name(
bytes32 node
) external view virtual override returns (string memory) {
return versionable_names[recordVersions[node]][node];
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
interfaceID == type(INameResolver).interfaceId ||
super.supportsInterface(interfaceID);
}
}
// ---- resolvers/profiles/DNSResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "../ResolverBase.sol";
import "../../dnssec-oracle/RRUtils.sol";
import "./IDNSRecordResolver.sol";
import "./IDNSZoneResolver.sol";
abstract contract DNSResolver is
IDNSRecordResolver,
IDNSZoneResolver,
ResolverBase
{
using RRUtils for *;
using BytesUtils for bytes;
// Zone hashes for the domains.
// A zone hash is an EIP-1577 content hash in binary format that should point to a
// resource containing a single zonefile.
// node => contenthash
mapping(uint64 => mapping(bytes32 => bytes)) private versionable_zonehashes;
// The records themselves. Stored as binary RRSETs
// node => version => name => resource => data
mapping(uint64 => mapping(bytes32 => mapping(bytes32 => mapping(uint16 => bytes))))
private versionable_records;
// Count of number of entries for a given name. Required for DNS resolvers
// when resolving wildcards.
// node => version => name => number of records
mapping(uint64 => mapping(bytes32 => mapping(bytes32 => uint16)))
private versionable_nameEntriesCount;
/// Set one or more DNS records. Records are supplied in wire-format.
/// Records with the same node/name/resource must be supplied one after the
/// other to ensure the data is updated correctly. For example, if the data
/// was supplied:
/// a.example.com IN A 1.2.3.4
/// a.example.com IN A 5.6.7.8
/// www.example.com IN CNAME a.example.com.
/// then this would store the two A records for a.example.com correctly as a
/// single RRSET, however if the data was supplied:
/// a.example.com IN A 1.2.3.4
/// www.example.com IN CNAME a.example.com.
/// a.example.com IN A 5.6.7.8
/// then this would store the first A record, the CNAME, then the second A
/// record which would overwrite the first.
///
/// @param node the namehash of the node for which to set the records
/// @param data the DNS wire format records to set
function setDNSRecords(
bytes32 node,
bytes calldata data
) external virtual authorised(node) {
uint16 resource = 0;
uint256 offset = 0;
bytes memory name;
bytes memory value;
bytes32 nameHash;
uint64 version = recordVersions[node];
// Iterate over the data to add the resource records
for (
RRUtils.RRIterator memory iter = data.iterateRRs(0);
!iter.done();
iter.next()
) {
if (resource == 0) {
resource = iter.dnstype;
name = iter.name();
nameHash = keccak256(abi.encodePacked(name));
value = bytes(iter.rdata());
} else {
bytes memory newName = iter.name();
if (resource != iter.dnstype || !name.equals(newName)) {
setDNSRRSet(
node,
name,
resource,
data,
offset,
iter.offset - offset,
value.length == 0,
version
);
resource = iter.dnstype;
offset = iter.offset;
name = newName;
nameHash = keccak256(name);
value = bytes(iter.rdata());
}
}
}
if (name.length > 0) {
setDNSRRSet(
node,
name,
resource,
data,
offset,
data.length - offset,
value.length == 0,
version
);
}
}
/// Obtain a DNS record.
/// @param node the namehash of the node for which to fetch the record
/// @param name the keccak-256 hash of the fully-qualified name for which to fetch the record
/// @param resource the ID of the resource as per https://en.wikipedia.org/wiki/List_of_DNS_record_types
/// @return the DNS record in wire format if present, otherwise empty
function dnsRecord(
bytes32 node,
bytes32 name,
uint16 resource
) public view virtual override returns (bytes memory) {
return versionable_records[recordVersions[node]][node][name][resource];
}
/// Check if a given node has records.
/// @param node the namehash of the node for which to check the records
/// @param name the namehash of the node for which to check the records
function hasDNSRecords(
bytes32 node,
bytes32 name
) public view virtual returns (bool) {
return (versionable_nameEntriesCount[recordVersions[node]][node][
name
] != 0);
}
/// setZonehash sets the hash for the zone.
/// May only be called by the owner of that node in the ENS registry.
/// @param node The node to update.
/// @param hash The zonehash to set
function setZonehash(
bytes32 node,
bytes calldata hash
) external virtual authorised(node) {
uint64 currentRecordVersion = recordVersions[node];
bytes memory oldhash = versionable_zonehashes[currentRecordVersion][
node
];
versionable_zonehashes[currentRecordVersion][node] = hash;
emit DNSZonehashChanged(node, oldhash, hash);
}
/// zonehash obtains the hash for the zone.
/// @param node The ENS node to query.
/// @return The associated contenthash.
function zonehash(
bytes32 node
) external view virtual override returns (bytes memory) {
return versionable_zonehashes[recordVersions[node]][node];
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
interfaceID == type(IDNSRecordResolver).interfaceId ||
interfaceID == type(IDNSZoneResolver).interfaceId ||
super.supportsInterface(interfaceID);
}
function setDNSRRSet(
bytes32 node,
bytes memory name,
uint16 resource,
bytes memory data,
uint256 offset,
uint256 size,
bool deleteRecord,
uint64 version
) private {
bytes32 nameHash = keccak256(name);
bytes memory rrData = data.substring(offset, size);
if (deleteRecord) {
if (
versionable_records[version][node][nameHash][resource].length !=
0
) {
versionable_nameEntriesCount[version][node][nameHash]--;
}
delete (versionable_records[version][node][nameHash][resource]);
emit DNSRecordDeleted(node, name, resource);
} else {
if (
versionable_records[version][node][nameHash][resource].length ==
0
) {
versionable_nameEntriesCount[version][node][nameHash]++;
}
versionable_records[version][node][nameHash][resource] = rrData;
emit DNSRecordChanged(node, name, resource, rrData);
}
}
}
// ---- resolvers/profiles/IDNSRecordResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface IDNSRecordResolver {
// DNSRecordChanged is emitted whenever a given node/name/resource's RRSET is updated.
event DNSRecordChanged(
bytes32 indexed node,
bytes name,
uint16 resource,
bytes record
);
// DNSRecordDeleted is emitted whenever a given node/name/resource's RRSET is deleted.
event DNSRecordDeleted(bytes32 indexed node, bytes name, uint16 resource);
/// Obtain a DNS record.
/// @param node the namehash of the node for which to fetch the record
/// @param name the keccak-256 hash of the fully-qualified name for which to fetch the record
/// @param resource the ID of the resource as per https://en.wikipedia.org/wiki/List_of_DNS_record_types
/// @return the DNS record in wire format if present, otherwise empty
function dnsRecord(
bytes32 node,
bytes32 name,
uint16 resource
) external view returns (bytes memory);
}
// ---- resolvers/profiles/IAddrResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
/// Interface for the legacy (ETH-only) addr function.
interface IAddrResolver {
event AddrChanged(bytes32 indexed node, address a);
/// Returns the address associated with an ENS node.
/// @param node The ENS node to query.
/// @return The associated address.
function addr(bytes32 node) external view returns (address payable);
}
// ---- resolvers/profiles/IABIResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface IABIResolver {
event ABIChanged(bytes32 indexed node, uint256 indexed contentType);
/// Returns the ABI associated with an ENS node.
/// Defined in EIP205.
/// @param node The ENS node to query
/// @param contentTypes A bitwise OR of the ABI formats accepted by the caller.
/// @return contentType The content type of the return value
/// @return data The ABI data
function ABI(
bytes32 node,
uint256 contentTypes
) external view returns (uint256, bytes memory);
}
// ---- resolvers/profiles/IDNSZoneResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface IDNSZoneResolver {
// DNSZonehashChanged is emitted whenever a given node's zone hash is updated.
event DNSZonehashChanged(
bytes32 indexed node,
bytes lastzonehash,
bytes zonehash
);
/// zonehash obtains the hash for the zone.
/// @param node The ENS node to query.
/// @return The associated contenthash.
function zonehash(bytes32 node) external view returns (bytes memory);
}
// ---- resolvers/profiles/ITextResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface ITextResolver {
event TextChanged(
bytes32 indexed node,
string indexed indexedKey,
string key,
string value
);
/// Returns the text data associated with an ENS node and key.
/// @param node The ENS node to query.
/// @param key The text data key to query.
/// @return The associated text data.
function text(
bytes32 node,
string calldata key
) external view returns (string memory);
}
// ---- resolvers/profiles/ExtendedDNSResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "../../resolvers/profiles/IExtendedDNSResolver.sol";
import "../../resolvers/profiles/IAddressResolver.sol";
import "../../resolvers/profiles/IAddrResolver.sol";
import "../../resolvers/profiles/ITextResolver.sol";
import "../../utils/HexUtils.sol";
import "../../utils/BytesUtils.sol";
/// @dev Resolves names on ENS by interpreting record data stored in a DNS TXT record.
/// This resolver implements the IExtendedDNSResolver interface, meaning that when
/// a DNS name specifies it as the resolver via a TXT record, this resolver's
/// resolve() method is invoked, and is passed any additional information from that
/// text record. This resolver implements a simple text parser allowing a variety
/// of records to be specified in text, which will then be used to resolve the name
/// in ENS.
///
/// To use this, set a TXT record on your DNS name in the following format:
/// ENS1
///
/// For example:
/// ENS1 2.dnsname.ens.eth a[60]=0x1234...
///
/// The record data consists of a series of key=value pairs, separated by spaces. Keys
/// may have an optional argument in square brackets, and values may be either unquoted
/// - in which case they may not contain spaces - or single-quoted. Single quotes in
/// a quoted value may be backslash-escaped.
///
///
/// ┌────────┐
/// │ ┌───┐ │
/// ┌──────────────────────────────┴─┤" "│◄─┴────────────────────────────────────────┐
/// │ └───┘ │
/// │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌────────────┐ ┌───┐ │
/// ^─┴─►│key├─┬─►│"["├───►│arg├───►│"]"├─┬─►│"="├─┬─►│"'"├───►│quoted_value├───►│"'"├─┼─$
/// └───┘ │ └───┘ └───┘ └───┘ │ └───┘ │ └───┘ └────────────┘ └───┘ │
/// └──────────────────────────┘ │ ┌──────────────┐ │
/// └─────────►│unquoted_value├─────────┘
/// └──────────────┘
///
/// Record types:
/// - a[] - Specifies how an `addr()` request should be resolved for the specified
/// `coinType`. Ethereum has `coinType` 60. The value must be 0x-prefixed hexadecimal, and will
/// be returned unmodified; this means that non-EVM addresses will need to be translated
/// into binary format and then encoded in hex.
/// Examples:
/// - a[60]=0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7
/// - a[0]=0x00149010587f8364b964fcaa70687216b53bd2cbd798
/// - a[e] - Specifies how an `addr()` request should be resolved for the specified
/// `chainId`. The value must be 0x-prefixed hexadecimal. When encoding an address for an
/// EVM-based cryptocurrency that uses a chainId instead of a coinType, this syntax *must*
/// be used in place of the coin type - eg, Optimism is `a[e10]`, not `a[2147483658]`.
/// A list of supported cryptocurrencies for both syntaxes can be found here:
/// https://github.com/ensdomains/address-encoder/blob/master/docs/supported-cryptocurrencies.md
/// Example:
/// - a[e10]=0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7
/// - t[] - Specifies how a `text()` request should be resolved for the specified `key`.
/// Examples:
/// - t[com.twitter]=nicksdjohnson
/// - t[url]='https://ens.domains/'
/// - t[note]='I\'m great'
contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 {
using HexUtils for *;
using BytesUtils for *;
using Strings for *;
uint256 private constant COIN_TYPE_ETH = 60;
error NotImplemented();
error InvalidAddressFormat(bytes addr);
function supportsInterface(
bytes4 interfaceId
) external view virtual override returns (bool) {
return interfaceId == type(IExtendedDNSResolver).interfaceId;
}
function resolve(
bytes calldata /* name */,
bytes calldata data,
bytes calldata context
) external pure override returns (bytes memory) {
bytes4 selector = bytes4(data);
if (selector == IAddrResolver.addr.selector) {
return _resolveAddr(context);
} else if (selector == IAddressResolver.addr.selector) {
return _resolveAddress(data, context);
} else if (selector == ITextResolver.text.selector) {
return _resolveText(data, context);
}
revert NotImplemented();
}
function _resolveAddress(
bytes calldata data,
bytes calldata context
) internal pure returns (bytes memory) {
(, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256));
bytes memory value;
// Per https://docs.ens.domains/ensip/11#specification
if (coinType & 0x80000000 != 0) {
value = _findValue(
context,
bytes.concat(
"a[e",
bytes((coinType & 0x7fffffff).toString()),
"]="
)
);
} else {
value = _findValue(
context,
bytes.concat("a[", bytes(coinType.toString()), "]=")
);
}
if (value.length == 0) {
return value;
}
(address record, bool valid) = value.hexToAddress(2, value.length);
if (!valid) revert InvalidAddressFormat(value);
return abi.encode(record);
}
function _resolveAddr(
bytes calldata context
) internal pure returns (bytes memory) {
bytes memory value = _findValue(context, "a[60]=");
if (value.length == 0) {
return value;
}
(address record, bool valid) = value.hexToAddress(2, value.length);
if (!valid) revert InvalidAddressFormat(value);
return abi.encode(record);
}
function _resolveText(
bytes calldata data,
bytes calldata context
) internal pure returns (bytes memory) {
(, string memory key) = abi.decode(data[4:], (bytes32, string));
bytes memory value = _findValue(
context,
bytes.concat("t[", bytes(key), "]=")
);
return abi.encode(value);
}
uint256 constant STATE_START = 0;
uint256 constant STATE_IGNORED_KEY = 1;
uint256 constant STATE_IGNORED_KEY_ARG = 2;
uint256 constant STATE_VALUE = 3;
uint256 constant STATE_QUOTED_VALUE = 4;
uint256 constant STATE_UNQUOTED_VALUE = 5;
uint256 constant STATE_IGNORED_VALUE = 6;
uint256 constant STATE_IGNORED_QUOTED_VALUE = 7;
uint256 constant STATE_IGNORED_UNQUOTED_VALUE = 8;
/// @dev Implements a DFA to parse the text record, looking for an entry
/// matching `key`.
/// @param data The text record to parse.
/// @param key The exact key to search for.
/// @return value The value if found, or an empty string if `key` does not exist.
function _findValue(
bytes memory data,
bytes memory key
) internal pure returns (bytes memory value) {
// Here we use a simple state machine to parse the text record. We
// process characters one at a time; each character can trigger a
// transition to a new state, or terminate the DFA and return a value.
// For states that expect to process a number of tokens, we use
// inner loops for efficiency reasons, to avoid the need to go
// through the outer loop and switch statement for every character.
uint256 state = STATE_START;
uint256 len = data.length;
for (uint256 i = 0; i < len; ) {
if (state == STATE_START) {
// Look for a matching key.
if (data.equals(i, key, 0, key.length)) {
i += key.length;
state = STATE_VALUE;
} else {
state = STATE_IGNORED_KEY;
}
} else if (state == STATE_IGNORED_KEY) {
for (; i < len; i++) {
if (data[i] == "=") {
state = STATE_IGNORED_VALUE;
i += 1;
break;
} else if (data[i] == "[") {
state = STATE_IGNORED_KEY_ARG;
i += 1;
break;
}
}
} else if (state == STATE_IGNORED_KEY_ARG) {
for (; i < len; i++) {
if (data[i] == "]") {
state = STATE_IGNORED_VALUE;
i += 1;
if (data[i] == "=") {
i += 1;
}
break;
}
}
} else if (state == STATE_VALUE) {
if (data[i] == "'") {
state = STATE_QUOTED_VALUE;
i += 1;
} else {
state = STATE_UNQUOTED_VALUE;
}
} else if (state == STATE_QUOTED_VALUE) {
uint256 start = i;
uint256 valueLen = 0;
bool escaped = false;
for (; i < len; i++) {
if (escaped) {
data[start + valueLen] = data[i];
valueLen += 1;
escaped = false;
} else {
if (data[i] == "\\") {
escaped = true;
} else if (data[i] == "'") {
return data.substring(start, valueLen);
} else {
data[start + valueLen] = data[i];
valueLen += 1;
}
}
}
} else if (state == STATE_UNQUOTED_VALUE) {
uint256 start = i;
for (; i < len; i++) {
if (data[i] == " ") {
return data.substring(start, i - start);
}
}
return data.substring(start, len - start);
} else if (state == STATE_IGNORED_VALUE) {
if (data[i] == "'") {
state = STATE_IGNORED_QUOTED_VALUE;
i += 1;
} else {
state = STATE_IGNORED_UNQUOTED_VALUE;
}
} else if (state == STATE_IGNORED_QUOTED_VALUE) {
bool escaped = false;
for (; i < len; i++) {
if (escaped) {
escaped = false;
} else {
if (data[i] == "\\") {
escaped = true;
} else if (data[i] == "'") {
i += 1;
while (data[i] == " ") {
i += 1;
}
state = STATE_START;
break;
}
}
}
} else {
assert(state == STATE_IGNORED_UNQUOTED_VALUE);
for (; i < len; i++) {
if (data[i] == " ") {
while (data[i] == " ") {
i += 1;
}
state = STATE_START;
break;
}
}
}
}
return "";
}
}
// ---- resolvers/profiles/IInterfaceResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface IInterfaceResolver {
event InterfaceChanged(
bytes32 indexed node,
bytes4 indexed interfaceID,
address implementer
);
/// Returns the address of a contract that implements the specified interface for this name.
/// If an implementer has not been set for this interfaceID and name, the resolver will query
/// the contract at `addr()`. If `addr()` is set, a contract exists at that address, and that
/// contract implements EIP165 and returns `true` for the specified interfaceID, its address
/// will be returned.
/// @param node The ENS node to query.
/// @param interfaceID The EIP 165 interface ID to check for.
/// @return The address that implements this interface, or 0 if the interface is unsupported.
function interfaceImplementer(
bytes32 node,
bytes4 interfaceID
) external view returns (address);
}
// ---- resolvers/profiles/ExtendedResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract ExtendedResolver {
function resolve(
bytes memory /* name */,
bytes memory data
) external view returns (bytes memory) {
(bool success, bytes memory result) = address(this).staticcall(data);
if (success) {
return result;
} else {
// Revert with the reason provided by the call
assembly {
revert(add(result, 0x20), mload(result))
}
}
}
}
// ---- resolvers/profiles/InterfaceResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "../ResolverBase.sol";
import "./AddrResolver.sol";
import "./IInterfaceResolver.sol";
abstract contract InterfaceResolver is IInterfaceResolver, AddrResolver {
mapping(uint64 => mapping(bytes32 => mapping(bytes4 => address))) versionable_interfaces;
/// Sets an interface associated with a name.
/// Setting the address to 0 restores the default behaviour of querying the contract at `addr()` for interface support.
/// @param node The node to update.
/// @param interfaceID The EIP 165 interface ID.
/// @param implementer The address of a contract that implements this interface for this node.
function setInterface(
bytes32 node,
bytes4 interfaceID,
address implementer
) external virtual authorised(node) {
versionable_interfaces[recordVersions[node]][node][
interfaceID
] = implementer;
emit InterfaceChanged(node, interfaceID, implementer);
}
/// Returns the address of a contract that implements the specified interface for this name.
/// If an implementer has not been set for this interfaceID and name, the resolver will query
/// the contract at `addr()`. If `addr()` is set, a contract exists at that address, and that
/// contract implements EIP165 and returns `true` for the specified interfaceID, its address
/// will be returned.
/// @param node The ENS node to query.
/// @param interfaceID The EIP 165 interface ID to check for.
/// @return The address that implements this interface, or 0 if the interface is unsupported.
function interfaceImplementer(
bytes32 node,
bytes4 interfaceID
) external view virtual override returns (address) {
address implementer = versionable_interfaces[recordVersions[node]][
node
][interfaceID];
if (implementer != address(0)) {
return implementer;
}
address a = addr(node);
if (a == address(0)) {
return address(0);
}
(bool success, bytes memory returnData) = a.staticcall(
abi.encodeWithSignature(
"supportsInterface(bytes4)",
type(IERC165).interfaceId
)
);
if (!success || returnData.length < 32 || returnData[31] == 0) {
// EIP 165 not supported by target
return address(0);
}
(success, returnData) = a.staticcall(
abi.encodeWithSignature("supportsInterface(bytes4)", interfaceID)
);
if (!success || returnData.length < 32 || returnData[31] == 0) {
// Specified interface not supported by target
return address(0);
}
return a;
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
interfaceID == type(IInterfaceResolver).interfaceId ||
super.supportsInterface(interfaceID);
}
}
// ---- resolvers/profiles/TextResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "../ResolverBase.sol";
import "./ITextResolver.sol";
abstract contract TextResolver is ITextResolver, ResolverBase {
mapping(uint64 => mapping(bytes32 => mapping(string => string))) versionable_texts;
/// Sets the text data associated with an ENS node and key.
/// May only be called by the owner of that node in the ENS registry.
/// @param node The node to update.
/// @param key The key to set.
/// @param value The text data value to set.
function setText(
bytes32 node,
string calldata key,
string calldata value
) external virtual authorised(node) {
versionable_texts[recordVersions[node]][node][key] = value;
emit TextChanged(node, key, key, value);
}
/// Returns the text data associated with an ENS node and key.
/// @param node The ENS node to query.
/// @param key The text data key to query.
/// @return The associated text data.
function text(
bytes32 node,
string calldata key
) external view virtual override returns (string memory) {
return versionable_texts[recordVersions[node]][node][key];
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
interfaceID == type(ITextResolver).interfaceId ||
super.supportsInterface(interfaceID);
}
}
// ---- resolvers/PublicResolver.sol ----
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.17 <0.9.0;
import "../registry/ENS.sol";
import "./profiles/ABIResolver.sol";
import "./profiles/AddrResolver.sol";
import "./profiles/ContentHashResolver.sol";
import "./profiles/DNSResolver.sol";
import "./profiles/InterfaceResolver.sol";
import "./profiles/NameResolver.sol";
import "./profiles/PubkeyResolver.sol";
import "./profiles/TextResolver.sol";
import "./Multicallable.sol";
import {ReverseClaimer} from "../reverseRegistrar/ReverseClaimer.sol";
import {INameWrapper} from "../wrapper/INameWrapper.sol";
/// A simple resolver anyone can use; only allows the owner of a node to set its
/// address.
contract PublicResolver is
Multicallable,
ABIResolver,
AddrResolver,
ContentHashResolver,
DNSResolver,
InterfaceResolver,
NameResolver,
PubkeyResolver,
TextResolver,
ReverseClaimer
{
ENS immutable ens;
INameWrapper immutable nameWrapper;
address immutable trustedETHController;
address immutable trustedReverseRegistrar;
/// A mapping of operators. An address that is authorised for an address
/// may make any changes to the name that the owner could, but may not update
/// the set of authorisations.
/// (owner, operator) => approved
mapping(address => mapping(address => bool)) private _operatorApprovals;
/// A mapping of delegates. A delegate that is authorised by an owner
/// for a name may make changes to the name's resolver, but may not update
/// the set of token approvals.
/// (owner, name, delegate) => approved
mapping(address => mapping(bytes32 => mapping(address => bool)))
private _tokenApprovals;
// Logged when an operator is added or removed.
event ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
);
// Logged when a delegate is approved or an approval is revoked.
event Approved(
address owner,
bytes32 indexed node,
address indexed delegate,
bool indexed approved
);
constructor(
ENS _ens,
INameWrapper wrapperAddress,
address _trustedETHController,
address _trustedReverseRegistrar
) ReverseClaimer(_ens, msg.sender) {
ens = _ens;
nameWrapper = wrapperAddress;
trustedETHController = _trustedETHController;
trustedReverseRegistrar = _trustedReverseRegistrar;
}
/// @dev See {IERC1155-setApprovalForAll}.
function setApprovalForAll(address operator, bool approved) external {
require(
msg.sender != operator,
"ERC1155: setting approval status for self"
);
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
/// @dev See {IERC1155-isApprovedForAll}.
function isApprovedForAll(
address account,
address operator
) public view returns (bool) {
return _operatorApprovals[account][operator];
}
/// @dev Approve a delegate to be able to updated records on a node.
function approve(bytes32 node, address delegate, bool approved) external {
require(msg.sender != delegate, "Setting delegate status for self");
_tokenApprovals[msg.sender][node][delegate] = approved;
emit Approved(msg.sender, node, delegate, approved);
}
/// @dev Check to see if the delegate has been approved by the owner for the node.
function isApprovedFor(
address owner,
bytes32 node,
address delegate
) public view returns (bool) {
return _tokenApprovals[owner][node][delegate];
}
function isAuthorised(bytes32 node) internal view override returns (bool) {
if (
msg.sender == trustedETHController ||
msg.sender == trustedReverseRegistrar
) {
return true;
}
address owner = ens.owner(node);
if (owner == address(nameWrapper)) {
owner = nameWrapper.ownerOf(uint256(node));
}
return
owner == msg.sender ||
isApprovedForAll(owner, msg.sender) ||
isApprovedFor(owner, node, msg.sender);
}
function supportsInterface(
bytes4 interfaceID
)
public
view
override(
Multicallable,
ABIResolver,
AddrResolver,
ContentHashResolver,
DNSResolver,
InterfaceResolver,
NameResolver,
PubkeyResolver,
TextResolver
)
returns (bool)
{
return super.supportsInterface(interfaceID);
}
}
// ---- resolvers/Resolver.sol ----
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "./profiles/IABIResolver.sol";
import "./profiles/IAddressResolver.sol";
import "./profiles/IAddrResolver.sol";
import "./profiles/IContentHashResolver.sol";
import "./profiles/IDNSRecordResolver.sol";
import "./profiles/IDNSZoneResolver.sol";
import "./profiles/IInterfaceResolver.sol";
import "./profiles/INameResolver.sol";
import "./profiles/IPubkeyResolver.sol";
import "./profiles/ITextResolver.sol";
import "./profiles/IExtendedResolver.sol";
/// A generic resolver interface which includes all the functions including the ones deprecated
interface Resolver is
IERC165,
IABIResolver,
IAddressResolver,
IAddrResolver,
IContentHashResolver,
IDNSRecordResolver,
IDNSZoneResolver,
IInterfaceResolver,
INameResolver,
IPubkeyResolver,
ITextResolver,
IExtendedResolver
{
/* Deprecated events */
event ContentChanged(bytes32 indexed node, bytes32 hash);
function setApprovalForAll(address, bool) external;
function approve(bytes32 node, address delegate, bool approved) external;
function isApprovedForAll(address account, address operator) external;
function isApprovedFor(
address owner,
bytes32 node,
address delegate
) external;
function setABI(
bytes32 node,
uint256 contentType,
bytes calldata data
) external;
function setAddr(bytes32 node, address addr) external;
function setAddr(bytes32 node, uint256 coinType, bytes calldata a) external;
function setContenthash(bytes32 node, bytes calldata hash) external;
function setDnsrr(bytes32 node, bytes calldata data) external;
function setName(bytes32 node, string calldata _name) external;
function setPubkey(bytes32 node, bytes32 x, bytes32 y) external;
function setText(
bytes32 node,
string calldata key,
string calldata value
) external;
function setInterface(
bytes32 node,
bytes4 interfaceID,
address implementer
) external;
function multicall(
bytes[] calldata data
) external returns (bytes[] memory results);
function multicallWithNodeCheck(
bytes32 nodehash,
bytes[] calldata data
) external returns (bytes[] memory results);
/* Deprecated functions */
function content(bytes32 node) external view returns (bytes32);
function multihash(bytes32 node) external view returns (bytes memory);
function setContent(bytes32 node, bytes32 hash) external;
function setMultihash(bytes32 node, bytes calldata hash) external;
}
// ---- resolvers/Multicallable.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "./IMulticallable.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
abstract contract Multicallable is IMulticallable, ERC165 {
function _multicall(
bytes32 nodehash,
bytes[] calldata data
) internal returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
if (nodehash != bytes32(0)) {
bytes32 txNamehash = bytes32(data[i][4:36]);
require(
txNamehash == nodehash,
"multicall: All records must have a matching namehash"
);
}
(bool success, bytes memory result) = address(this).delegatecall(
data[i]
);
require(success);
results[i] = result;
}
return results;
}
// This function provides an extra security check when called
// from priviledged contracts (such as EthRegistrarController)
// that can set records on behalf of the node owners
function multicallWithNodeCheck(
bytes32 nodehash,
bytes[] calldata data
) external returns (bytes[] memory results) {
return _multicall(nodehash, data);
}
function multicall(
bytes[] calldata data
) public override returns (bytes[] memory results) {
return _multicall(bytes32(0), data);
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
interfaceID == type(IMulticallable).interfaceId ||
super.supportsInterface(interfaceID);
}
}
// ---- resolvers/mocks/DummyNameWrapper.sol ----
pragma solidity ^0.8.4;
/// @dev Implements a dummy NameWrapper which returns the caller's address
contract DummyNameWrapper {
function ownerOf(uint256 /* id */) public view returns (address) {
return tx.origin;
}
}
// ---- ethregistrar/LinearPremiumPriceOracle.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "./SafeMath.sol";
import "./StablePriceOracle.sol";
contract LinearPremiumPriceOracle is StablePriceOracle {
using SafeMath for *;
uint256 immutable GRACE_PERIOD = 90 days;
uint256 public immutable initialPremium;
uint256 public immutable premiumDecreaseRate;
bytes4 private constant TIME_UNTIL_PREMIUM_ID =
bytes4(keccak256("timeUntilPremium(uint,uint"));
constructor(
AggregatorInterface _usdOracle,
uint256[] memory _rentPrices,
uint256 _initialPremium,
uint256 _premiumDecreaseRate
) public StablePriceOracle(_usdOracle, _rentPrices) {
initialPremium = _initialPremium;
premiumDecreaseRate = _premiumDecreaseRate;
}
function _premium(
string memory name,
uint256 expires,
uint256 /*duration*/
) internal view override returns (uint256) {
expires = expires.add(GRACE_PERIOD);
if (expires > block.timestamp) {
// No premium for renewals
return 0;
}
// Calculate the discount off the maximum premium
uint256 discount = premiumDecreaseRate.mul(
block.timestamp.sub(expires)
);
// If we've run out the premium period, return 0.
if (discount > initialPremium) {
return 0;
}
return initialPremium - discount;
}
/// @dev Returns the timestamp at which a name with the specified expiry date will have
/// the specified re-registration price premium.
/// @param expires The timestamp at which the name expires.
/// @param amount The amount, in wei, the caller is willing to pay
/// @return The timestamp at which the premium for this domain will be `amount`.
function timeUntilPremium(
uint256 expires,
uint256 amount
) external view returns (uint256) {
amount = weiToAttoUSD(amount);
require(amount <= initialPremium);
expires = expires.add(GRACE_PERIOD);
uint256 discount = initialPremium.sub(amount);
uint256 duration = discount.div(premiumDecreaseRate);
return expires.add(duration);
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return
(interfaceID == TIME_UNTIL_PREMIUM_ID) ||
super.supportsInterface(interfaceID);
}
}
// ---- ethregistrar/BulkRenewal.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "../registry/ENS.sol";
import "./ETHRegistrarController.sol";
import "./IETHRegistrarController.sol";
import "../resolvers/Resolver.sol";
import "./IBulkRenewal.sol";
import "./IPriceOracle.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
contract BulkRenewal is IBulkRenewal {
bytes32 private constant ETH_NAMEHASH =
0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae;
ENS public immutable ens;
constructor(ENS _ens) {
ens = _ens;
}
function getController() internal view returns (ETHRegistrarController) {
Resolver r = Resolver(ens.resolver(ETH_NAMEHASH));
return
ETHRegistrarController(
r.interfaceImplementer(
ETH_NAMEHASH,
type(IETHRegistrarController).interfaceId
)
);
}
function rentPrice(
string[] calldata names,
uint256 duration
) external view override returns (uint256 total) {
ETHRegistrarController controller = getController();
uint256 length = names.length;
for (uint256 i = 0; i < length; ) {
IPriceOracle.Price memory price = controller.rentPrice(
names[i],
duration
);
unchecked {
++i;
total += (price.base + price.premium);
}
}
}
function renewAll(
string[] calldata names,
uint256 duration
) external payable override {
ETHRegistrarController controller = getController();
uint256 length = names.length;
uint256 total;
for (uint256 i = 0; i < length; ) {
IPriceOracle.Price memory price = controller.rentPrice(
names[i],
duration
);
uint256 totalPrice = price.base + price.premium;
controller.renew{value: totalPrice}(names[i], duration);
unchecked {
++i;
total += totalPrice;
}
}
// Send any excess funds back
payable(msg.sender).transfer(address(this).balance);
}
function supportsInterface(
bytes4 interfaceID
) external pure returns (bool) {
return
interfaceID == type(IERC165).interfaceId ||
interfaceID == type(IBulkRenewal).interfaceId;
}
}
// ---- ethregistrar/IBaseRegistrar.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "../registry/ENS.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
interface IBaseRegistrar is IERC721 {
event ControllerAdded(address indexed controller);
event ControllerRemoved(address indexed controller);
event NameMigrated(
uint256 indexed id,
address indexed owner,
uint256 expires
);
event NameRegistered(
uint256 indexed id,
address indexed owner,
uint256 expires
);
event NameRenewed(uint256 indexed id, uint256 expires);
// Authorises a controller, who can register and renew domains.
function addController(address controller) external;
// Revoke controller permission for an address.
function removeController(address controller) external;
// Set the resolver for the TLD this registrar manages.
function setResolver(address resolver) external;
// Returns the expiration timestamp of the specified label hash.
function nameExpires(uint256 id) external view returns (uint256);
// Returns true if the specified name is available for registration.
function available(uint256 id) external view returns (bool);
/// @dev Register a name.
function register(
uint256 id,
address owner,
uint256 duration
) external returns (uint256);
function renew(uint256 id, uint256 duration) external returns (uint256);
/// @dev Reclaim ownership of a name in ENS, if you own it in the registrar.
function reclaim(uint256 id, address owner) external;
}
// ---- ethregistrar/ILinearPremiumPriceOracle.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
interface ILinearPremiumPriceOracle {
function timeUntilPremium(
uint256 expires,
uint256 amount
) external view returns (uint256);
}
// ---- ethregistrar/TestResolver.sol ----
pragma solidity >=0.8.4;
/// @dev A test resolver implementation
contract TestResolver {
mapping(bytes32 => address) addresses;
constructor() public {}
function supportsInterface(bytes4 interfaceID) public pure returns (bool) {
return interfaceID == 0x01ffc9a7 || interfaceID == 0x3b3b57de;
}
function addr(bytes32 node) public view returns (address) {
return addresses[node];
}
function setAddr(bytes32 node, address addr) public {
addresses[node] = addr;
}
}
// ---- ethregistrar/IBulkRenewal.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
interface IBulkRenewal {
function rentPrice(
string[] calldata names,
uint256 duration
) external view returns (uint256 total);
function renewAll(
string[] calldata names,
uint256 duration
) external payable;
}
// ---- ethregistrar/StablePriceOracle.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "./IPriceOracle.sol";
import "../utils/StringUtils.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
interface AggregatorInterface {
function latestAnswer() external view returns (int256);
}
// StablePriceOracle sets a price in USD, based on an oracle.
contract StablePriceOracle is IPriceOracle {
using StringUtils for *;
// Rent in base price units by length
uint256 public immutable price1Letter;
uint256 public immutable price2Letter;
uint256 public immutable price3Letter;
uint256 public immutable price4Letter;
uint256 public immutable price5Letter;
// Oracle address
AggregatorInterface public immutable usdOracle;
event RentPriceChanged(uint256[] prices);
constructor(AggregatorInterface _usdOracle, uint256[] memory _rentPrices) {
usdOracle = _usdOracle;
price1Letter = _rentPrices[0];
price2Letter = _rentPrices[1];
price3Letter = _rentPrices[2];
price4Letter = _rentPrices[3];
price5Letter = _rentPrices[4];
}
function price(
string calldata name,
uint256 expires,
uint256 duration
) external view override returns (IPriceOracle.Price memory) {
uint256 len = name.strlen();
uint256 basePrice;
if (len >= 5) {
basePrice = price5Letter * duration;
} else if (len == 4) {
basePrice = price4Letter * duration;
} else if (len == 3) {
basePrice = price3Letter * duration;
} else if (len == 2) {
basePrice = price2Letter * duration;
} else {
basePrice = price1Letter * duration;
}
return
IPriceOracle.Price({
base: attoUSDToWei(basePrice),
premium: attoUSDToWei(_premium(name, expires, duration))
});
}
/// @dev Returns the pricing premium in wei.
function premium(
string calldata name,
uint256 expires,
uint256 duration
) external view returns (uint256) {
return attoUSDToWei(_premium(name, expires, duration));
}
/// @dev Returns the pricing premium in internal base units.
function _premium(
string memory name,
uint256 expires,
uint256 duration
) internal view virtual returns (uint256) {
return 0;
}
function attoUSDToWei(uint256 amount) internal view returns (uint256) {
uint256 ethPrice = uint256(usdOracle.latestAnswer());
return (amount * 1e8) / ethPrice;
}
function weiToAttoUSD(uint256 amount) internal view returns (uint256) {
uint256 ethPrice = uint256(usdOracle.latestAnswer());
return (amount * ethPrice) / 1e8;
}
function supportsInterface(
bytes4 interfaceID
) public view virtual returns (bool) {
return
interfaceID == type(IERC165).interfaceId ||
interfaceID == type(IPriceOracle).interfaceId;
}
}
// ---- ethregistrar/IETHRegistrarController.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "./IPriceOracle.sol";
interface IETHRegistrarController {
function rentPrice(
string memory,
uint256
) external view returns (IPriceOracle.Price memory);
function available(string memory) external returns (bool);
function makeCommitment(
string memory,
address,
uint256,
bytes32,
address,
bytes[] calldata,
bool,
uint16
) external pure returns (bytes32);
function commit(bytes32) external;
function register(
string calldata,
address,
uint256,
bytes32,
address,
bytes[] calldata,
bool,
uint16
) external payable;
function renew(string calldata, uint256) external payable;
}
// ---- ethregistrar/BaseRegistrarImplementation.sol ----
pragma solidity >=0.8.4;
import "../registry/ENS.sol";
import "./IBaseRegistrar.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract BaseRegistrarImplementation is ERC721, IBaseRegistrar, Ownable {
// A map of expiry times
mapping(uint256 => uint256) expiries;
// The ENS registry
ENS public ens;
// The namehash of the TLD this registrar owns (eg, .eth)
bytes32 public baseNode;
// A map of addresses that are authorised to register and renew names.
mapping(address => bool) public controllers;
uint256 public constant GRACE_PERIOD = 90 days;
bytes4 private constant INTERFACE_META_ID =
bytes4(keccak256("supportsInterface(bytes4)"));
bytes4 private constant ERC721_ID =
bytes4(
keccak256("balanceOf(address)") ^
keccak256("ownerOf(uint256)") ^
keccak256("approve(address,uint256)") ^
keccak256("getApproved(uint256)") ^
keccak256("setApprovalForAll(address,bool)") ^
keccak256("isApprovedForAll(address,address)") ^
keccak256("transferFrom(address,address,uint256)") ^
keccak256("safeTransferFrom(address,address,uint256)") ^
keccak256("safeTransferFrom(address,address,uint256,bytes)")
);
bytes4 private constant RECLAIM_ID =
bytes4(keccak256("reclaim(uint256,address)"));
/// v2.1.3 version of _isApprovedOrOwner which calls ownerOf(tokenId) and takes grace period into consideration instead of ERC721.ownerOf(tokenId);
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v2.1.3/contracts/token/ERC721/ERC721.sol#L187
/// @dev Returns whether the given spender can transfer a given token ID
/// @param spender address of the spender to query
/// @param tokenId uint256 ID of the token to be transferred
/// @return bool whether the msg.sender is approved for the given token ID,
/// is an operator of the owner, or is the owner of the token
function _isApprovedOrOwner(
address spender,
uint256 tokenId
) internal view override returns (bool) {
address owner = ownerOf(tokenId);
return (spender == owner ||
getApproved(tokenId) == spender ||
isApprovedForAll(owner, spender));
}
constructor(ENS _ens, bytes32 _baseNode) ERC721("", "") {
ens = _ens;
baseNode = _baseNode;
}
modifier live() {
require(ens.owner(baseNode) == address(this));
_;
}
modifier onlyController() {
require(controllers[msg.sender]);
_;
}
/// @dev Gets the owner of the specified token ID. Names become unowned
/// when their registration expires.
/// @param tokenId uint256 ID of the token to query the owner of
/// @return address currently marked as the owner of the given token ID
function ownerOf(
uint256 tokenId
) public view override(IERC721, ERC721) returns (address) {
require(expiries[tokenId] > block.timestamp);
return super.ownerOf(tokenId);
}
// Authorises a controller, who can register and renew domains.
function addController(address controller) external override onlyOwner {
controllers[controller] = true;
emit ControllerAdded(controller);
}
// Revoke controller permission for an address.
function removeController(address controller) external override onlyOwner {
controllers[controller] = false;
emit ControllerRemoved(controller);
}
// Set the resolver for the TLD this registrar manages.
function setResolver(address resolver) external override onlyOwner {
ens.setResolver(baseNode, resolver);
}
// Returns the expiration timestamp of the specified id.
function nameExpires(uint256 id) external view override returns (uint256) {
return expiries[id];
}
// Returns true iff the specified name is available for registration.
function available(uint256 id) public view override returns (bool) {
// Not available if it's registered here or in its grace period.
return expiries[id] + GRACE_PERIOD < block.timestamp;
}
/// @dev Register a name.
/// @param id The token ID (keccak256 of the label).
/// @param owner The address that should own the registration.
/// @param duration Duration in seconds for the registration.
function register(
uint256 id,
address owner,
uint256 duration
) external override returns (uint256) {
return _register(id, owner, duration, true);
}
/// @dev Register a name, without modifying the registry.
/// @param id The token ID (keccak256 of the label).
/// @param owner The address that should own the registration.
/// @param duration Duration in seconds for the registration.
function registerOnly(
uint256 id,
address owner,
uint256 duration
) external returns (uint256) {
return _register(id, owner, duration, false);
}
function _register(
uint256 id,
address owner,
uint256 duration,
bool updateRegistry
) internal live onlyController returns (uint256) {
require(available(id));
require(
block.timestamp + duration + GRACE_PERIOD >
block.timestamp + GRACE_PERIOD
); // Prevent future overflow
expiries[id] = block.timestamp + duration;
if (_exists(id)) {
// Name was previously owned, and expired
_burn(id);
}
_mint(owner, id);
if (updateRegistry) {
ens.setSubnodeOwner(baseNode, bytes32(id), owner);
}
emit NameRegistered(id, owner, block.timestamp + duration);
return block.timestamp + duration;
}
function renew(
uint256 id,
uint256 duration
) external override live onlyController returns (uint256) {
require(expiries[id] + GRACE_PERIOD >= block.timestamp); // Name must be registered here or in grace period
require(
expiries[id] + duration + GRACE_PERIOD > duration + GRACE_PERIOD
); // Prevent future overflow
expiries[id] += duration;
emit NameRenewed(id, expiries[id]);
return expiries[id];
}
/// @dev Reclaim ownership of a name in ENS, if you own it in the registrar.
function reclaim(uint256 id, address owner) external override live {
require(_isApprovedOrOwner(msg.sender, id));
ens.setSubnodeOwner(baseNode, bytes32(id), owner);
}
function supportsInterface(
bytes4 interfaceID
) public view override(ERC721, IERC165) returns (bool) {
return
interfaceID == INTERFACE_META_ID ||
interfaceID == ERC721_ID ||
interfaceID == RECLAIM_ID;
}
}
// ---- ethregistrar/DummyOracle.sol ----
pragma solidity >=0.8.4;
contract DummyOracle {
int256 value;
constructor(int256 _value) public {
set(_value);
}
function set(int256 _value) public {
value = _value;
}
function latestAnswer() public view returns (int256) {
return value;
}
}
// ---- ethregistrar/SafeMath.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
/// @title SafeMath
/// @dev Unsigned math operations with safety checks that revert on error
library SafeMath {
/// @dev Multiplies two unsigned integers, reverts on overflow.
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b);
return c;
}
/// @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero.
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// Solidity only automatically asserts when dividing by 0
require(b > 0);
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/// @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend).
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a);
uint256 c = a - b;
return c;
}
/// @dev Adds two unsigned integers, reverts on overflow.
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a);
return c;
}
/// @dev Divides two unsigned integers and returns the remainder (unsigned integer modulo),
/// reverts when dividing by zero.
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0);
return a % b;
}
}
// ---- ethregistrar/ETHRegistrarController.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import {BaseRegistrarImplementation} from "./BaseRegistrarImplementation.sol";
import {StringUtils} from "../utils/StringUtils.sol";
import {Resolver} from "../resolvers/Resolver.sol";
import {ENS} from "../registry/ENS.sol";
import {ReverseRegistrar} from "../reverseRegistrar/ReverseRegistrar.sol";
import {ReverseClaimer} from "../reverseRegistrar/ReverseClaimer.sol";
import {IETHRegistrarController, IPriceOracle} from "./IETHRegistrarController.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {INameWrapper} from "../wrapper/INameWrapper.sol";
import {ERC20Recoverable} from "../utils/ERC20Recoverable.sol";
error CommitmentTooNew(bytes32 commitment);
error CommitmentTooOld(bytes32 commitment);
error NameNotAvailable(string name);
error DurationTooShort(uint256 duration);
error ResolverRequiredWhenDataSupplied();
error UnexpiredCommitmentExists(bytes32 commitment);
error InsufficientValue();
error Unauthorised(bytes32 node);
error MaxCommitmentAgeTooLow();
error MaxCommitmentAgeTooHigh();
/// @dev A registrar controller for registering and renewing names at fixed cost.
contract ETHRegistrarController is
Ownable,
IETHRegistrarController,
IERC165,
ERC20Recoverable,
ReverseClaimer
{
using StringUtils for *;
using Address for address;
uint256 public constant MIN_REGISTRATION_DURATION = 28 days;
bytes32 private constant ETH_NODE =
0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae;
uint64 private constant MAX_EXPIRY = type(uint64).max;
BaseRegistrarImplementation immutable base;
IPriceOracle public immutable prices;
uint256 public immutable minCommitmentAge;
uint256 public immutable maxCommitmentAge;
ReverseRegistrar public immutable reverseRegistrar;
INameWrapper public immutable nameWrapper;
mapping(bytes32 => uint256) public commitments;
event NameRegistered(
string name,
bytes32 indexed label,
address indexed owner,
uint256 baseCost,
uint256 premium,
uint256 expires
);
event NameRenewed(
string name,
bytes32 indexed label,
uint256 cost,
uint256 expires
);
constructor(
BaseRegistrarImplementation _base,
IPriceOracle _prices,
uint256 _minCommitmentAge,
uint256 _maxCommitmentAge,
ReverseRegistrar _reverseRegistrar,
INameWrapper _nameWrapper,
ENS _ens
) ReverseClaimer(_ens, msg.sender) {
if (_maxCommitmentAge <= _minCommitmentAge) {
revert MaxCommitmentAgeTooLow();
}
if (_maxCommitmentAge > block.timestamp) {
revert MaxCommitmentAgeTooHigh();
}
base = _base;
prices = _prices;
minCommitmentAge = _minCommitmentAge;
maxCommitmentAge = _maxCommitmentAge;
reverseRegistrar = _reverseRegistrar;
nameWrapper = _nameWrapper;
}
function rentPrice(
string memory name,
uint256 duration
) public view override returns (IPriceOracle.Price memory price) {
bytes32 label = keccak256(bytes(name));
price = prices.price(name, base.nameExpires(uint256(label)), duration);
}
function valid(string memory name) public pure returns (bool) {
return name.strlen() >= 3;
}
function available(string memory name) public view override returns (bool) {
bytes32 label = keccak256(bytes(name));
return valid(name) && base.available(uint256(label));
}
function makeCommitment(
string memory name,
address owner,
uint256 duration,
bytes32 secret,
address resolver,
bytes[] calldata data,
bool reverseRecord,
uint16 ownerControlledFuses
) public pure override returns (bytes32) {
bytes32 label = keccak256(bytes(name));
if (data.length > 0 && resolver == address(0)) {
revert ResolverRequiredWhenDataSupplied();
}
return
keccak256(
abi.encode(
label,
owner,
duration,
secret,
resolver,
data,
reverseRecord,
ownerControlledFuses
)
);
}
function commit(bytes32 commitment) public override {
if (commitments[commitment] + maxCommitmentAge >= block.timestamp) {
revert UnexpiredCommitmentExists(commitment);
}
commitments[commitment] = block.timestamp;
}
function register(
string calldata name,
address owner,
uint256 duration,
bytes32 secret,
address resolver,
bytes[] calldata data,
bool reverseRecord,
uint16 ownerControlledFuses
) public payable override {
IPriceOracle.Price memory price = rentPrice(name, duration);
if (msg.value < price.base + price.premium) {
revert InsufficientValue();
}
_consumeCommitment(
name,
duration,
makeCommitment(
name,
owner,
duration,
secret,
resolver,
data,
reverseRecord,
ownerControlledFuses
)
);
uint256 expires = nameWrapper.registerAndWrapETH2LD(
name,
owner,
duration,
resolver,
ownerControlledFuses
);
if (data.length > 0) {
_setRecords(resolver, keccak256(bytes(name)), data);
}
if (reverseRecord) {
_setReverseRecord(name, resolver, msg.sender);
}
emit NameRegistered(
name,
keccak256(bytes(name)),
owner,
price.base,
price.premium,
expires
);
if (msg.value > (price.base + price.premium)) {
payable(msg.sender).transfer(
msg.value - (price.base + price.premium)
);
}
}
function renew(
string calldata name,
uint256 duration
) external payable override {
bytes32 labelhash = keccak256(bytes(name));
uint256 tokenId = uint256(labelhash);
IPriceOracle.Price memory price = rentPrice(name, duration);
if (msg.value < price.base) {
revert InsufficientValue();
}
uint256 expires = nameWrapper.renew(tokenId, duration);
if (msg.value > price.base) {
payable(msg.sender).transfer(msg.value - price.base);
}
emit NameRenewed(name, labelhash, msg.value, expires);
}
function withdraw() public {
payable(owner()).transfer(address(this).balance);
}
function supportsInterface(
bytes4 interfaceID
) external pure returns (bool) {
return
interfaceID == type(IERC165).interfaceId ||
interfaceID == type(IETHRegistrarController).interfaceId;
}
/* Internal functions */
function _consumeCommitment(
string memory name,
uint256 duration,
bytes32 commitment
) internal {
// Require an old enough commitment.
if (commitments[commitment] + minCommitmentAge > block.timestamp) {
revert CommitmentTooNew(commitment);
}
// If the commitment is too old, or the name is registered, stop
if (commitments[commitment] + maxCommitmentAge <= block.timestamp) {
revert CommitmentTooOld(commitment);
}
if (!available(name)) {
revert NameNotAvailable(name);
}
delete (commitments[commitment]);
if (duration < MIN_REGISTRATION_DURATION) {
revert DurationTooShort(duration);
}
}
function _setRecords(
address resolverAddress,
bytes32 label,
bytes[] calldata data
) internal {
// use hardcoded .eth namehash
bytes32 nodehash = keccak256(abi.encodePacked(ETH_NODE, label));
Resolver resolver = Resolver(resolverAddress);
resolver.multicallWithNodeCheck(nodehash, data);
}
function _setReverseRecord(
string memory name,
address resolver,
address owner
) internal {
reverseRegistrar.setNameForAddr(
msg.sender,
owner,
resolver,
string.concat(name, ".eth")
);
}
}
// ---- ethregistrar/IPriceOracle.sol ----
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.17 <0.9.0;
interface IPriceOracle {
struct Price {
uint256 base;
uint256 premium;
}
/// @dev Returns the price to register or renew a name.
/// @param name The name being registered or renewed.
/// @param expires When the name presently expires (0 if this is a new registration).
/// @param duration How long the name is being registered or extended for, in seconds.
/// @return base premium tuple of base price + premium price
function price(
string calldata name,
uint256 expires,
uint256 duration
) external view returns (Price calldata);
}
// ---- ethregistrar/ExponentialPremiumPriceOracle.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "./StablePriceOracle.sol";
contract ExponentialPremiumPriceOracle is StablePriceOracle {
uint256 constant GRACE_PERIOD = 90 days;
uint256 immutable startPremium;
uint256 immutable endValue;
constructor(
AggregatorInterface _usdOracle,
uint256[] memory _rentPrices,
uint256 _startPremium,
uint256 totalDays
) StablePriceOracle(_usdOracle, _rentPrices) {
startPremium = _startPremium;
endValue = _startPremium >> totalDays;
}
uint256 constant PRECISION = 1e18;
uint256 constant bit1 = 999989423469314432; // 0.5 ^ 1/65536 * (10 ** 18)
uint256 constant bit2 = 999978847050491904; // 0.5 ^ 2/65536 * (10 ** 18)
uint256 constant bit3 = 999957694548431104;
uint256 constant bit4 = 999915390886613504;
uint256 constant bit5 = 999830788931929088;
uint256 constant bit6 = 999661606496243712;
uint256 constant bit7 = 999323327502650752;
uint256 constant bit8 = 998647112890970240;
uint256 constant bit9 = 997296056085470080;
uint256 constant bit10 = 994599423483633152;
uint256 constant bit11 = 989228013193975424;
uint256 constant bit12 = 978572062087700096;
uint256 constant bit13 = 957603280698573696;
uint256 constant bit14 = 917004043204671232;
uint256 constant bit15 = 840896415253714560;
uint256 constant bit16 = 707106781186547584;
/// @dev Returns the pricing premium in internal base units.
function _premium(
string memory,
uint256 expires,
uint256
) internal view override returns (uint256) {
expires = expires + GRACE_PERIOD;
if (expires > block.timestamp) {
return 0;
}
uint256 elapsed = block.timestamp - expires;
uint256 premium = decayedPremium(startPremium, elapsed);
if (premium >= endValue) {
return premium - endValue;
}
return 0;
}
/// @dev Returns the premium price at current time elapsed
/// @param startPremium starting price
/// @param elapsed time past since expiry
function decayedPremium(
uint256 startPremium,
uint256 elapsed
) public pure returns (uint256) {
uint256 daysPast = (elapsed * PRECISION) / 1 days;
uint256 intDays = daysPast / PRECISION;
uint256 premium = startPremium >> intDays;
uint256 partDay = (daysPast - intDays * PRECISION);
uint256 fraction = (partDay * (2 ** 16)) / PRECISION;
uint256 totalPremium = addFractionalPremium(fraction, premium);
return totalPremium;
}
function addFractionalPremium(
uint256 fraction,
uint256 premium
) internal pure returns (uint256) {
if (fraction & (1 << 0) != 0) {
premium = (premium * bit1) / PRECISION;
}
if (fraction & (1 << 1) != 0) {
premium = (premium * bit2) / PRECISION;
}
if (fraction & (1 << 2) != 0) {
premium = (premium * bit3) / PRECISION;
}
if (fraction & (1 << 3) != 0) {
premium = (premium * bit4) / PRECISION;
}
if (fraction & (1 << 4) != 0) {
premium = (premium * bit5) / PRECISION;
}
if (fraction & (1 << 5) != 0) {
premium = (premium * bit6) / PRECISION;
}
if (fraction & (1 << 6) != 0) {
premium = (premium * bit7) / PRECISION;
}
if (fraction & (1 << 7) != 0) {
premium = (premium * bit8) / PRECISION;
}
if (fraction & (1 << 8) != 0) {
premium = (premium * bit9) / PRECISION;
}
if (fraction & (1 << 9) != 0) {
premium = (premium * bit10) / PRECISION;
}
if (fraction & (1 << 10) != 0) {
premium = (premium * bit11) / PRECISION;
}
if (fraction & (1 << 11) != 0) {
premium = (premium * bit12) / PRECISION;
}
if (fraction & (1 << 12) != 0) {
premium = (premium * bit13) / PRECISION;
}
if (fraction & (1 << 13) != 0) {
premium = (premium * bit14) / PRECISION;
}
if (fraction & (1 << 14) != 0) {
premium = (premium * bit15) / PRECISION;
}
if (fraction & (1 << 15) != 0) {
premium = (premium * bit16) / PRECISION;
}
return premium;
}
function supportsInterface(
bytes4 interfaceID
) public view virtual override returns (bool) {
return super.supportsInterface(interfaceID);
}
}
// ---- ethregistrar/mocks/DummyProxyRegistry.sol ----
pragma solidity >=0.8.4;
contract DummyProxyRegistry {
address target;
constructor(address _target) public {
target = _target;
}
function proxies(address a) external view returns (address) {
return target;
}
}
// ---- ethregistrar/StaticBulkRenewal.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "./ETHRegistrarController.sol";
import "./IBulkRenewal.sol";
import "./IPriceOracle.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
contract StaticBulkRenewal is IBulkRenewal {
ETHRegistrarController controller;
constructor(ETHRegistrarController _controller) {
controller = _controller;
}
function rentPrice(
string[] calldata names,
uint256 duration
) external view override returns (uint256 total) {
uint256 length = names.length;
for (uint256 i = 0; i < length; ) {
IPriceOracle.Price memory price = controller.rentPrice(
names[i],
duration
);
unchecked {
++i;
total += (price.base + price.premium);
}
}
}
function renewAll(
string[] calldata names,
uint256 duration
) external payable override {
uint256 length = names.length;
uint256 total;
for (uint256 i = 0; i < length; ) {
IPriceOracle.Price memory price = controller.rentPrice(
names[i],
duration
);
uint256 totalPrice = price.base + price.premium;
controller.renew{value: totalPrice}(names[i], duration);
unchecked {
++i;
total += totalPrice;
}
}
// Send any excess funds back
payable(msg.sender).transfer(address(this).balance);
}
function supportsInterface(
bytes4 interfaceID
) external pure returns (bool) {
return
interfaceID == type(IERC165).interfaceId ||
interfaceID == type(IBulkRenewal).interfaceId;
}
}
// ---- utils/DummyRevertResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.17 <0.9.0;
contract DummyRevertResolver {
function resolve(
bytes calldata,
bytes calldata
) external pure returns (bytes memory) {
revert("Not Supported");
}
function supportsInterface(bytes4) external pure returns (bool) {
return true;
}
}
// ---- utils/NameCoder.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {HexUtils} from "../utils/HexUtils.sol";
/// @dev Library for encoding/decoding names.
///
/// An ENS name is stop-separated labels, eg. "aaa.bb.c".
///
/// A DNS-encoded name is composed of byte length-prefixed labels with a terminator byte.
/// eg. "\x03aaa\x02bb\x01c\x00".
/// - maximum label length is 255 bytes.
/// - length = 0 is reserved for the terminator (root).
///
/// To encode a label larger than 255 bytes, use a hashed label.
/// A label of any length can be converted to a hashed label.
///
/// A hashed label is encoded as "[" + toHex(keccak256(label)) + "]".
/// eg. [af2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc] = "vitalik".
/// - always 66 bytes.
/// - matches: `/^\[[0-9a-f]{64}\]$/`.
///
/// w/o hashed labels: `dns.length == 2 + ens.length` and the mapping is injective.
/// w/ hashed labels: `dns.length == 2 + ens.split('.').map(x => x.utf8Length).sum(n => n > 255 ? 66 : n)`.
library NameCoder {
/// @dev The DNS-encoded name is malformed.
error DNSDecodingFailed(bytes dns);
/// @dev A label of the ENS name has an invalid size.
error DNSEncodingFailed(string ens);
/// @dev Same as `BytesUtils.readLabel()` but supports hashed labels.
/// Only the last labelHash is zero.
/// Disallows hashed label of zero (eg. `[0..0]`) to prevent confusion with terminator.
/// Reverts `DNSDecodingFailed`.
/// @param name The DNS-encoded name.
/// @param idx The offset into `name` to start reading.
/// @return labelHash The resulting labelhash.
/// @return newIdx The offset into `name` of the next label.
function readLabel(
bytes memory name,
uint256 idx
) internal pure returns (bytes32 labelHash, uint256 newIdx) {
if (idx >= name.length) revert DNSDecodingFailed(name); // "readLabel: expected length"
uint256 len = uint256(uint8(name[idx++]));
newIdx = idx + len;
if (newIdx > name.length) revert DNSDecodingFailed(name); // "readLabel: expected label"
if (len == 66 && name[idx] == "[" && name[newIdx - 1] == "]") {
bool valid;
(labelHash, valid) = HexUtils.hexStringToBytes32(
name,
idx + 1,
newIdx - 1
); // will not revert
if (!valid || labelHash == bytes32(0)) {
revert DNSDecodingFailed(name); // "readLabel: malformed" or null literal
}
} else if (len > 0) {
assembly {
labelHash := keccak256(add(add(name, idx), 32), len)
}
}
}
/// @dev Same as `BytesUtils.namehash()` but supports hashed labels.
/// Reverts `DNSDecodingFailed`.
/// @param name The DNS-encoded name.
/// @param idx The offset into name start hashing.
/// @return hash The resulting namehash.
function namehash(
bytes memory name,
uint256 idx
) internal pure returns (bytes32 hash) {
(hash, idx) = readLabel(name, idx);
if (hash == bytes32(0)) {
if (idx != name.length) revert DNSDecodingFailed(name); // "namehash: Junk at end of name"
} else {
bytes32 parent = namehash(name, idx);
assembly {
mstore(0, parent)
mstore(32, hash)
hash := keccak256(0, 64)
}
}
}
/// @dev Convert DNS-encoded name to ENS name.
/// Reverts `DNSDecodingFailed`.
/// @param dns The DNS-encoded name to convert, eg. `\x03aaa\x02bb\x01c\x00`.
/// @return ens The equivalent ENS name, eg. `aaa.bb.c`.
function decode(
bytes memory dns
) internal pure returns (string memory ens) {
unchecked {
uint256 n = dns.length;
if (n == 1 && dns[0] == 0) return ""; // only valid answer is root
if (n < 3) revert DNSDecodingFailed(dns);
bytes memory v = new bytes(n - 2); // always 2-shorter
uint256 src;
uint256 dst;
while (src < n) {
uint8 len = uint8(dns[src++]);
if (len == 0) break;
uint256 end = src + len;
if (end > dns.length) revert DNSDecodingFailed(dns); // overflow
if (dst > 0) v[dst++] = "."; // skip first stop
while (src < end) {
bytes1 x = dns[src++]; // read byte
if (x == ".") revert DNSDecodingFailed(dns); // malicious label
v[dst++] = x; // write byte
}
}
if (src != dns.length) revert DNSDecodingFailed(dns); // junk at end
return string(v);
}
}
/// @dev Convert ENS name to DNS-encoded name.
/// Hashes labels longer than 255 bytes.
/// Reverts `DNSEncodingFailed`.
/// @param ens The ENS name to convert, eg. `aaa.bb.c`.
/// @return dns The corresponding DNS-encoded name, eg. `\x03aaa\x02bb\x01c\x00`.
function encode(
string memory ens
) internal pure returns (bytes memory dns) {
unchecked {
uint256 n = bytes(ens).length;
if (n == 0) return hex"00"; // root
dns = new bytes(n + 2);
uint256 start;
assembly {
start := add(dns, 32) // first byte of output
}
uint256 end = start; // remember position to write length
for (uint256 i; i < n; i++) {
bytes1 x = bytes(ens)[i]; // read byte
if (x == ".") {
start = _createHashedLabel(start, end);
if (start == 0) revert DNSEncodingFailed(ens);
end = start; // jump to next position
} else {
assembly {
end := add(end, 1) // increase length
mstore(end, x) // write byte
}
}
}
start = _createHashedLabel(start, end);
if (start == 0) revert DNSEncodingFailed(ens);
assembly {
mstore8(start, 0) // terminal byte
mstore(dns, sub(start, add(dns, 31))) // truncate length
}
}
}
/// @dev Write the label length.
/// If longer than 255, writes a hashed label instead.
/// @param start The memory offset of the length-prefixed label.
/// @param end The memory offset at the end of the label.
/// @return next The memory offset for the next label.
/// Returns 0 if label is empty (handled by caller).
function _createHashedLabel(
uint256 start,
uint256 end
) internal pure returns (uint256 next) {
uint256 size = end - start; // length of label
if (size > 255) {
assembly {
mstore(0, keccak256(add(start, 1), size)) // compute hash of label
}
HexUtils.unsafeHex(0, start + 2, 64); // override label with hex(hash)
assembly {
mstore8(add(start, 1), 0x5B) // "["
mstore8(add(start, 66), 0x5D) // "]"
}
size = 66;
}
if (size > 0) {
assembly {
mstore8(start, size) // update length
}
next = start + 1 + size; // advance
}
}
}
// ---- utils/TestENSIP19.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {ENSIP19} from "./ENSIP19.sol";
contract TestENSIP19 {
function reverseName(
bytes memory encodedAddress,
uint256 coinType
) external pure returns (string memory) {
return ENSIP19.reverseName(encodedAddress, coinType);
}
function parse(
bytes memory name
) external pure returns (bytes memory, uint256) {
return ENSIP19.parse(name);
}
function chainFromCoinType(
uint256 coinType
) external pure returns (uint32) {
return ENSIP19.chainFromCoinType(coinType);
}
function isEVMCoinType(uint256 coinType) external pure returns (bool) {
return ENSIP19.isEVMCoinType(coinType);
}
}
// ---- utils/TestHexUtils.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import {HexUtils} from "./HexUtils.sol";
contract TestHexUtils {
function hexToBytes(
bytes calldata name,
uint256 pos,
uint256 end
) public pure returns (bytes memory, bool) {
return HexUtils.hexToBytes(name, pos, end);
}
function hexStringToBytes32(
bytes calldata name,
uint256 pos,
uint256 end
) public pure returns (bytes32, bool) {
return HexUtils.hexStringToBytes32(name, pos, end);
}
function hexToAddress(
bytes calldata input,
uint256 pos,
uint256 end
) public pure returns (address, bool) {
return HexUtils.hexToAddress(input, pos, end);
}
function addressToHex(
address addr
) external pure returns (string memory hexString) {
return HexUtils.addressToHex(addr);
}
function unpaddedUintToHex(
uint256 value,
bool dropZeroNibble
) external pure returns (string memory hexString) {
return HexUtils.unpaddedUintToHex(value, dropZeroNibble);
}
function bytesToHex(
bytes memory v
) external pure returns (string memory hexString) {
return HexUtils.bytesToHex(v);
}
}
// ---- utils/BytesUtils.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
library BytesUtils {
error OffsetOutOfBoundsError(uint256 offset, uint256 length);
/// @dev Returns the keccak-256 hash of a byte range.
/// @param self The byte string to hash.
/// @param offset The position to start hashing at.
/// @param len The number of bytes to hash.
/// @return ret The hash of the byte range.
function keccak(
bytes memory self,
uint256 offset,
uint256 len
) internal pure returns (bytes32 ret) {
require(offset + len <= self.length);
assembly {
ret := keccak256(add(add(self, 32), offset), len)
}
}
/// @dev Returns a positive number if `other` comes lexicographically after
/// `self`, a negative number if it comes before, or zero if the
/// contents of the two bytes are equal.
/// @param self The first bytes to compare.
/// @param other The second bytes to compare.
/// @return The result of the comparison.
function compare(
bytes memory self,
bytes memory other
) internal pure returns (int256) {
return compare(self, 0, self.length, other, 0, other.length);
}
/// @dev Returns a positive number if `other` comes lexicographically after
/// `self`, a negative number if it comes before, or zero if the
/// contents of the two bytes are equal. Comparison is done per-rune,
/// on unicode codepoints.
/// @param self The first bytes to compare.
/// @param offset The offset of self.
/// @param len The length of self.
/// @param other The second bytes to compare.
/// @param otheroffset The offset of the other string.
/// @param otherlen The length of the other string.
/// @return The result of the comparison.
function compare(
bytes memory self,
uint256 offset,
uint256 len,
bytes memory other,
uint256 otheroffset,
uint256 otherlen
) internal pure returns (int256) {
if (offset + len > self.length) {
revert OffsetOutOfBoundsError(offset + len, self.length);
}
if (otheroffset + otherlen > other.length) {
revert OffsetOutOfBoundsError(otheroffset + otherlen, other.length);
}
uint256 shortest = len;
if (otherlen < len) shortest = otherlen;
uint256 selfptr;
uint256 otherptr;
assembly {
selfptr := add(self, add(offset, 32))
otherptr := add(other, add(otheroffset, 32))
}
for (uint256 idx = 0; idx < shortest; idx += 32) {
uint256 a;
uint256 b;
assembly {
a := mload(selfptr)
b := mload(otherptr)
}
if (a != b) {
uint256 rest = shortest - idx;
if (rest < 32) {
// shift out the irrelevant bits
rest = (32 - rest) << 3; // bits to drop
a >>= rest;
b >>= rest;
}
if (a < b) {
return -1;
} else if (a > b) {
return 1;
}
}
selfptr += 32;
otherptr += 32;
}
return int256(len) - int256(otherlen);
}
/// @dev Returns true if the two byte ranges are equal.
/// @param self The first byte range to compare.
/// @param offset The offset into the first byte range.
/// @param other The second byte range to compare.
/// @param otherOffset The offset into the second byte range.
/// @param len The number of bytes to compare
/// @return True if the byte ranges are equal, false otherwise.
function equals(
bytes memory self,
uint256 offset,
bytes memory other,
uint256 otherOffset,
uint256 len
) internal pure returns (bool) {
return keccak(self, offset, len) == keccak(other, otherOffset, len);
}
/// @dev Returns true if the two byte ranges are equal with offsets.
/// @param self The first byte range to compare.
/// @param offset The offset into the first byte range.
/// @param other The second byte range to compare.
/// @param otherOffset The offset into the second byte range.
/// @return True if the byte ranges are equal, false otherwise.
function equals(
bytes memory self,
uint256 offset,
bytes memory other,
uint256 otherOffset
) internal pure returns (bool) {
return
keccak(self, offset, self.length - offset) ==
keccak(other, otherOffset, other.length - otherOffset);
}
/// @dev Compares a range of 'self' to all of 'other' and returns True iff
/// they are equal.
/// @param self The first byte range to compare.
/// @param offset The offset into the first byte range.
/// @param other The second byte range to compare.
/// @return True if the byte ranges are equal, false otherwise.
function equals(
bytes memory self,
uint256 offset,
bytes memory other
) internal pure returns (bool) {
return
self.length == offset + other.length &&
equals(self, offset, other, 0, other.length);
}
/// @dev Returns true if the two byte ranges are equal.
/// @param self The first byte range to compare.
/// @param other The second byte range to compare.
/// @return True if the byte ranges are equal, false otherwise.
function equals(
bytes memory self,
bytes memory other
) internal pure returns (bool) {
return
self.length == other.length &&
equals(self, 0, other, 0, self.length);
}
/// @dev Returns the 8-bit number at the specified index of self.
/// @param self The byte string.
/// @param idx The index into the bytes
/// @return ret The specified 8 bits of the string, interpreted as an integer.
function readUint8(
bytes memory self,
uint256 idx
) internal pure returns (uint8 ret) {
return uint8(self[idx]);
}
/// @dev Returns the 16-bit number at the specified index of self.
/// @param self The byte string.
/// @param idx The index into the bytes
/// @return ret The specified 16 bits of the string, interpreted as an integer.
function readUint16(
bytes memory self,
uint256 idx
) internal pure returns (uint16 ret) {
require(idx + 2 <= self.length);
assembly {
ret := and(mload(add(add(self, 2), idx)), 0xFFFF)
}
}
/// @dev Returns the 32-bit number at the specified index of self.
/// @param self The byte string.
/// @param idx The index into the bytes
/// @return ret The specified 32 bits of the string, interpreted as an integer.
function readUint32(
bytes memory self,
uint256 idx
) internal pure returns (uint32 ret) {
require(idx + 4 <= self.length);
assembly {
ret := and(mload(add(add(self, 4), idx)), 0xFFFFFFFF)
}
}
/// @dev Returns the 32 byte value at the specified index of self.
/// @param self The byte string.
/// @param idx The index into the bytes
/// @return ret The specified 32 bytes of the string.
function readBytes32(
bytes memory self,
uint256 idx
) internal pure returns (bytes32 ret) {
require(idx + 32 <= self.length);
assembly {
ret := mload(add(add(self, 32), idx))
}
}
/// @dev Returns the 32 byte value at the specified index of self.
/// @param self The byte string.
/// @param idx The index into the bytes
/// @return ret The specified 32 bytes of the string.
function readBytes20(
bytes memory self,
uint256 idx
) internal pure returns (bytes20 ret) {
require(idx + 20 <= self.length);
assembly {
ret := and(
mload(add(add(self, 32), idx)),
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000
)
}
}
/// @dev Returns the n byte value at the specified index of self.
/// @param self The byte string.
/// @param idx The index into the bytes.
/// @param len The number of bytes.
/// @return ret The specified 32 bytes of the string.
function readBytesN(
bytes memory self,
uint256 idx,
uint256 len
) internal pure returns (bytes32 ret) {
require(len <= 32);
require(idx + len <= self.length);
assembly {
let mask := not(sub(exp(256, sub(32, len)), 1))
ret := and(mload(add(add(self, 32), idx)), mask)
}
}
function memcpy(uint256 dest, uint256 src, uint256 len) private pure {
// Copy word-length chunks while possible
for (; len >= 32; len -= 32) {
assembly {
mstore(dest, mload(src))
}
dest += 32;
src += 32;
}
// Copy remaining bytes
unchecked {
uint256 mask = (256 ** (32 - len)) - 1;
assembly {
let srcpart := and(mload(src), not(mask))
let destpart := and(mload(dest), mask)
mstore(dest, or(destpart, srcpart))
}
}
}
/// @dev Copies a substring into a new byte string.
/// @param self The byte string to copy from.
/// @param offset The offset to start copying at.
/// @param len The number of bytes to copy.
function substring(
bytes memory self,
uint256 offset,
uint256 len
) internal pure returns (bytes memory) {
require(offset + len <= self.length);
bytes memory ret = new bytes(len);
uint256 dest;
uint256 src;
assembly {
dest := add(ret, 32)
src := add(add(self, 32), offset)
}
memcpy(dest, src, len);
return ret;
}
// Maps characters from 0x30 to 0x7A to their base32 values.
// 0xFF represents invalid characters in that range.
bytes constant base32HexTable =
hex"00010203040506070809FFFFFFFFFFFFFF0A0B0C0D0E0F101112131415161718191A1B1C1D1E1FFFFFFFFFFFFFFFFFFFFF0A0B0C0D0E0F101112131415161718191A1B1C1D1E1F";
/// @dev Decodes unpadded base32 data of up to one word in length.
/// @param self The data to decode.
/// @param off Offset into the string to start at.
/// @param len Number of characters to decode.
/// @return The decoded data, left aligned.
function base32HexDecodeWord(
bytes memory self,
uint256 off,
uint256 len
) internal pure returns (bytes32) {
require(len <= 52);
uint256 ret = 0;
uint8 decoded;
for (uint256 i = 0; i < len; i++) {
bytes1 char = self[off + i];
require(char >= 0x30 && char <= 0x7A);
decoded = uint8(base32HexTable[uint256(uint8(char)) - 0x30]);
require(decoded <= 0x20);
if (i == len - 1) {
break;
}
ret = (ret << 5) | decoded;
}
uint256 bitlen = len * 5;
if (len % 8 == 0) {
// Multiple of 8 characters, no padding
ret = (ret << 5) | decoded;
} else if (len % 8 == 2) {
// Two extra characters - 1 byte
ret = (ret << 3) | (decoded >> 2);
bitlen -= 2;
} else if (len % 8 == 4) {
// Four extra characters - 2 bytes
ret = (ret << 1) | (decoded >> 4);
bitlen -= 4;
} else if (len % 8 == 5) {
// Five extra characters - 3 bytes
ret = (ret << 4) | (decoded >> 1);
bitlen -= 1;
} else if (len % 8 == 7) {
// Seven extra characters - 4 bytes
ret = (ret << 2) | (decoded >> 3);
bitlen -= 3;
} else {
revert();
}
return bytes32(ret << (256 - bitlen));
}
/// @dev Finds the first occurrence of the byte `needle` in `self`.
/// @param self The string to search
/// @param off The offset to start searching at
/// @param len The number of bytes to search
/// @param needle The byte to search for
/// @return The offset of `needle` in `self`, or 2**256-1 if it was not found.
function find(
bytes memory self,
uint256 off,
uint256 len,
bytes1 needle
) internal pure returns (uint256) {
for (uint256 idx = off; idx < off + len; idx++) {
if (self[idx] == needle) {
return idx;
}
}
return type(uint256).max;
}
}
// ---- utils/DummyOldResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract DummyOldResolver {
function test() public returns (bool) {
return true;
}
function name(bytes32) public returns (string memory) {
return "test.eth";
}
}
// ---- utils/HexUtils.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
library HexUtils {
/// @dev Convert `hexString[pos:end]` to `bytes32`.
/// Accepts 0-64 hex-chars.
/// Uses right alignment: `1` → `0000000000000000000000000000000000000000000000000000000000000001`.
/// @param hexString The string to parse.
/// @param pos The index to start parsing.
/// @param end The (exclusive) index to stop parsing.
/// @return word The parsed bytes32.
/// @return valid True if the parse was successful.
function hexStringToBytes32(
bytes memory hexString,
uint256 pos,
uint256 end
) internal pure returns (bytes32 word, bool valid) {
uint256 nibbles = end - pos;
if (nibbles > 64 || end > hexString.length) {
return (bytes32(0), false); // too large or out of bounds
}
uint256 src;
assembly {
src := add(add(hexString, 32), pos)
}
valid = unsafeBytes(src, 0, nibbles);
assembly {
let pad := sub(32, shr(1, add(nibbles, 1))) // number of bytes
word := shr(shl(3, pad), mload(0)) // right align
}
}
/// @dev Convert `hexString[pos:end]` to `address`.
/// Accepts exactly 40 hex-chars.
/// @param hexString The string to parse.
/// @param pos The index to start parsing.
/// @param end The (exclusive) index to stop parsing.
/// @return addr The parsed address.
/// @return valid True if the parse was successful.
function hexToAddress(
bytes memory hexString,
uint256 pos,
uint256 end
) internal pure returns (address addr, bool valid) {
if (end - pos != 40) return (address(0), false); // wrong length
bytes32 word;
(word, valid) = hexStringToBytes32(hexString, pos, end);
addr = address(uint160(uint256(word)));
}
/// @dev Convert `hexString[pos:end]` to `bytes`.
/// Accepts 0+ hex-chars.
/// @param pos The index to start parsing.
/// @param end The (exclusive) index to stop parsing.
/// @return v The parsed bytes.
/// @return valid True if the parse was successful.
function hexToBytes(
bytes memory hexString,
uint256 pos,
uint256 end
) internal pure returns (bytes memory v, bool valid) {
uint256 nibbles = end - pos;
v = new bytes((1 + nibbles) >> 1); // round up
uint256 src;
uint256 dst;
assembly {
src := add(add(hexString, 32), pos)
dst := add(v, 32)
}
valid = unsafeBytes(src, dst, nibbles);
}
/// @dev Convert arbitrary hex-encoded memory to bytes.
/// If nibbles is odd, leading hex-char is padded, eg. `F` → `0x0F`.
/// Matches: /^[0-9a-f]*$/i.
/// @param src The memory offset of first hex-char of input.
/// @param dst The memory offset of first byte of output (cannot alias `src`).
/// @param nibbles The number of hex-chars to convert.
/// @return valid True if all characters were hex.
function unsafeBytes(
uint256 src,
uint256 dst,
uint256 nibbles
) internal pure returns (bool valid) {
assembly {
function getHex(c, i) -> ascii {
c := byte(i, c)
// chars 48-57: 0-9
if and(gt(c, 47), lt(c, 58)) {
ascii := sub(c, 48)
leave
}
// chars 65-70: A-F
if and(gt(c, 64), lt(c, 71)) {
ascii := add(sub(c, 65), 10)
leave
}
// chars 97-102: a-f
if and(gt(c, 96), lt(c, 103)) {
ascii := add(sub(c, 97), 10)
leave
}
// invalid char
ascii := 0x100
}
valid := true
let end := add(src, nibbles)
if and(nibbles, 1) {
let b := getHex(mload(src), 0) // "f" -> 15
mstore8(dst, b) // write ascii byte
src := add(src, 1) // update pointers
dst := add(dst, 1)
if gt(b, 255) {
valid := false
src := end // terminate loop
}
}
for {
} lt(src, end) {
src := add(src, 2) // 2 nibbles
dst := add(dst, 1) // per byte
} {
let word := mload(src) // read word (left aligned)
let b := or(shl(4, getHex(word, 0)), getHex(word, 1)) // "ff" -> 255
if gt(b, 255) {
valid := false
break
}
mstore8(dst, b) // write ascii byte
}
}
}
/// @dev Format `address` as a hex string.
/// @param addr The address to format.
/// @return hexString The corresponding hex string w/o a 0x-prefix.
function addressToHex(
address addr
) internal pure returns (string memory hexString) {
// return bytesToHex(abi.encodePacked(addr));
hexString = new string(40);
uint256 dst;
assembly {
mstore(0, addr)
dst := add(hexString, 32)
}
unsafeHex(12, dst, 40);
}
/// @dev Format `uint256` as a variable-length hex string without zero padding.
/// * unpaddedUintToHex(0, true) = "0"
/// * unpaddedUintToHex(1, true) = "1"
/// * unpaddedUintToHex(0, false) = "00"
/// * unpaddedUintToHex(1, false) = "01"
/// @param value The number to format.
/// @param dropZeroNibble If true, the leading byte will use one nibble if less than 16.
/// @return hexString The corresponding hex string w/o an 0x-prefix.
function unpaddedUintToHex(
uint256 value,
bool dropZeroNibble
) internal pure returns (string memory hexString) {
uint256 temp = value;
uint256 shift;
for (uint256 b = 128; b >= 8; b >>= 1) {
if (temp < (1 << b)) {
shift += b; // number of zero upper bits
} else {
temp >>= b; // shift away lower half
}
}
if (dropZeroNibble && temp < 16) shift += 4;
uint256 nibbles = 64 - (shift >> 2);
hexString = new string(nibbles);
uint256 dst;
assembly {
mstore(0, shl(shift, value)) // left-align
dst := add(hexString, 32)
}
unsafeHex(0, dst, nibbles);
}
/// @dev Format `bytes` as a hex string.
/// @param v The bytes to format.
/// @return hexString The corresponding hex string w/o a 0x-prefix.
function bytesToHex(
bytes memory v
) internal pure returns (string memory hexString) {
uint256 nibbles = v.length << 1;
hexString = new string(nibbles);
uint256 src;
uint256 dst;
assembly {
src := add(v, 32)
dst := add(hexString, 32)
}
unsafeHex(src, dst, nibbles);
}
/// @dev Converts arbitrary memory to a hex string.
/// @param src The memory offset of first nibble of input.
/// @param dst The memory offset of first hex-char of output (can alias `src`).
/// @param nibbles The number of nibbles to convert and the byte-length of the output.
function unsafeHex(
uint256 src,
uint256 dst,
uint256 nibbles
) internal pure {
unchecked {
for (uint256 end = dst + nibbles; dst < end; src += 32) {
uint256 word;
assembly {
word := mload(src)
}
for (uint256 shift = 256; dst < end && shift > 0; dst++) {
uint256 b = (word >> (shift -= 4)) & 15; // each nibble
b = b < 10 ? b + 0x30 : b + 0x57; // ("a" - 10) => 0x57
assembly {
mstore8(dst, b)
}
}
}
}
}
}
// ---- utils/TestNameCoder.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {NameCoder} from "./NameCoder.sol";
contract TestNameCoder {
function namehash(
bytes memory name,
uint256 offset
) external pure returns (bytes32 nameHash) {
return NameCoder.namehash(name, offset);
}
function encode(
string memory ens
) external pure returns (bytes memory dns) {
return NameCoder.encode(ens);
}
function decode(
bytes memory dns
) external pure returns (string memory ens) {
return NameCoder.decode(dns);
}
}
// ---- utils/LowLevelCallUtils.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
library LowLevelCallUtils {
using Address for address;
/// @dev Makes a static call to the specified `target` with `data`. Return data can be fetched with
/// `returnDataSize` and `readReturnData`.
/// @param target The address to staticcall.
/// @param data The data to pass to the call.
/// @return success True if the call succeeded, or false if it reverts.
function functionStaticCall(
address target,
bytes memory data
) internal view returns (bool success) {
return functionStaticCall(target, data, gasleft());
}
/// @dev Makes a static call to the specified `target` with `data` using `gasLimit`. Return data can be fetched with
/// `returnDataSize` and `readReturnData`.
/// @param target The address to staticcall.
/// @param data The data to pass to the call.
/// @param gasLimit The gas limit to use for the call.
/// @return success True if the call succeeded, or false if it reverts.
function functionStaticCall(
address target,
bytes memory data,
uint256 gasLimit
) internal view returns (bool success) {
require(
target.isContract(),
"LowLevelCallUtils: static call to non-contract"
);
assembly {
success := staticcall(
gasLimit,
target,
add(data, 32),
mload(data),
0,
0
)
}
}
/// @dev Returns the size of the return data of the most recent external call.
function returnDataSize() internal pure returns (uint256 len) {
assembly {
len := returndatasize()
}
}
/// @dev Reads return data from the most recent external call.
/// @param offset Offset into the return data.
/// @param length Number of bytes to return.
function readReturnData(
uint256 offset,
uint256 length
) internal pure returns (bytes memory data) {
data = new bytes(length);
assembly {
returndatacopy(add(data, 32), offset, length)
}
}
/// @dev Reverts with the return data from the most recent external call.
function propagateRevert() internal pure {
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
}
// ---- utils/MigrationHelper.sol ----
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
import {IBaseRegistrar} from "../ethregistrar/IBaseRegistrar.sol";
import {INameWrapper} from "../wrapper/INameWrapper.sol";
import {Controllable} from "../wrapper/Controllable.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract MigrationHelper is Ownable, Controllable {
IBaseRegistrar public immutable registrar;
INameWrapper public immutable wrapper;
address public migrationTarget;
error MigrationTargetNotSet();
event MigrationTargetUpdated(address indexed target);
constructor(IBaseRegistrar _registrar, INameWrapper _wrapper) {
registrar = _registrar;
wrapper = _wrapper;
}
function setMigrationTarget(address target) external onlyOwner {
migrationTarget = target;
emit MigrationTargetUpdated(target);
}
function migrateNames(
address nameOwner,
uint256[] memory tokenIds,
bytes memory data
) external onlyController {
if (migrationTarget == address(0)) {
revert MigrationTargetNotSet();
}
for (uint256 i = 0; i < tokenIds.length; i++) {
registrar.safeTransferFrom(
nameOwner,
migrationTarget,
tokenIds[i],
data
);
}
}
function migrateWrappedNames(
address nameOwner,
uint256[] memory tokenIds,
bytes memory data
) external onlyController {
if (migrationTarget == address(0)) {
revert MigrationTargetNotSet();
}
uint256[] memory amounts = new uint256[](tokenIds.length);
for (uint256 i = 0; i < amounts.length; i++) {
amounts[i] = 1;
}
wrapper.safeBatchTransferFrom(
nameOwner,
migrationTarget,
tokenIds,
amounts,
data
);
}
}
// ---- utils/ERC20Recoverable.sol ----
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.17 <0.9.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @notice Contract is used to recover ERC20 tokens sent to the contract by mistake.
contract ERC20Recoverable is Ownable {
/// @notice Recover ERC20 tokens sent to the contract by mistake.
/// @dev The contract is Ownable and only the owner can call the recover function.
/// @param _to The address to send the tokens to.
/// @param _token The address of the ERC20 token to recover
/// @param _amount The amount of tokens to recover.
function recoverFunds(
address _token,
address _to,
uint256 _amount
) external onlyOwner {
IERC20(_token).transfer(_to, _amount);
}
}
// ---- utils/BytesUtils_LEGACY.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
// ********************************************************************************
/// @dev DO NOT USE THIS CONTRACT
/// This library is provided so NameWrapper can remain unmodified.
/// The rest of the repo can switch to NameCoder, w/hashed label support.
// ********************************************************************************
import {BytesUtils} from "./BytesUtils.sol";
library BytesUtils_LEGACY {
/// @dev Returns the ENS namehash of a DNS-encoded name.
/// @param self The DNS-encoded name to hash.
/// @param offset The offset at which to start hashing.
/// @return The namehash of the name.
function namehash(
bytes memory self,
uint256 offset
) internal pure returns (bytes32) {
(bytes32 labelhash, uint256 newOffset) = readLabel(self, offset);
if (labelhash == bytes32(0)) {
require(offset == self.length - 1, "namehash: Junk at end of name");
return bytes32(0);
}
return
keccak256(abi.encodePacked(namehash(self, newOffset), labelhash));
}
/// @dev Returns the keccak-256 hash of a DNS-encoded label, and the offset to the start of the next label.
/// @param self The byte string to read a label from.
/// @param idx The index to read a label at.
/// @return labelhash The hash of the label at the specified index, or 0 if it is the last label.
/// @return newIdx The index of the start of the next label.
function readLabel(
bytes memory self,
uint256 idx
) internal pure returns (bytes32 labelhash, uint256 newIdx) {
require(idx < self.length, "readLabel: Index out of bounds");
uint256 len = uint256(uint8(self[idx]));
if (len > 0) {
labelhash = BytesUtils.keccak(self, idx + 1, len);
} else {
labelhash = bytes32(0);
}
newIdx = idx + len + 1;
}
}
// ---- utils/StringUtils.sol ----
pragma solidity >=0.8.4;
library StringUtils {
/// @dev Returns the length of a given string
/// @param s The string to measure the length of
/// @return The length of the input string
function strlen(string memory s) internal pure returns (uint256) {
uint256 len;
uint256 i = 0;
uint256 bytelength = bytes(s).length;
for (len = 0; i < bytelength; len++) {
bytes1 b = bytes(s)[i];
if (b < 0x80) {
i += 1;
} else if (b < 0xE0) {
i += 2;
} else if (b < 0xF0) {
i += 3;
} else if (b < 0xF8) {
i += 4;
} else if (b < 0xFC) {
i += 5;
} else {
i += 6;
}
}
return len;
}
/// @dev Escapes special characters in a given string
/// @param str The string to escape
/// @return The escaped string
function escape(string memory str) internal pure returns (string memory) {
bytes memory strBytes = bytes(str);
uint extraChars = 0;
// count extra space needed for escaping
for (uint i = 0; i < strBytes.length; i++) {
if (_needsEscaping(strBytes[i])) {
extraChars++;
}
}
// allocate buffer with the exact size needed
bytes memory buffer = new bytes(strBytes.length + extraChars);
uint index = 0;
// escape characters
for (uint i = 0; i < strBytes.length; i++) {
if (_needsEscaping(strBytes[i])) {
buffer[index++] = "\\";
buffer[index++] = _getEscapedChar(strBytes[i]);
} else {
buffer[index++] = strBytes[i];
}
}
return string(buffer);
}
// determine if a character needs escaping
function _needsEscaping(bytes1 char) private pure returns (bool) {
return
char == '"' ||
char == "/" ||
char == "\\" ||
char == "\n" ||
char == "\r" ||
char == "\t";
}
// get the escaped character
function _getEscapedChar(bytes1 char) private pure returns (bytes1) {
if (char == "\n") return "n";
if (char == "\r") return "r";
if (char == "\t") return "t";
return char;
}
}
// ---- utils/ENSIP19.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {HexUtils} from "../utils/HexUtils.sol";
import {NameCoder} from "../utils/NameCoder.sol";
uint32 constant CHAIN_ID_ETH = 1;
uint256 constant COIN_TYPE_ETH = 60;
uint256 constant EVM_BIT = 1 << 31;
string constant SLUG_ETH = "addr"; // <=> COIN_TYPE_ETH
string constant SLUG_DEFAULT = "default"; // <=> EVM_BIT
string constant TLD_REVERSE = "reverse";
/// @dev Library for generating reverse names according to ENSIP-19.
/// https://docs.ens.domains/ensip/19
library ENSIP19 {
/// @dev The supplied address was `0x`.
error EmptyAddress();
/// @dev Extract Chain ID from `coinType`.
/// @param coinType The coin type.
/// @return chain The Chain ID or 0 if non-EVM Chain.
function chainFromCoinType(
uint256 coinType
) internal pure returns (uint32 chain) {
if (coinType == COIN_TYPE_ETH) return CHAIN_ID_ETH;
return
uint32(
uint32(coinType) == coinType && (coinType & EVM_BIT) != 0
? coinType ^ EVM_BIT
: 0
);
}
/// @dev Determine if Coin Type is for an EVM address.
/// @param coinType The coin type.
/// @return isEVM True if coin type represents an EVM address.
function isEVMCoinType(
uint256 coinType
) internal pure returns (bool isEVM) {
isEVM = chainFromCoinType(coinType) != 0 || coinType == EVM_BIT;
}
/// @dev Generate Reverse Name from Address + Coin Type.
/// Reverts `EmptyAddress` if `addressBytes` is `0x`.
/// @param addressBytes The input address.
/// @param coinType The coin type.
/// @return name The ENS reverse name, eg. `1234abcd.addr.reverse`.
function reverseName(
bytes memory addressBytes,
uint256 coinType
) internal pure returns (string memory name) {
if (addressBytes.length == 0) revert EmptyAddress();
name = string(
abi.encodePacked(
HexUtils.bytesToHex(addressBytes),
bytes1("."),
coinType == COIN_TYPE_ETH
? SLUG_ETH
: coinType == EVM_BIT
? SLUG_DEFAULT
: HexUtils.unpaddedUintToHex(coinType, true),
bytes1("."),
TLD_REVERSE
)
);
}
/// @dev Parse Reverse Name into Address + Coin Type.
/// Matches: /^[0-9a-fA-F]+\.([0-9a-f]{1,64}|addr|default)\.reverse$/.
/// Reverts `DNSDecodingFailed`.
/// @param name The DNS-encoded name.
/// @return addressBytes The address or empty if invalid.
/// @return coinType The coin type.
function parse(
bytes memory name
) internal pure returns (bytes memory addressBytes, uint256 coinType) {
(, uint256 offset) = NameCoder.readLabel(name, 0);
bool valid;
(addressBytes, valid) = HexUtils.hexToBytes(name, 1, offset);
if (!valid || addressBytes.length == 0) return ("", 0); // addressBytes not 1+ hex
(bytes32 labelHash, uint256 offset2) = NameCoder.readLabel(
name,
offset
);
if (labelHash == keccak256(bytes(SLUG_ETH))) {
coinType = COIN_TYPE_ETH;
} else if (labelHash == keccak256(bytes(SLUG_DEFAULT))) {
coinType = EVM_BIT;
} else if (labelHash == bytes32(0)) {
return ("", 0); // no slug
} else {
bytes32 word;
(word, valid) = HexUtils.hexStringToBytes32(
name,
1 + offset,
offset2
);
if (!valid) return ("", 0); // invalid coinType
coinType = uint256(word);
}
(labelHash, offset) = NameCoder.readLabel(name, offset2);
if (labelHash != keccak256(bytes(TLD_REVERSE))) return ("", 0); // invalid tld
(labelHash, ) = NameCoder.readLabel(name, offset);
if (labelHash != bytes32(0)) return ("", 0); // not tld
}
}
// ---- dnsregistrar/RecordParser.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
import "../utils/BytesUtils.sol";
library RecordParser {
using BytesUtils for bytes;
/// @dev Parses a key-value record into a key and value.
/// @param input The input string
/// @param offset The offset to start reading at
function readKeyValue(
bytes memory input,
uint256 offset,
uint256 len
)
internal
pure
returns (bytes memory key, bytes memory value, uint256 nextOffset)
{
uint256 separator = input.find(offset, len, "=");
if (separator == type(uint256).max) {
return ("", "", type(uint256).max);
}
uint256 terminator = input.find(
separator,
len + offset - separator,
" "
);
if (terminator == type(uint256).max) {
terminator = len + offset;
nextOffset = terminator;
} else {
nextOffset = terminator + 1;
}
key = input.substring(offset, separator - offset);
value = input.substring(separator + 1, terminator - separator - 1);
}
}
// ---- dnsregistrar/DNSClaimChecker.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "../dnssec-oracle/DNSSEC.sol";
import "../dnssec-oracle/RRUtils.sol";
import "../utils/BytesUtils.sol";
import "../utils/HexUtils.sol";
import "@ensdomains/buffer/contracts/Buffer.sol";
library DNSClaimChecker {
using BytesUtils for bytes;
using HexUtils for bytes;
using RRUtils for *;
using Buffer for Buffer.buffer;
uint16 constant CLASS_INET = 1;
uint16 constant TYPE_TXT = 16;
function getOwnerAddress(
bytes memory name,
bytes memory data
) internal pure returns (address, bool) {
// Add "_ens." to the front of the name.
Buffer.buffer memory buf;
buf.init(name.length + 5);
buf.append("\x04_ens");
buf.append(name);
for (
RRUtils.RRIterator memory iter = data.iterateRRs(0);
!iter.done();
iter.next()
) {
if (iter.name().compareNames(buf.buf) != 0) continue;
bool found;
address addr;
(addr, found) = parseRR(data, iter.rdataOffset, iter.nextOffset);
if (found) {
return (addr, true);
}
}
return (address(0x0), false);
}
function parseRR(
bytes memory rdata,
uint256 idx,
uint256 endIdx
) internal pure returns (address, bool) {
while (idx < endIdx) {
uint256 len = rdata.readUint8(idx);
idx += 1;
bool found;
address addr;
(addr, found) = parseString(rdata, idx, len);
if (found) return (addr, true);
idx += len;
}
return (address(0x0), false);
}
function parseString(
bytes memory str,
uint256 idx,
uint256 len
) internal pure returns (address, bool) {
// TODO: More robust parsing that handles whitespace and multiple key/value pairs
if (str.readUint32(idx) != 0x613d3078) return (address(0x0), false); // 0x613d3078 == 'a=0x'
return str.hexToAddress(idx + 4, idx + len);
}
}
// ---- dnsregistrar/OffchainDNSResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "../../contracts/resolvers/profiles/IAddrResolver.sol";
import "../../contracts/resolvers/profiles/IExtendedResolver.sol";
import "../../contracts/resolvers/profiles/IExtendedDNSResolver.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "../dnssec-oracle/DNSSEC.sol";
import "../dnssec-oracle/RRUtils.sol";
import "../registry/ENSRegistry.sol";
import "../utils/HexUtils.sol";
import "../utils/BytesUtils.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {LowLevelCallUtils} from "../utils/LowLevelCallUtils.sol";
error InvalidOperation();
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);
interface IDNSGateway {
function resolve(
bytes memory name,
uint16 qtype
) external returns (DNSSEC.RRSetWithSignature[] memory);
}
uint16 constant CLASS_INET = 1;
uint16 constant TYPE_TXT = 16;
contract OffchainDNSResolver is IExtendedResolver, IERC165 {
using RRUtils for *;
using Address for address;
using BytesUtils for bytes;
using HexUtils for bytes;
ENS public immutable ens;
DNSSEC public immutable oracle;
string public gatewayURL;
error CouldNotResolve(bytes name);
constructor(ENS _ens, DNSSEC _oracle, string memory _gatewayURL) {
ens = _ens;
oracle = _oracle;
gatewayURL = _gatewayURL;
}
function supportsInterface(
bytes4 interfaceId
) external pure override returns (bool) {
return interfaceId == type(IExtendedResolver).interfaceId;
}
function resolve(
bytes calldata name,
bytes calldata data
) external view returns (bytes memory) {
revertWithDefaultOffchainLookup(name, data);
}
function resolveCallback(
bytes calldata response,
bytes calldata extraData
) external view returns (bytes memory) {
(bytes memory name, bytes memory query, bytes4 selector) = abi.decode(
extraData,
(bytes, bytes, bytes4)
);
if (selector != bytes4(0)) {
(bytes memory targetData, address targetResolver) = abi.decode(
query,
(bytes, address)
);
return
callWithOffchainLookupPropagation(
targetResolver,
name,
query,
abi.encodeWithSelector(
selector,
response,
abi.encode(targetData, address(this))
)
);
}
DNSSEC.RRSetWithSignature[] memory rrsets = abi.decode(
response,
(DNSSEC.RRSetWithSignature[])
);
(bytes memory data, ) = oracle.verifyRRSet(rrsets);
for (
RRUtils.RRIterator memory iter = data.iterateRRs(0);
!iter.done();
iter.next()
) {
// Ignore records with wrong name, type, or class
bytes memory rrname = RRUtils.readName(iter.data, iter.offset);
if (
!rrname.equals(name) ||
iter.class != CLASS_INET ||
iter.dnstype != TYPE_TXT
) {
continue;
}
// Look for a valid ENS-DNS TXT record
(address dnsresolver, bytes memory context) = parseRR(
iter.data,
iter.rdataOffset,
iter.nextOffset
);
// If we found a valid record, try to resolve it
if (dnsresolver != address(0)) {
if (
IERC165(dnsresolver).supportsInterface(
IExtendedDNSResolver.resolve.selector
)
) {
return
callWithOffchainLookupPropagation(
dnsresolver,
name,
query,
abi.encodeCall(
IExtendedDNSResolver.resolve,
(name, query, context)
)
);
} else if (
IERC165(dnsresolver).supportsInterface(
IExtendedResolver.resolve.selector
)
) {
return
callWithOffchainLookupPropagation(
dnsresolver,
name,
query,
abi.encodeCall(
IExtendedResolver.resolve,
(name, query)
)
);
} else {
(bool ok, bytes memory ret) = address(dnsresolver)
.staticcall(query);
if (ok) {
return ret;
} else {
revert CouldNotResolve(name);
}
}
}
}
// No valid records; revert.
revert CouldNotResolve(name);
}
function parseRR(
bytes memory data,
uint256 idx,
uint256 lastIdx
) internal view returns (address, bytes memory) {
bytes memory txt = readTXT(data, idx, lastIdx);
// Must start with the magic word
if (txt.length < 5 || !txt.equals(0, "ENS1 ", 0, 5)) {
return (address(0), "");
}
// Parse the name or address
uint256 lastTxtIdx = txt.find(5, txt.length - 5, " ");
if (lastTxtIdx > txt.length) {
address dnsResolver = parseAndResolve(txt, 5, txt.length);
return (dnsResolver, "");
} else {
address dnsResolver = parseAndResolve(txt, 5, lastTxtIdx);
return (
dnsResolver,
txt.substring(lastTxtIdx + 1, txt.length - lastTxtIdx - 1)
);
}
}
function readTXT(
bytes memory data,
uint256 startIdx,
uint256 lastIdx
) internal pure returns (bytes memory) {
// TODO: Concatenate multiple text fields
uint256 fieldLength = data.readUint8(startIdx);
assert(startIdx + fieldLength < lastIdx);
return data.substring(startIdx + 1, fieldLength);
}
function parseAndResolve(
bytes memory nameOrAddress,
uint256 idx,
uint256 lastIdx
) internal view returns (address) {
if (nameOrAddress[idx] == "0" && nameOrAddress[idx + 1] == "x") {
(address ret, bool valid) = nameOrAddress.hexToAddress(
idx + 2,
lastIdx
);
if (valid) {
return ret;
}
}
return resolveName(nameOrAddress, idx, lastIdx);
}
function resolveName(
bytes memory name,
uint256 idx,
uint256 lastIdx
) internal view returns (address) {
bytes32 node = textNamehash(name, idx, lastIdx);
address resolver = ens.resolver(node);
if (resolver == address(0)) {
return address(0);
}
return IAddrResolver(resolver).addr(node);
}
/// @dev Namehash function that operates on dot-separated names (not dns-encoded names)
/// @param name Name to hash
/// @param idx Index to start at
/// @param lastIdx Index to end at
function textNamehash(
bytes memory name,
uint256 idx,
uint256 lastIdx
) internal view returns (bytes32) {
uint256 separator = name.find(idx, name.length - idx, bytes1("."));
bytes32 parentNode = bytes32(0);
if (separator < lastIdx) {
parentNode = textNamehash(name, separator + 1, lastIdx);
} else {
separator = lastIdx;
}
return
keccak256(
abi.encodePacked(parentNode, name.keccak(idx, separator - idx))
);
}
function callWithOffchainLookupPropagation(
address target,
bytes memory name,
bytes memory innerdata,
bytes memory data
) internal view returns (bytes memory) {
if (!target.isContract()) {
revertWithDefaultOffchainLookup(name, innerdata);
}
bool result = LowLevelCallUtils.functionStaticCall(
address(target),
data
);
uint256 size = LowLevelCallUtils.returnDataSize();
if (result) {
bytes memory returnData = LowLevelCallUtils.readReturnData(0, size);
return abi.decode(returnData, (bytes));
}
// Failure
if (size >= 4) {
bytes memory errorId = LowLevelCallUtils.readReturnData(0, 4);
if (bytes4(errorId) == OffchainLookup.selector) {
// Offchain lookup. Decode the revert message and create our own that nests it.
bytes memory revertData = LowLevelCallUtils.readReturnData(
4,
size - 4
);
handleOffchainLookupError(revertData, target, name);
}
}
LowLevelCallUtils.propagateRevert();
}
function revertWithDefaultOffchainLookup(
bytes memory name,
bytes memory data
) internal view {
string[] memory urls = new string[](1);
urls[0] = gatewayURL;
revert OffchainLookup(
address(this),
urls,
abi.encodeCall(IDNSGateway.resolve, (name, TYPE_TXT)),
OffchainDNSResolver.resolveCallback.selector,
abi.encode(name, data, bytes4(0))
);
}
function handleOffchainLookupError(
bytes memory returnData,
address target,
bytes memory name
) internal view {
(
address sender,
string[] memory urls,
bytes memory callData,
bytes4 innerCallbackFunction,
bytes memory extraData
) = abi.decode(returnData, (address, string[], bytes, bytes4, bytes));
if (sender != target) {
revert InvalidOperation();
}
revert OffchainLookup(
address(this),
urls,
callData,
OffchainDNSResolver.resolveCallback.selector,
abi.encode(name, extraData, innerCallbackFunction)
);
}
}
// ---- dnsregistrar/DNSRegistrar.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@ensdomains/buffer/contracts/Buffer.sol";
import "../dnssec-oracle/DNSSEC.sol";
import "../dnssec-oracle/RRUtils.sol";
import "../registry/ENSRegistry.sol";
import "../root/Root.sol";
import "../resolvers/profiles/AddrResolver.sol";
import "../utils/BytesUtils.sol";
import "./DNSClaimChecker.sol";
import "./PublicSuffixList.sol";
import "./IDNSRegistrar.sol";
/// @dev An ENS registrar that allows the owner of a DNS name to claim the
/// corresponding name in ENS.
contract DNSRegistrar is IDNSRegistrar, IERC165 {
using BytesUtils for bytes;
using Buffer for Buffer.buffer;
using RRUtils for *;
ENS public immutable ens;
DNSSEC public immutable oracle;
PublicSuffixList public suffixes;
address public immutable previousRegistrar;
address public immutable resolver;
// A mapping of the most recent signatures seen for each claimed domain.
mapping(bytes32 => uint32) public inceptions;
error NoOwnerRecordFound();
error PermissionDenied(address caller, address owner);
error PreconditionNotMet();
error StaleProof();
error InvalidPublicSuffix(bytes name);
struct OwnerRecord {
bytes name;
address owner;
address resolver;
uint64 ttl;
}
event Claim(
bytes32 indexed node,
address indexed owner,
bytes dnsname,
uint32 inception
);
event NewPublicSuffixList(address suffixes);
constructor(
address _previousRegistrar,
address _resolver,
DNSSEC _dnssec,
PublicSuffixList _suffixes,
ENS _ens
) {
previousRegistrar = _previousRegistrar;
resolver = _resolver;
oracle = _dnssec;
suffixes = _suffixes;
emit NewPublicSuffixList(address(suffixes));
ens = _ens;
}
/// @dev This contract's owner-only functions can be invoked by the owner of the ENS root.
modifier onlyOwner() {
Root root = Root(ens.owner(bytes32(0)));
address owner = root.owner();
require(msg.sender == owner);
_;
}
function setPublicSuffixList(PublicSuffixList _suffixes) public onlyOwner {
suffixes = _suffixes;
emit NewPublicSuffixList(address(suffixes));
}
/// @dev Submits proofs to the DNSSEC oracle, then claims a name using those proofs.
/// @param name The name to claim, in DNS wire format.
/// @param input A chain of signed DNS RRSETs ending with a text record.
function proveAndClaim(
bytes memory name,
DNSSEC.RRSetWithSignature[] memory input
) public override {
(bytes32 rootNode, bytes32 labelHash, address addr) = _claim(
name,
input
);
ens.setSubnodeOwner(rootNode, labelHash, addr);
}
function proveAndClaimWithResolver(
bytes memory name,
DNSSEC.RRSetWithSignature[] memory input,
address resolver,
address addr
) public override {
(bytes32 rootNode, bytes32 labelHash, address owner) = _claim(
name,
input
);
if (msg.sender != owner) {
revert PermissionDenied(msg.sender, owner);
}
ens.setSubnodeRecord(rootNode, labelHash, owner, resolver, 0);
if (addr != address(0)) {
if (resolver == address(0)) {
revert PreconditionNotMet();
}
bytes32 node = keccak256(abi.encodePacked(rootNode, labelHash));
// Set the resolver record
AddrResolver(resolver).setAddr(node, addr);
}
}
function supportsInterface(
bytes4 interfaceID
) external pure override returns (bool) {
return
interfaceID == type(IERC165).interfaceId ||
interfaceID == type(IDNSRegistrar).interfaceId;
}
function _claim(
bytes memory name,
DNSSEC.RRSetWithSignature[] memory input
) internal returns (bytes32 parentNode, bytes32 labelHash, address addr) {
(bytes memory data, uint32 inception) = oracle.verifyRRSet(input);
// Get the first label
uint256 labelLen = name.readUint8(0);
labelHash = name.keccak(1, labelLen);
bytes memory parentName = name.substring(
labelLen + 1,
name.length - labelLen - 1
);
// Make sure the parent name is enabled
parentNode = enableNode(parentName);
bytes32 node = keccak256(abi.encodePacked(parentNode, labelHash));
if (!RRUtils.serialNumberGte(inception, inceptions[node])) {
revert StaleProof();
}
inceptions[node] = inception;
bool found;
(addr, found) = DNSClaimChecker.getOwnerAddress(name, data);
if (!found) {
revert NoOwnerRecordFound();
}
emit Claim(node, addr, name, inception);
}
function enableNode(bytes memory domain) public returns (bytes32 node) {
// Name must be in the public suffix list.
if (!suffixes.isPublicSuffix(domain)) {
revert InvalidPublicSuffix(domain);
}
return _enableNode(domain, 0);
}
function _enableNode(
bytes memory domain,
uint256 offset
) internal returns (bytes32 node) {
uint256 len = domain.readUint8(offset);
if (len == 0) {
return bytes32(0);
}
bytes32 parentNode = _enableNode(domain, offset + len + 1);
bytes32 label = domain.keccak(offset + 1, len);
node = keccak256(abi.encodePacked(parentNode, label));
address owner = ens.owner(node);
if (owner == address(0) || owner == previousRegistrar) {
if (parentNode == bytes32(0)) {
Root root = Root(ens.owner(bytes32(0)));
root.setSubnodeOwner(label, address(this));
ens.setResolver(node, resolver);
} else {
ens.setSubnodeRecord(
parentNode,
label,
address(this),
resolver,
0
);
}
} else if (owner != address(this)) {
revert PreconditionNotMet();
}
return node;
}
}
// ---- dnsregistrar/IDNSRegistrar.sol ----
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "../dnssec-oracle/DNSSEC.sol";
interface IDNSRegistrar {
function proveAndClaim(
bytes memory name,
DNSSEC.RRSetWithSignature[] memory input
) external;
function proveAndClaimWithResolver(
bytes memory name,
DNSSEC.RRSetWithSignature[] memory input,
address resolver,
address addr
) external;
}
// ---- dnsregistrar/TLDPublicSuffixList.sol ----
pragma solidity ^0.8.4;
import "../utils/BytesUtils.sol";
import "./PublicSuffixList.sol";
/// @dev A public suffix list that treats all TLDs as public suffixes.
contract TLDPublicSuffixList is PublicSuffixList {
using BytesUtils for bytes;
function isPublicSuffix(
bytes calldata name
) external view override returns (bool) {
uint256 labellen = name.readUint8(0);
return labellen > 0 && name.readUint8(labellen + 1) == 0;
}
}
// ---- dnsregistrar/SimplePublicSuffixList.sol ----
pragma solidity ^0.8.4;
pragma experimental ABIEncoderV2;
import "../root/Ownable.sol";
import "./PublicSuffixList.sol";
contract SimplePublicSuffixList is PublicSuffixList, Ownable {
mapping(bytes => bool) suffixes;
event SuffixAdded(bytes suffix);
function addPublicSuffixes(bytes[] memory names) public onlyOwner {
for (uint256 i = 0; i < names.length; i++) {
suffixes[names[i]] = true;
emit SuffixAdded(names[i]);
}
}
function isPublicSuffix(
bytes calldata name
) external view override returns (bool) {
return suffixes[name];
}
}
// ---- dnsregistrar/PublicSuffixList.sol ----
pragma solidity ^0.8.4;
interface PublicSuffixList {
function isPublicSuffix(bytes calldata name) external view returns (bool);
}
// ---- dnsregistrar/mocks/DummyLegacyTextResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "../../resolvers/profiles/ITextResolver.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
contract DummyLegacyTextResolver is ITextResolver, IERC165 {
function supportsInterface(
bytes4 interfaceId
) external pure override returns (bool) {
return interfaceId == type(ITextResolver).interfaceId;
}
function text(
bytes32 /* node */,
string calldata key
) external view override returns (string memory) {
return key;
}
}
// ---- dnsregistrar/mocks/DummyParser.sol ----
pragma solidity ^0.8.4;
import "../../utils/BytesUtils.sol";
import "../RecordParser.sol";
contract DummyParser {
using BytesUtils for bytes;
// parse data in format: name;key1=value1 key2=value2;url
function parseData(
bytes memory data,
uint256 kvCount
)
external
pure
returns (
string memory name,
string[] memory keys,
string[] memory values,
string memory url
)
{
uint256 len = data.length;
// retrieve name
uint256 sep1 = data.find(0, len, ";");
name = string(data.substring(0, sep1));
// retrieve url
uint256 sep2 = data.find(sep1 + 1, len - sep1, ";");
url = string(data.substring(sep2 + 1, len - sep2 - 1));
keys = new string[](kvCount);
values = new string[](kvCount);
// retrieve keys and values
uint256 offset = sep1 + 1;
for (uint256 i; i < kvCount && offset < len; i++) {
(
bytes memory key,
bytes memory val,
uint256 nextOffset
) = RecordParser.readKeyValue(data, offset, sep2 - offset);
keys[i] = string(key);
values[i] = string(val);
offset = nextOffset;
}
}
}
// ---- dnsregistrar/mocks/DummyExtendedDNSSECResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "../../resolvers/profiles/IExtendedDNSResolver.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
contract DummyExtendedDNSSECResolver is IExtendedDNSResolver, IERC165 {
function supportsInterface(
bytes4 interfaceId
) external pure override returns (bool) {
return interfaceId == type(IExtendedDNSResolver).interfaceId;
}
function resolve(
bytes memory /* name */,
bytes memory /* data */,
bytes memory context
) external view override returns (bytes memory) {
return abi.encode(context);
}
}
// ---- dnsregistrar/mocks/DummyDnsRegistrarDNSSEC.sol ----
pragma solidity ^0.8.4;
contract DummyDNSSEC {
uint16 expectedType;
bytes expectedName;
uint32 inception;
uint64 inserted;
bytes20 hash;
function setData(
uint16 _expectedType,
bytes memory _expectedName,
uint32 _inception,
uint64 _inserted,
bytes memory _proof
) public {
expectedType = _expectedType;
expectedName = _expectedName;
inception = _inception;
inserted = _inserted;
if (_proof.length != 0) {
hash = bytes20(keccak256(_proof));
}
}
function rrdata(
uint16 dnstype,
bytes memory name
) public view returns (uint32, uint64, bytes20) {
require(dnstype == expectedType);
require(keccak256(name) == keccak256(expectedName));
return (inception, inserted, hash);
}
}
// ---- dnsregistrar/mocks/DummyNonCCIPAwareResolver.sol ----
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "../OffchainDNSResolver.sol";
import "../../resolvers/profiles/IExtendedResolver.sol";
contract DummyNonCCIPAwareResolver is IExtendedResolver, ERC165 {
OffchainDNSResolver dnsResolver;
constructor(OffchainDNSResolver _dnsResolver) {
dnsResolver = _dnsResolver;
}
function supportsInterface(
bytes4 interfaceId
) public view virtual override returns (bool) {
return
interfaceId == type(IExtendedResolver).interfaceId ||
super.supportsInterface(interfaceId);
}
function resolve(
bytes calldata /* name */,
bytes calldata data
) external view returns (bytes memory) {
string[] memory urls = new string[](1);
urls[0] = "https://example.com/";
revert OffchainLookup(
address(dnsResolver),
urls,
data,
OffchainDNSResolver.resolveCallback.selector,
data
);
}
}
```
---
# docs.login.xyz llms.txt
> Offering resources and guidance for integrating Sign-In with Ethereum, enhancing user control over digital identities in web applications, while promoting best practices and supporting community involvement within the Ethereum ecosystem.
Sign-In with EthereumYour Keys, Your IdentifierNextQuickstart GuideLast updated 3 years ago
Your Keys, Your Identifier
Last updated 3 years ago
Sign-In with Ethereum is a new form of authentication that enables users to control their digital identity with their Ethereum account and ENS profile instead of relying on a traditional intermediary. Already used throughout Web3, this effort standardizes the method with best practices and makes it easy to adopt securely.
To hop right in, check out our Quickstart Guide.
Sign-in With Ethereum was a standard built collaboratively with the greater Ethereum community. For more information on the EIP, check out the following page:
For more information on Sign-In with Ethereum and its related benefits to both the Web3 ecosystem and Web2 services, check out the following page:
💻 Login.xyz - Check out the Sign-In with Ethereum home page for more information about supporters, and recent activity.
👾 Discord - Join the #sign-in-with-ethereum channel in the Spruce Discord Server for additional support.
📖 Blog - Check out the latest updates on Sign-In with Ethereum posted on the Spruce blog.
We host a Sign-In with Ethereum community where we discuss relevant updates, new libraries, additional integrations, and more. If you're interested in contributing to Sign-In with Ethereum, we encourage that you join the calls by filling in this form.
# Quickstart Guide
Sign-In with EthereumQuickstart GuideThis guide will show how to implement Sign-In with Ethereum (SIWE) in a client-server JavaScript web application.PreviousSign-In with EthereumNextCreating SIWE MessagesLast updated 3 years ago
This guide will show how to implement Sign-In with Ethereum (SIWE) in a client-server JavaScript web application.
Requirements
An Ethereum account in the installed MetaMask wallet
The repository for this tutorial can be found here:
# Creating SIWE Messages
Sign-In with EthereumQuickstart GuideCreating SIWE MessagesThis section describes how to generate Sign-In with Ethereum messages and print them to the console.PreviousQuickstart GuideNextImplement the FrontendLast updated 2 years ago
This section describes how to generate Sign-In with Ethereum messages and print them to the console.
Last updated 2 years ago
A completed version of this part can be found in the example repository ().
Creating SIWE messages in JavaScript is straightforward when using the siwe library in npm. To begin, create a new project called siwe-print.
siwe
siwe-print
mkdir siwe-print && cd siwe-print/
yarn init --yes
yarn add siwe ethers
mkdir src/
We can then write the following into ./src/index.js:
./src/index.js
const siwe = require('siwe');
const domain = "localhost";
const origin = "https://localhost/login";
function createSiweMessage (address, statement) {
const siweMessage = new siwe.SiweMessage({
domain,
address,
statement,
uri: origin,
version: '1',
chainId: '1'
});
return siweMessage.prepareMessage();
}
console.log(createSiweMessage(
"0x6Ee9894c677EFa1c56392e5E7533DE76004C8D94",
"This is a test statement."
));
Now run the example:
node src/index.js
You should see output similar to the following message, with different values for the Nonce and Issued At fields:
localhost wants you to sign in with your Ethereum account:
0x6Ee9894c677EFa1c56392e5E7533DE76004C8D94
This is a test statement.
URI: https://localhost/login
Version: 1
Chain ID: 1
Nonce: oNCEHm5jzQU2WvuBB
Issued At: 2022-01-28T23:28:16.013Z
To learn about all the available fields in a SiweMessage, check out the information in EIP-4361
SiweMessage
The fields we are most interested in for the purposes of this guide are address and statement. address is the Ethereum address which the user is signing in with, and the statement as this will describe to the user what action we wish to perform on their behalf.
address
statement
Often, as in this example, we don't need to do any manipulation of the message, so we can immediately convert it into the textual representation that the user will sign.
00_print
# Implement the Backend
Sign-In with EthereumQuickstart GuideImplement the BackendHere we learn how to build the backend server to handle the user's submission using Express.js.PreviousImplement the FrontendNextConnect the FrontendLast updated 2 years ago
Here we learn how to build the backend server to handle the user's submission using Express.js.
A completed version of this part can be found here (). This example uses only uses the command line in the terminal to print messages, no monitoring of the browser console log is necessary.
The backend server gives the frontend a nonce to include in the SIWE message and also verifies the submission. As such, this basic example only provides two corresponding endpoints:
/nonce to generate the nonce for the interaction via GET request.
/nonce
GET
/verify to verify the submitted SIWE message and signature via POST request.
/verify
POST
While this simple example does not check the nonce during verification, all production implementations should, as demonstrated in the final section .
1. Setup the project directory:
mkdir siwe-backend && cd siwe-backend/
yarn add cors express siwe ethers
2. Make sure that the package.json type is module like the following:
package.json
type
module
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"license": "MIT",
"scripts": {
"start": "node src/index.js"
},
"dependencies": {
"siwe": "^2.1.4",
"cors": "^2.8.5",
"ethers": "^6.3.0",
"express": "^4.18.2"
}
3. Populate src/index.js with the following:
src/index.js
import cors from 'cors';
import express from 'express';
import { generateNonce, SiweMessage } from 'siwe';
const app = express();
app.use(express.json());
app.use(cors());
app.get('/nonce', function (_, res) {
res.setHeader('Content-Type', 'text/plain');
res.send(generateNonce());
});
app.post('/verify', async function (req, res) {
const { message, signature } = req.body;
const siweMessage = new SiweMessage(message);
try {
await siweMessage.verify({ signature });
res.send(true);
} catch {
res.send(false);
}
app.listen(3000);
4. You can run the server with the following command.
yarn start
In a new terminal window, test the /nonce endpoint to make sure the backend is working:
curl 'http://localhost:3000/nonce'
In the same new terminal window, test the /verify endpoint use the following, and ensure the response is true:
true
curl 'http://localhost:3000/verify' \
-H 'Content-Type: application/json' \
--data-raw '{"message":"localhost:8080 wants you to sign in with your Ethereum account:\n0x9D85ca56217D2bb651b00f15e694EB7E713637D4\n\nSign in with Ethereum to the app.\n\nURI: http://localhost:8080\nVersion: 1\nChain ID: 1\nNonce: spAsCWHwxsQzLcMzi\nIssued At: 2022-01-29T03:22:26.716Z","signature":"0xe117ad63b517e7b6823e472bf42691c28a4663801c6ad37f7249a1fe56aa54b35bfce93b1e9fa82da7d55bbf0d75ca497843b0702b9dfb7ca9d9c6edb25574c51c"}'
We can verify the received SIWE message by parsing it back into a SiweMessage object (the constructor handles this), assigning the received signature to it and calling the verify method:
verify
message.verify({ signature })
message.verify({ signature }) in the above snippet makes sure that the given signature is correct for the message, ensuring that the Ethereum address within the message produced the matching signature.
In other applications, you may wish to do further verification on other fields in the message, for example asserting that the authority matches the expected domain, or checking that the named address has the authority to access the named URI.
A small example of this is shown later where the nonce attribute is used to track that a given address has signed the message given by the server.
# Connect the Frontend
Sign-In with EthereumQuickstart GuideConnect the FrontendHere we learn how to update the frontend to send the signed messages to the server.PreviousImplement the BackendNextImplement SessionsLast updated 2 years ago
Here we learn how to update the frontend to send the signed messages to the server.
A completed version of the updated frontend can be found here (). This example uses the to print messages, so it should be actively monitored.
1. Revisit the siwe-frontend directory, stop any running servers, and update src/index.js:
siwe-frontend
src/index.js:
import { BrowserProvider } from 'ethers';
import { SiweMessage } from 'siwe';
const domain = window.location.host;
const origin = window.location.origin;
const provider = new BrowserProvider(window.ethereum);
const BACKEND_ADDR = "http://localhost:3000";
async function createSiweMessage(address, statement) {
const res = await fetch(`${BACKEND_ADDR}/nonce`);
const message = new SiweMessage({
domain,
address,
statement,
uri: origin,
version: '1',
chainId: '1',
nonce: await res.text()
});
return message.prepareMessage();
function connectWallet() {
provider.send('eth_requestAccounts', [])
.catch(() => console.log('user rejected request'));
let message = null;
let signature = null;
async function signInWithEthereum() {
const signer = await provider.getSigner();
message = await createSiweMessage(
await signer.address,
'Sign in with Ethereum to the app.'
);
console.log(message);
signature = await signer.signMessage(message);
console.log(signature);
async function sendForVerification() {
const res = await fetch(`${BACKEND_ADDR}/verify`, {
method: "POST",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message, signature }),
console.log(await res.text());
const connectWalletBtn = document.getElementById('connectWalletBtn');
const siweBtn = document.getElementById('siweBtn');
const verifyBtn = document.getElementById('verifyBtn');
connectWalletBtn.onclick = connectWallet;
siweBtn.onclick = signInWithEthereum;
verifyBtn.onclick = sendForVerification;
2. Update src/index.html:
src/index.html
SIWE Quickstart
3. For this last step, you need to have both the frontend and backend running together. Start by running the backend server with the following command from the parent directory:
cd siwe-backend
In a separate terminal, start the frontend by running the following command and visit the URL printed to the console:
cd siwe-frontend
4. Try to Sign-In with Ethereum by visiting the URL printed to the console, connecting your wallet, and signing in. You can now hit the Send for verification button to receive a true in the console.
Send for verification
03_complete_app/frontend
# Implement Sessions
Sign-In with EthereumQuickstart GuideImplement SessionsHere we learn how to implement sessions with Express.js to add the necessary backend security.PreviousConnect the FrontendNextResolve ENS ProfilesLast updated 2 years ago
Here we learn how to implement sessions with Express.js to add the necessary backend security.
A completed version of the updated backend can be found here (). This example uses the to print messages, so it should be actively monitored.
For additional security against replay attacks, it is not enough for the backend to generate the nonce. It must also be tied to a browser session with the end-user. In the siwe-backend directory, install the following and edit index.js:
siwe-backend
index.js
yarn add express-session
Update src/index.js to the following:
import Session from 'express-session';
app.use(cors({
origin: 'http://localhost:8080',
credentials: true,
}))
app.use(Session({
name: 'siwe-quickstart',
secret: "siwe-quickstart-secret",
resave: true,
saveUninitialized: true,
cookie: { secure: false, sameSite: true }
}));
app.get('/nonce', async function (req, res) {
req.session.nonce = generateNonce();
res.status(200).send(req.session.nonce);
if (!req.body.message) {
res.status(422).json({ message: 'Expected prepareMessage object as body.' });
return;
}
let SIWEObject = new SiweMessage(req.body.message);
const { data: message } = await SIWEObject.verify({ signature: req.body.signature, nonce: req.session.nonce });
req.session.siwe = message;
req.session.cookie.expires = new Date(message.expirationTime);
req.session.save(() => res.status(200).send(true));
} catch (e) {
req.session.siwe = null;
req.session.nonce = null;
console.error(e);
switch (e) {
case ErrorTypes.EXPIRED_MESSAGE: {
req.session.save(() => res.status(440).json({ message: e.message }));
break;
}
case ErrorTypes.INVALID_SIGNATURE: {
req.session.save(() => res.status(422).json({ message: e.message }));
default: {
req.session.save(() => res.status(500).json({ message: e.message }));
app.get('/personal_information', function (req, res) {
if (!req.session.siwe) {
res.status(401).json({ message: 'You have to first sign_in' });
return;
console.log("User is authenticated!");
res.send(`You are authenticated and your address is: ${req.session.siwe.address}`);
This way, the session (req.session) stores the nonce for the initial validation of the message, and once that's done, more can be added. For example, here we store the message's fields, so we can always reference the address of the user.
req.session
A potential extension is to resolve the ENS domain of the user and keep it in the session.
On the client side, the flow is similar to the previous example, except we now need to send cookies with our requests for the session to work. We can add a new endpoint, personal_information, to retrieve the information from the session in place, without having to send the message and signature every time.
personal_information
In the siwe-frontend directory, stop any running instances and populate src/index.js to match the following:
const res = await fetch(`${BACKEND_ADDR}/nonce`, {
credentials: 'include',
const message = await createSiweMessage(
await signer.getAddress(),
const signature = await signer.signMessage(message);
credentials: 'include'
async function getInformation() {
const res = await fetch(`${BACKEND_ADDR}/personal_information`, {
const infoBtn = document.getElementById('infoBtn');
infoBtn.onclick = getInformation;
Update the siwe-frontend/src/index.html to match the following:
siwe-frontend/src/index.html
SIWE Quickstart
4. Try to Sign-In with Ethereum by visiting the URL printed to the console, connecting your wallet, and signing in. You can now hit the Get session information button to receive a message similar to the following in the console:
Get session information
You are authenticated and your address is:
Refer to for additional information on how to use express-session in production.
express-session
03_complete_app/backend
# Resolve ENS Profiles
Sign-In with EthereumQuickstart GuideResolve ENS ProfilesHere we learn how to resolve a user's ENS profile data.PreviousImplement SessionsNextResolve NFT HoldingsLast updated 2 years ago
Here we learn how to resolve a user's ENS profile data.
A completed version of the updated frontend can be found here: ().
Now that the application knows the user's connected account, a basic profile can be built using additional information from if available. Because the frontend is already is using ethers, it is simple to add this functionality and retrieve this data.
frontend
ethers
Update the frontend/src/index.html file to the following:
frontend/src/index.html
ENS Metadata:
No ENS Profile detected.
This will create a table with data based on the user's ENS information if it exists. Otherwise, if there isn't any data, it will remain hidden and a "No ENS Profile detected." message will be displayed.
Finally, we can finish by updating the index.js file to include what's needed.
Update the frontend/src/index.js file to the following:
frontend/src/index.js
const profileElm = document.getElementById('profile');
const noProfileElm = document.getElementById('noProfile');
const welcomeElm = document.getElementById('welcome');
const ensLoaderElm = document.getElementById('ensLoader');
const ensContainerElm = document.getElementById('ensContainer');
const ensTableElm = document.getElementById('ensTable');
let address;
profileElm.classList = 'hidden';
noProfileElm.classList = 'hidden';
welcomeElm.classList = 'hidden';
address = await signer.getAddress()
if (!res.ok) {
console.error(`Failed in getInformation: ${res.statusText}`);
return
displayENSProfile();
let result = await res.text();
console.log(result);
address = result.split(" ")[result.split(" ").length - 1];
async function displayENSProfile() {
const ensName = await provider.lookupAddress(address);
if (ensName) {
profileElm.classList = '';
welcomeElm.innerHTML = `Hello, ${ensName}`;
let avatar = await provider.getAvatar(ensName);
if (avatar) {
welcomeElm.innerHTML += ` `;
ensLoaderElm.innerHTML = 'Loading...';
ensTableElm.innerHTML.concat(`
`;
for (const key of keys)
ensTableElm.innerHTML += `
${key}:
${await resolver.getText(key)}
`;
ensLoaderElm.innerHTML = '';
ensContainerElm.classList = '';
} else {
welcomeElm.innerHTML = `Hello, ${address}`;
noProfileElm.classList = '';
welcomeElm.classList = '';
The above adds a look-up for some ENS metadata (email, url, description and twitter), then converts the result into content that is displayed in the table. Other functionality includes the showing and hiding of UI elements to make the page dynamic.
email
url
description
twitter
By doing this we're grabbing the label to each of the text records for a user's ENS profile, resolving each of them, and placing the result into a basic table.
To see the result, go into frontend and run:
yarn
Then go into backend and run:
backend
And run through the updated example!
Now once the Sign-In with Ethereum flow is complete and there's an ENS profile associated with the account, the ENS name and avatar will appear along with all additional metadata from the profile in a new table.
04_ens_resolution/frontend
# Resolve NFT Holdings
Sign-In with EthereumQuickstart GuideResolve NFT HoldingsHere we learn how to pull information on a users' NFT holdingsPreviousResolve ENS ProfilesNextTypeScriptLast updated 2 years ago
Here we learn how to pull information on a users' NFT holdings
Similar to the ENS look-up, we can also query the user's NFT ownership. In this example, we will display basic information about the user's NFTs in a table, via the . However, this could also be extended to even give the user a visual gallery view of their NFTs once connected.
First, we need to change index.html to include a new table. We'll use the same structure as in the last chapter, separating the two tables with an tag:
index.html
table,
th,
td {
No ENS Profile Found.
NFT Ownership
Next, we'll update the index.js file to reach out to OpenSea's API using the logged-in user's address, then format the output to place the information in the table. If the user has no NFTs, we'll display a "No NFTs found" message in the loader div.
div
const nftElm = document.getElementById('nft');
const nftLoaderElm = document.getElementById('nftLoader');
const nftContainerElm = document.getElementById('nftContainer');
const nftTableElm = document.getElementById('nftTable');
async function getNFTs() {
let res = await fetch(`https://api.opensea.io/api/v1/assets?owner=${address}`);
if (!res.ok) {
throw new Error(res.statusText)
let body = await res.json();
if (!body.assets || !Array.isArray(body.assets) || body.assets.length === 0) {
return []
return body.assets.map((asset) => {
let {name, asset_contract, token_id} = asset;
let {address} = asset_contract;
return {name, address, token_id};
});
} catch (err) {
console.error(`Failed to resolve nfts: ${err.message}`);
return [];
async function displayNFTs() {
nftLoaderElm.innerHTML = 'Loading NFT Ownership...';
nftElm.classList = '';
let nfts = await getNFTs();
if (nfts.length === 0) {
nftLoaderElm.innerHTML = 'No NFTs found';
let tableHtml = "
Name
Address
Token ID
";
nfts.forEach((nft) => {
tableHtml += `
${nft.name}
${nft.address}
${nft.token_id}
`
nftTableElm.innerHTML = tableHtml;
nftContainerElm.classList = '';
nftLoaderElm.innerHTML = '';
displayNFTs();
Similar to the previous guide, to see the result, go into frontend and run:
Now, when a user signs in, information on NFT holdings is displayed below the ENS information (if available).
OpenSea's API is a great resource for interacting with NFT data off-chain. Learn more .
05_nft_resolution/frontend
# TypeScript
LibrariesTypeScriptA TypeScript implementation of EIP-4361: Sign In With Ethereum.PreviousResolve NFT HoldingsNextMigrating to v2Last updated 3 years ago
A TypeScript implementation of EIP-4361: Sign In With Ethereum.
The TypeScript implementation of Sign-In with Ethereum can be found here:
Sign-In with Ethereum can be installed as an npm package. For more information and package information, click .
npm
# Migrating to v2
LibrariesTypeScriptMigrating to v2TypeScript v2PreviousTypeScriptNextTypeScript QuickstartLast updated 2 years ago
TypeScript v2
If you are using siwe v1.1.6, we encourage you to update to the latest version (2.1.x). The following guide walks you through how to update your application.
siwe v1.1.6
2.1.x
The function validate(sig, provider) is now deprecated and is replaced by verify(VerifyParams, VerifyOpts). These two new parameters accept the following fields:
validate(sig, provider)
verify(VerifyParams, VerifyOpts)
export interface VerifyParams {
/** Signature of the message signed by the wallet */
signature: string;
/** RFC 4501 dns authority that is requesting the signing. */
domain?: string;
/** Randomized token used to prevent replay attacks, at least 8 alphanumeric characters. */
nonce?: string;
/**ISO 8601 datetime string of the current time. */
time?: string;
export interface VerifyOpts {
/** ethers provider to be used for EIP-1271 validation */
provider?: providers.Provider;
/** If the library should reject promises on errors, defaults to false */
suppressExceptions?: boolean;
The new function makes it easier to match fields automatically - like domain, nonce and match against other TimeDate instead of now (time).
domain
nonce
time
The return type was also modified. It now returns a SiweResponse instead of a SiweMessage, and this new object is defined by the following interface:
SiweResponse
export interface SiweResponse {
/** Boolean representing if the message was verified with success. */
success: boolean;
/** If present `success` MUST be false and will provide extra information on the failure reason. */
error?: SiweError;
/** Original message that was verified. */
data: SiweMessage;
As part of the new API, new error types were introduced to clarify when a message fails verification. These errors are defined at:
More information regarding the rationale behind the API Harmonization and TypeScript v2.0 beta release can be found here:
# TypeScript Quickstart
LibrariesTypeScriptTypeScript QuickstartA Quickstart example using the TypeScript SIWE LibraryPreviousMigrating to v2NextRustLast updated 3 years ago
A Quickstart example using the TypeScript SIWE Library
Goals
Run a Sign-In with Etheruem example locally
Sign-In with Ethereum using a preferred wallet
version 14.0 or higher
First clone the siwe repository from GitHub by running the following command:
git clone https://github.com/spruceid/siwe-notepad
Next, enter the directory and run the example by using the following commands:
cd siwe-notepad
npm install
npm run dev
Finally, visit the example at http://localhost:4361 (or whichever port npm allocated).
Once the example has loaded, sign in with Ethereum by clicking on one of the wallet options, enter some text, and save that text. After disconnecting, try reconnecting to reload that text once the session has been reestablished.
The full example can be found here:
# Rust
LibrariesRustA Rust implementation of EIP-4361: Sign In With Ethereum.PreviousTypeScript QuickstartNextElixirLast updated 2 years ago
A Rust implementation of EIP-4361: Sign In With Ethereum.
The Rust implementation and latest documentation for Sign-In with Ethereum can be found here:
Sign-In with Ethereum can be found on .
# Elixir
LibrariesElixirAn Elixir implementation of EIP-4361: Sign In With Ethereum.PreviousRustNextPythonLast updated 3 years ago
An Elixir implementation of EIP-4361: Sign In With Ethereum.
The Elixir implementation of Sign-In with Ethereum can be found here:
The package can be installed by adding siwe to your list of dependencies in mix.exs:
mix.exs
def deps do
[
{:siwe, "~> 0.3"}
]
end
To see how this works in iex, clone this repository and from the root run:
iex
$ mix deps.get
Then create two files message.txt:
message.txt
login.xyz wants you to sign in with your Ethereum account:
0xfA151B5453CE69ABf60f0dbdE71F6C9C5868800E
Sign-In With Ethereum Example Statement
URI: https://login.xyz
Nonce: ToTaLLyRanDOM
Issued At: 2021-12-17T00:38:39.834Z
signature.txt:
signature.txt
0x8d1327a1abbdf172875e5be41706c50fc3bede8af363b67aefbb543d6d082fb76a22057d7cb6d668ceba883f7d70ab7f1dc015b76b51d226af9d610fa20360ad1c
then run
$ iex -S mix
Once in iex, you can then run the following to see the result:
iex> {:ok, msg} = File.read("./message.txt")
...
iex> {:ok, sig} = File.read("./signature.txt")
iex> Siwe.parse_if_valid(String.trim(msg), String.trim(sig))
{:ok, %{
__struct__: Siwe,
address: "0xfA151B5453CE69ABf60f0dbdE71F6C9C5868800E",
chain_id: "1",
domain: "login.xyz",
expiration_time: nil,
issued_at: "2021-12-17T00:38:39.834Z",
nonce: "ToTaLLyRanDOM",
not_before: nil,
request_id: nil,
resources: [],
statement: "Sign-In With Ethereum Example Statement",
uri: "https://login.xyz",
version: "1"
}}
Any valid SIWE message and signature pair can be substituted.The functions described below can also be tested with msg, sig, or a value set to the result Siwe.parse_if_valid.
msg
sig
Siwe.parse_if_valid
Sign-In with Ethereum can be installed as a hex. For more information and package information, click
hex
# Ruby
LibrariesRubyA Ruby implementation of EIP-4361: Sign In With Ethereum.PreviousPythonNextRailsLast updated 3 years ago
A Ruby implementation of EIP-4361: Sign In With Ethereum.
The Ruby implementation of Sign-In with Ethereum can be found here:
Additional packages may be required to install the gem:
brew install automake openssl libtool pkg-config gmp libffi
sudo apt-get install build-essential automake pkg-config libtool \
libffi-dev libssl-dev libgmp-dev python-dev
After installing any required dependencies SIWE can be easily installed with:
gem install siwe
SIWE provides a Message class which implements EIP-4361.
Message
require 'siwe'
require 'time'
# Only the mandatory arguments
Siwe::Message.new("domain.example", "0x9D85ca56217D2bb651b00f15e694EB7E713637D4", "some.uri", "1")
# Complete SIWE message with default values
Siwe::Message.new("domain.example", "0x9D85ca56217D2bb651b00f15e694EB7E713637D4", "some.uri", "1", {
issued_at: Time.now.utc.iso8601,
statement: "Example statement for SIWE",
nonce: Siwe::Util.generate_nonce,
chain_id: "1",
expiration_time: "",
not_before: "",
request_id: "",
resources: []
})
To parse from EIP-4361 you have to use Siwe::Message.from_message
Siwe::Message.from_message
Siwe::Message.from_message "domain.example wants you to sign in with your Ethereum account:\n0x9D85ca56217D2bb651b00f15e694EB7E713637D4\n\nExample statement for SIWE\n\nURI: some.uri\nVersion: 1\nChain ID: 1\nNonce: k1Ne4KWzBHYEFQo8\nIssued At: 2022-02-03T20:06:19Z"
Messages can be parsed to and from JSON strings, using Siwe::Message.from_json_string and Siwe::Message.to_json_string respectively:
Siwe::Message.from_json_string
Siwe::Message.to_json_string
Siwe::Message.from_json_string "{\"domain\":\"domain.example\",\"address\":\"0x9D85ca56217D2bb651b00f15e694EB7E713637D4\",\"uri\":\"some.uri\",\"version\":\"1\",\"chain_id\":\"1\",\"nonce\":\"k1Ne4KWzBHYEFQo8\",\"issued_at\":\"2022-02-03T20:06:19Z\",\"statement\":\"Example statement for SIWE\",\"expiration_time\":\"\",\"not_before\":\"\",\"request_id\":\"\",\"resources\":[]}"
Siwe::Message.new("domain.example", "0x9D85ca56217D2bb651b00f15e694EB7E713637D4", "some.uri", "1").to_json_string
Verification and authentication is performed via EIP-191, using the address field of the SiweMessage as the expected signer. The validate method checks message structural integrity, signature address validity, and time-based validity attributes.
begin
message.validate(signature) # returns true if valid throws otherwise
rescue Siwe::ExpiredMessage
# Used when the message is already expired. (Expires At < Time.now)
rescue Siwe::NotValidMessage
# Used when the message is not yet valid. (Not Before > Time.now)
rescue Siwe::InvalidSignature
# Used when the signature doesn't correspond to the address of the message.
Siwe::Message instances can also be serialized as their EIP-4361 string representations via the Siwe::Message.prepare_message method:
Siwe::Message
Siwe::Message.prepare_message
Siwe::Message.new("domain.example", "0x9D85ca56217D2bb651b00f15e694EB7E713637D4", "some.uri", "1").prepare_message
Parsing and verifying a Siwe::Message:
message = Siwe::Message.from_message "https://example.com wants you to sign in with your Ethereum account:\n0xA712a0AFBFA8656581BfA96352c9EdFc519e9cad\n\n\nURI: https://example.com\nVersion: 1\nChain ID: 1\nNonce: 9WrH24z8zpiYOoBQ\nIssued At: 2022-02-04T15:52:03Z"
message.validate "aca5e5649a357cee608ecbd1a8455b4143311381636b88a66ec7bcaf64b3a4743ff2c7cc18501a3401e182f79233dc73fc56d01506a6098d5e7e4d881bbb02921c"
puts "Congrats, your message is valid"
Sign-In with Ethereum can be found on RubyGems. For more information and package information, click
RubyGems
# Go
LibrariesGoA Go implementation of EIP-4361: Sign In With Ethereum.PreviousRailsNextDiscourseLast updated 3 years ago
A Go implementation of EIP-4361: Sign In With Ethereum.
The Go implementation of Sign-In with Ethereum can be found here:
SIWE can be easily installed in any Go project by running:
go get -u github.com/spruceid/siwe-go
SIWE exposes a Message struct which implements EIP-4361.
Parsing is done via the siwe.ParseMessage function:
siwe.ParseMessage
var message *siwe.Message
var err error
message, err = siwe.ParseMessage(messageStr)
The function will return a nil pointer and an error if there was an issue while parsing.
Verification and Authentication is performed via EIP-191, using the address field of the Message as the expected signer. This returns the Ethereum public key of the signer:
var publicKey *ecdsa.PublicKey
publicKey, err = message.VerifyEIP191(signature)
The time constraints (expiry and not-before) can also be validated, at current or particular times:
if message.ValidNow() {
// ...
// equivalent to
if message.ValidAt(time.Now().UTC()) {
Combined verification of time constraints and authentication can be done in a single call with verify:
// Optional nonce variable to be matched against the
// built message struct being verified
var optionalNonce *string
// Optional timestamp variable to verify at any point
// in time, by default it will use `time.Now()`
var optionalTimestamp *time.Time
publicKey, err = message.Verify(signature, optionalNonce, optionalTimestamp)
// If you won't be using nonce matching and want
// to verify the message at the current time, it's
// safe to pass `nil` in both arguments
publicKey, err = message.Verify(signature, nil, nil)
Message instances can also be serialized as their EIP-4361 string representations via the String method:
String
fmt.Printf("%s", message.String())
To sign messages directly from Go code, you will need to do it like shown below to correctly follow the personal_sign format:
personal_sign
func signHash(data []byte) common.Hash {
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
return crypto.Keccak256Hash([]byte(msg))
func signMessage(message string, privateKey *ecdsa.PrivateKey) ([]byte, error) {
sign := signHash([]byte(message))
signature, err := crypto.Sign(sign.Bytes(), privateKey)
if err != nil {
return nil, err
}
signature[64] += 27
return signature, nil
}
# Discourse
IntegrationsDiscourseA Discourse plugin to enable Sign-In with Ethereum as an authentication method.PreviousGoNextNextAuth.jsLast updated 1 year ago
A Discourse plugin to enable Sign-In with Ethereum as an authentication method.
Last updated 1 year ago
Overview
Discourse is an open-source discussion platform used for most crypto governances and projects to discuss proposals, updates, and research. The following is a quick guide on how to add Sign-In with Ethereum to your existing Discourse.
Note
This guide is currently compatible with . The discussion about the issues with other builds can be followed .
The Sign-In with Ethereum plugin still requires users to enter an email to associate with their accounts after authenticating for the first time. If the user owns an ENS address, it will be the default selected username. Once an email address is associated, users can then sign in using the SIWE option at any time.
Access your container’s app.yml file (present in /var/discourse/containers/)
app.yml
/var/discourse/containers/
cd /var/discourse
nano containers/app.yml
Add the plugin’s repository URL to your container’s app.yml file:
hooks:
before_code: # <-- added
- exec: # <-- added
cmd: # <-- added
- gem install rubyzip # <-- added
after_code:
- exec:
cd: $home/plugins
cmd:
- sudo -E -u discourse git clone https://github.com/discourse/docker_manager.git
- sudo -E -u discourse git clone https://github.com/spruceid/discourse-siwe-auth.git # <-- added
Follow the existing format of the docker_manager.git line; if it does not contain sudo -E -u discourse then insert - git clone https://github.com/spruceid/discourse-siwe-auth.git.
docker_manager.git
sudo -E -u discourse
- git clone https://github.com/spruceid/discourse-siwe-auth.git
Rebuild the container:
./launcher rebuild app
To disable it either remove the plugin or uncheck discourse siwe enabled at (Admin Settings -> Plugins -> discourse-siwe -> discourse siwe enabled ).
discourse siwe enabled
Admin Settings -> Plugins -> discourse-siwe -> discourse siwe enabled
By default, a statement is added to the messages: Sign-in to Discourse via Ethereum. To edit this statement access the settings (same as before) and update it.
To install and enable the plugin on your self-hosted Discourse use the :
This plugin uses the newest Web3Modal v2, in order to use it you need to create a free project id at and configure it in the plugin.
# NextAuth.js
IntegrationsNextAuth.jsA complete open source authentication solution.PreviousDiscourseNextAuth0Last updated 2 years ago
A complete open source authentication solution.
is an easy-to-implement, full-stack (client/server) open-source authentication library originally designed for and serverless applications.
The library provides the ability to set up a custom credential provider, which we can take advantage of in order to authenticate users using their existing Ethereum wallet via Sign-In with Ethereum (EIP-4361).
The complete example can be found .
First clone the official NextAuth.js example using your terminal:
git clone https://github.com/nextauthjs/next-auth-example
Then, switch to the project directory:
cd next-auth-example
After cloning, modify the given .env.local.example file, and populate it with the following variables:
.env.local.example
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=somereallysecretsecret
Note: After this, rename the file to .env.local. This example will be routed to http://localhost:3000.
.env.local
yarn add siwe@beta ethers wagmi
Now, modify pages/_app.tsx to inject the WagmiProvider component:
pages/_app.tsx
WagmiProvider
import { Session } from "next-auth"
import { SessionProvider } from "next-auth/react"
import type { AppProps } from "next/app"
import { WagmiConfig, createClient, configureChains, chain } from "wagmi"
import { publicProvider } from "wagmi/providers/public"
import "./styles.css"
export const { chains, provider } = configureChains(
[chain.mainnet, chain.polygon, chain.optimism, chain.arbitrum],
[publicProvider()]
)
const client = createClient({
autoConnect: true,
provider,
})
// Use of the is mandatory to allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({
Component,
pageProps,
}: AppProps<{
session: Session;
}>) {
return (
)
We're going to now add the provider that will handle the message validation. Since it's not possible to sign in using the default page, the original provider should be removed from the list of providers before rendering. Modify pages/api/auth/[...nextauth].ts with the following:
pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import { getCsrfToken } from "next-auth/react"
import { SiweMessage } from "siwe"
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export default async function auth(req: any, res: any) {
const providers = [
CredentialsProvider({
name: "Ethereum",
credentials: {
message: {
label: "Message",
type: "text",
placeholder: "0x0",
signature: {
label: "Signature",
},
async authorize(credentials) {
try {
const siwe = new SiweMessage(JSON.parse(credentials?.message || "{}"))
const nextAuthUrl = new URL(process.env.NEXTAUTH_URL)
const result = await siwe.verify({
signature: credentials?.signature || "",
domain: nextAuthUrl.host,
nonce: await getCsrfToken({ req }),
})
if (result.success) {
return {
id: siwe.address,
}
return null
} catch (e) {
}),
const isDefaultSigninPage =
req.method === "GET" && req.query.nextauth.includes("signin")
// Hide Sign-In with Ethereum from default sign page
if (isDefaultSigninPage) {
providers.pop()
return await NextAuth(req, res, {
// https://next-auth.js.org/configuration/providers/oauth
providers,
session: {
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async session({ session, token }: { session: any; token: any }) {
session.address = token.sub
session.user.name = token.sub
session.user.image = "https://www.fillmurray.com/128/128"
return session
})
The default sign-in page can't be used because there is no way to hook wagmi to listen for clicks on the default sign-in page provided by next-auth, so a custom page must be created to handle the sign-in flow. Create pages/siwe.tsx and populate it with the following:
pages/siwe.tsx
import { getCsrfToken, signIn, useSession } from "next-auth/react"
import { useAccount, useConnect, useNetwork, useSignMessage } from "wagmi"
import Layout from "../components/layout"
import { InjectedConnector } from 'wagmi/connectors/injected'
import { useEffect, useState } from "react"
function Siwe() {
const { signMessageAsync } = useSignMessage()
const { chain } = useNetwork()
const { address, isConnected } = useAccount()
const { connect } = useConnect({
connector: new InjectedConnector(),
const { data: session, status } = useSession()
const handleLogin = async () => {
const callbackUrl = "/protected"
const message = new SiweMessage({
domain: window.location.host,
address: address,
statement: "Sign in with Ethereum to the app.",
uri: window.location.origin,
version: "1",
chainId: chain?.id,
nonce: await getCsrfToken(),
})
const signature = await signMessageAsync({
message: message.prepareMessage(),
signIn("credentials", {
message: JSON.stringify(message),
redirect: false,
signature,
callbackUrl,
} catch (error) {
window.alert(error)
useEffect(() => {
console.log(isConnected);
if (isConnected && !session) {
handleLogin()
}, [isConnected])
export async function getServerSideProps(context: any) {
return {
props: {
csrfToken: await getCsrfToken(context),
Siwe.Layout = Layout
export default Siwe
Modify pages/styles.css by appending the following CSS:
pages/styles.css
button {
margin: 0 0 0.75rem 0;
text-decoration: none;
padding: 0.7rem 1.4rem;
border: 1px solid #346df1;
background-color: #346df1;
color: #fff;
font-size: 1rem;
border-radius: 4px;
transition: all 0.1s ease-in-out;
font-weight: 500;
position: relative;
button:hover {
cursor: pointer;
box-shadow: inset 0 0 5rem rgb(0 0 0 / 20%);
Finally, modify the components/header.tsx in order to clean it up and add a SIWE tab to navigate to the newly created page:
components/header.tsx
import { signOut, useSession } from "next-auth/react"
import Link from "next/link"
import { useDisconnect } from "wagmi"
import styles from "./header.module.css"
// The approach used in this component shows how to build a sign in and sign out
// component that works on pages which support both client and server side
// rendering, and avoids any flash incorrect content on initial page load.
export default function Header() {
const loading = status === "loading"
const { disconnect } = useDisconnect()