Skip to content

Refactor PermissionedResolver (Idea 2)#255

Draft
adraffy wants to merge 15 commits intomainfrom
feat/permres-fs
Draft

Refactor PermissionedResolver (Idea 2)#255
adraffy wants to merge 15 commits intomainfrom
feat/permres-fs

Conversation

@adraffy
Copy link
Copy Markdown
Member

@adraffy adraffy commented Mar 20, 2026

  • updated DNSTXTResolver
    • added IDataResolver (ENSIP-24) support
  • added IRecordResolver
    • new ENSv2 events
  • added IResolverSetters
    • new ENSv2 setters
  • refactored IPermissionedResolver
    • removed aliasing
    • removed IExtendedResolver
    • added IDataResolver (ENSIP-24) support
    • added link(name, node), and getRecordId(node)
    • replaced grantNamedRoles() with grantRecordRoles(name, roles, account)
    • replaced grantNamed{Text|Addr}Roles() with grantSetterRoles(setter, account)
      • fine-grained permissions apply to IAddressResolver, ITextResolver, IDataResolver, IABIResolver, and IInterfaceResolver

  • updated devnet and tests to account for changes
  • updated resolutions.ts
    • changed write to writeV1
    • added writeV2

// inodes created automatically
resolver.setAddress(dnsEncode("raffy.eth"), 60, abi.encodePacked(0x5105));

// link: chonk.eth <=> raffy.eth:
resolver.link(dnsEncode("chonk.eth"), namehash("raffy.eth"));

// edit: linked => modifies same record as raffy.eth
resolver.setAddress(dnsEncode("chonk.eth"), 60, abi.encodePacked(0xdead));

// clears the underlying record => raffy.eth and chonk.eth are cleared
resolver.clear(dnsEncode("chonk.eth"));

// unlink => raffy.eth "deleted", chonk.eth unaffected
resolver.link(dnsEncode("raffy.eth"), bytes32(0));

// default record
resolver.setAddress(dnsEncode(""), 60, abi.encodePacked(0x1234));

// find an inode
uint256 recordId = resolver.getRecordId(namehash("raffy.eth'));

Records are created automatically by setters and assigned internal ID numbers (starting at 1).
To create a new record, `ROLE_NEW_RECORD` is required on root.
`getRecordId(node)` reveals the internal record ID.

`link(name, node)` makes `name` use the record currently used by `node`.
To link an existing record, `ROLE_LINK_RECORD` is required on root.

`link(name, bytes32(0))` unlinks `name` from the record.
Once a record is no longer referenced, it becomes unreachable and is effectively deleted.

`clear(name)` reset the record and requires `ROLE_CLEAR_RECORD` on the name or root.

Names without a record fall back to the default record, which can be updated using the empty name (`0x00`).

Every record setter has the form: `f(name, ...)`

Every record setter has a corresponding role:
| Function           | Role                   |
| ------------------ | ---------------------- |
| `setABI()`         | `ROLE_SET_ABI`         |
| `setAddress()`     | `ROLE_SET_ADDRESS`     |
| `setContentHash()` | `ROLE_SET_CONTENTHASH` |
| `setData()`        | `ROLE_SET_DATA`        |
| `setInterface()`   | `ROLE_SET_INTERFACE`   |
| `setName()`        | `ROLE_SET_NAME`        |
| `setText()`        | `ROLE_SET_TEXT`        |

Record setters can be granted with `getRecordRoles()` and are annotated
using ABI-encoded calldata: `abi.encodeWithSelector(bytes4(0), name)`.

Fine-grained record setters have the form: `f(name, <arg>, ...)`
They can be granted with `grantSetterRoles()` and are annotated
using truncated ABI-encoded calldata:
* w/data: `abi.encodeCall(setAddr, (name, coinType, "..."))`
* w/o data: `abi.encodeCall(setAddr, (name coinType, ""))`
* truncated: `abi.encodeWithSelector(setAddr.selector, name, coinType)`
The `roleBitmap` can be derived from the setter selector.

The following setters are fine-grained:
* `setAddress(name, coinType, ...)`
* `setData(name, key, ...)`
* `setText(name, key, ...)`
* `setABI(name, contentType, ...)`
* `setInterface(name, interfaceId, ...)`

The argument is hashed accordingly:
| Argument      | Part                                    |
| ------------- | --------------------------------------- |
| `uint256 arg` | `PermissionedResolverLib.partHash(arg)` |
| `string arg`  | `PermissionedResolverLib.partHash(arg)` |
| `bytes4 arg`  | `PermissionedResolverLib.partHash(arg)` |

Record setters check (4) EAC resources:
                                                     Part Hash
            Resources      +-----------------------------+----------------------------------+
                           |           Any (*)           |           Specific (1)           |
            +--------------+-----------------------------+----------------------------------+
            |      Any (*) |       resource(0, 0)        |      resource(0, <partHash>)     |
 Record ID  |--------------+-----------------------------+----------------------------------+
            | Specific (1) |   resource(<recordId>, 0)   | resource(<recordId>, <partHash>) |
            +--------------+-----------------------------+----------------------------------+

eg. `setText(name, "key", ...)` with `recordId = getRecordId(namehash(name))`
     will check the following resources for `ROLE_SET_TEXT` permission:
1. `resource(recordId, partHash("key"))` => `arg="key"` for that record
2. `resource(recordId, 0)` => ANY part of that record
3. `resource(0, partHash("key"))` => `arg="key"` for ANY record
4. `resource(0, 0)` => ANY part of ANY record

@adraffy adraffy requested a review from Arachnid April 15, 2026 08:38
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be part of this PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing context?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, adding IDataResolver to DNSTXTResolver

import {IResolverSetters} from "./IResolverSetters.sol";

/// @dev The complete interface selector: `0xff38f248`
bytes4 constant RECORD_RESOLVER_INTERFACE_ID = type(IResolverSetters).interfaceId ^
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this accessible via type(IRecordResolver).interfaceId?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added those because the inherited interfaces don't contain the inherited functions.

Possibly, we need this "complete interface" logic in other places in ENSv2. ENSv1 didn't use any? inherited interfaces.

Comment thread contracts/src/resolver/libraries/PermissionedResolverLib.sol Outdated
Comment on lines +156 to +157
/// @dev Mapping from `recordId` to `version`.
mapping(uint256 recordId => uint256 version) internal _versions;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought part of the idea was that we eliminate the need for versions altogether? When we want to reset a record, we can just rm it, and create a new one with a fresh inode; no need for a versions mapping.

Copy link
Copy Markdown
Member Author

@adraffy adraffy Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that too, but the problem I ran into was unlink() vs clear()

  • link(name, 0) disassociates from inode from just name
  • clear(name) clears the inode, but it doesn't know how to update all of the other pointers
    • eg. inode["a"] = 1, inode["b"] = 1, clear("a"), inode["a"] = 2 — how do I update "b"?

So a separate level of indirection (version again) solves this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants