-
Notifications
You must be signed in to change notification settings - Fork 23
Add ERC-7969 compliant DKIMRegistry #184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ernestognw
wants to merge
4
commits into
master
Choose a base branch
from
feat/ERC-7969
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.20; | ||
|
||
/** | ||
* @title ERC-7969 DKIM Registry Interface. | ||
* | ||
* @dev This interface provides a standard way to register and validate DKIM public key hashes onchain | ||
* Domain owners can register their DKIM public key hashes and third parties can verify their validity | ||
* The interface enables email-based account abstraction and secure account recovery mechanisms. | ||
* | ||
* NOTE: The ERC-165 identifier for this interface is `0xdee3d600`. | ||
*/ | ||
interface IDKIMRegistry { | ||
/// @dev Emitted when a new DKIM public key hash is registered for a domain | ||
/// @param domainHash The keccak256 hash of the lowercase domain name | ||
/// @param keyHash The keccak256 hash of the DKIM public key | ||
event KeyHashRegistered(bytes32 domainHash, bytes32 keyHash); | ||
|
||
/// @dev Emitted when a DKIM public key hash is revoked for a domain | ||
/// @param domainHash The keccak256 hash of the domain name | ||
event KeyHashRevoked(bytes32 domainHash); | ||
|
||
/// @dev Checks if a DKIM key hash is valid for a given domain | ||
/// @param domainHash The keccak256 hash of the lowercase domain name | ||
/// @param keyHash The keccak256 hash of the DKIM public key | ||
/// @return True if the key hash is valid for the domain, false otherwise | ||
function isKeyHashValid(bytes32 domainHash, bytes32 keyHash) external view returns (bool); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.26; | ||
|
||
import {IDKIMRegistry} from "../../interfaces/IERC7969.sol"; | ||
|
||
/** | ||
* @dev Implementation of {IDKIMRegistry} for registering and validating DKIM public key hashes onchain. | ||
* | ||
* This contract provides a standard way to register and validate DKIM public key hashes, enabling | ||
* email-based account abstraction and secure account recovery mechanisms. Domain owners can register | ||
* their DKIM public key hashes and third parties can verify their validity. | ||
* | ||
* The contract stores mappings of domain hashes to DKIM public key hashes, where: | ||
* | ||
* * Domain hash: keccak256 hash of the lowercase domain name | ||
* * Key hash: keccak256 hash of the DKIM public key | ||
* | ||
* Example of usage: | ||
* | ||
* ```solidity | ||
* contract MyDKIMRegistry is DKIMRegistry, Ownable { | ||
* function setKeyHash(bytes32 domainHash, bytes32 keyHash) public onlyOwner { | ||
* _setKeyHash(domainHash, keyHash); | ||
* } | ||
* | ||
* function setKeyHashes(bytes32 domainHash, bytes32[] memory keyHashes) public onlyOwner { | ||
* _setKeyHashes(domainHash, keyHashes); | ||
* } | ||
* | ||
* function revokeKeyHash(bytes32 domainHash, bytes32 keyHash) public onlyOwner { | ||
* _revokeKeyHash(domainHash, keyHash); | ||
* } | ||
* } | ||
* ``` | ||
*/ | ||
abstract contract DKIMRegistry is IDKIMRegistry { | ||
/// @dev Mapping from domain hash to key hash to validity status | ||
mapping(bytes32 domainHash => mapping(bytes32 keyHash => bool)) private _keyHashes; | ||
|
||
/// @dev Returns whether a DKIM key hash is valid for a given domain. | ||
function isKeyHashValid(bytes32 domainHash, bytes32 keyHash) public view returns (bool) { | ||
return _keyHashes[domainHash][keyHash]; | ||
} | ||
|
||
/** | ||
* @dev Sets a DKIM key hash as valid for a domain. Internal version without access control. | ||
* | ||
* Emits a {KeyHashRegistered} event. | ||
* | ||
* NOTE: This function does not validate that keyHash is non-zero. Consider adding | ||
* validation in derived contracts if needed. | ||
*/ | ||
function _setKeyHash(bytes32 domainHash, bytes32 keyHash) internal { | ||
_keyHashes[domainHash][keyHash] = true; | ||
emit KeyHashRegistered(domainHash, keyHash); | ||
} | ||
|
||
/** | ||
* @dev Sets multiple DKIM key hashes as valid for a domain in a single transaction. | ||
* Internal version without access control. | ||
* | ||
* Emits a {KeyHashRegistered} event for each key hash. | ||
* | ||
* NOTE: This function does not validate that the keyHashes array is non-empty. | ||
* Consider adding validation in derived contracts if needed. | ||
*/ | ||
function _setKeyHashes(bytes32 domainHash, bytes32[] memory keyHashes) internal { | ||
for (uint256 i = 0; i < keyHashes.length; ++i) { | ||
_setKeyHash(domainHash, keyHashes[i]); | ||
} | ||
} | ||
|
||
/** | ||
* @dev Revokes a DKIM key hash for a domain, making it invalid. | ||
* Internal version without access control. | ||
* | ||
* Emits a {KeyHashRevoked} event. | ||
*/ | ||
function _revokeKeyHash(bytes32 domainHash, bytes32 keyHash) internal { | ||
delete _keyHashes[domainHash][keyHash]; | ||
emit KeyHashRevoked(domainHash); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
const { expect } = require('chai'); | ||
const { ethers } = require('hardhat'); | ||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); | ||
|
||
const DOMAIN_EXAMPLE_COM = ethers.keccak256(ethers.toUtf8Bytes('example.com')); | ||
const DOMAIN_EXAMPLE_ORG = ethers.keccak256(ethers.toUtf8Bytes('example.org')); | ||
const DOMAIN_SUBDOMAIN = ethers.keccak256(ethers.toUtf8Bytes('mail.example.com')); | ||
|
||
const KEY_HASH_1 = ethers.keccak256(ethers.toUtf8Bytes('dkim_public_key_1')); | ||
const KEY_HASH_2 = ethers.keccak256(ethers.toUtf8Bytes('dkim_public_key_2')); | ||
const KEY_HASH_3 = ethers.keccak256(ethers.toUtf8Bytes('dkim_public_key_3')); | ||
const ZERO_HASH = ethers.ZeroHash; | ||
|
||
async function fixture() { | ||
const registry = await ethers.deployContract('$DKIMRegistry'); | ||
|
||
return { registry }; | ||
} | ||
|
||
describe('DKIMRegistry', function () { | ||
beforeEach(async function () { | ||
Object.assign(this, await loadFixture(fixture)); | ||
}); | ||
|
||
describe('isKeyHashValid', function () { | ||
it('should return false for unregistered key hash', async function () { | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.false; | ||
}); | ||
|
||
it('should return true for registered key hash', async function () { | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
}); | ||
|
||
it('should return false for different domain with same key hash', async function () { | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_ORG, KEY_HASH_1)).to.eventually.be.false; | ||
}); | ||
|
||
it('should return false for same domain with different key hash', async function () { | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_2)).to.eventually.be.false; | ||
}); | ||
|
||
it('should handle zero hash values', async function () { | ||
await expect(this.registry.isKeyHashValid(ZERO_HASH, ZERO_HASH)).to.eventually.be.false; | ||
|
||
await this.registry.$_setKeyHash(ZERO_HASH, ZERO_HASH); | ||
await expect(this.registry.isKeyHashValid(ZERO_HASH, ZERO_HASH)).to.eventually.be.true; | ||
}); | ||
}); | ||
|
||
describe('_setKeyHash', function () { | ||
it('should set a key hash for a domain', async function () { | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
}); | ||
|
||
it('should emit KeyHashRegistered event', async function () { | ||
await expect(this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1)) | ||
.to.emit(this.registry, 'KeyHashRegistered') | ||
.withArgs(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
}); | ||
|
||
it('should allow setting multiple key hashes for same domain', async function () { | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_2); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_2)).to.eventually.be.true; | ||
}); | ||
|
||
it('should allow setting same key hash for different domains', async function () { | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_ORG, KEY_HASH_1); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_ORG, KEY_HASH_1)).to.eventually.be.true; | ||
}); | ||
|
||
it('should handle subdomains independently', async function () { | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await this.registry.$_setKeyHash(DOMAIN_SUBDOMAIN, KEY_HASH_2); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_SUBDOMAIN, KEY_HASH_2)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_2)).to.eventually.be.false; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_SUBDOMAIN, KEY_HASH_1)).to.eventually.be.false; | ||
}); | ||
|
||
it('should allow setting zero hash', async function () { | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, ZERO_HASH); | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, ZERO_HASH)).to.eventually.be.true; | ||
}); | ||
|
||
it('should overwrite existing key hash', async function () { | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); // Set again | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
}); | ||
}); | ||
|
||
describe('_setKeyHashes', function () { | ||
it('should set multiple key hashes for a domain', async function () { | ||
const keyHashes = [KEY_HASH_1, KEY_HASH_2, KEY_HASH_3]; | ||
await this.registry.$_setKeyHashes(DOMAIN_EXAMPLE_COM, keyHashes); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_2)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_3)).to.eventually.be.true; | ||
}); | ||
|
||
it('should emit KeyHashRegistered event for each key hash', async function () { | ||
const keyHashes = [KEY_HASH_1, KEY_HASH_2]; | ||
const tx = this.registry.$_setKeyHashes(DOMAIN_EXAMPLE_COM, keyHashes); | ||
|
||
await expect(tx).to.emit(this.registry, 'KeyHashRegistered').withArgs(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await expect(tx).to.emit(this.registry, 'KeyHashRegistered').withArgs(DOMAIN_EXAMPLE_COM, KEY_HASH_2); | ||
}); | ||
|
||
it('should handle single key hash in array', async function () { | ||
const keyHashes = [KEY_HASH_1]; | ||
await this.registry.$_setKeyHashes(DOMAIN_EXAMPLE_COM, keyHashes); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
}); | ||
|
||
it('should handle empty array', async function () { | ||
const keyHashes = []; | ||
await this.registry.$_setKeyHashes(DOMAIN_EXAMPLE_COM, keyHashes); | ||
|
||
// No key hashes should be set | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.false; | ||
}); | ||
|
||
it('should handle duplicate key hashes in array', async function () { | ||
const keyHashes = [KEY_HASH_1, KEY_HASH_1, KEY_HASH_2]; | ||
await this.registry.$_setKeyHashes(DOMAIN_EXAMPLE_COM, keyHashes); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_2)).to.eventually.be.true; | ||
}); | ||
|
||
it('should handle array with zero hash', async function () { | ||
const keyHashes = [KEY_HASH_1, ZERO_HASH, KEY_HASH_2]; | ||
await this.registry.$_setKeyHashes(DOMAIN_EXAMPLE_COM, keyHashes); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, ZERO_HASH)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_2)).to.eventually.be.true; | ||
}); | ||
}); | ||
|
||
describe('_revokeKeyHash', function () { | ||
beforeEach(async function () { | ||
// Set up some key hashes to revoke | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_2); | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_ORG, KEY_HASH_1); | ||
}); | ||
|
||
it('should revoke a key hash for a domain', async function () { | ||
await this.registry.$_revokeKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.false; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_2)).to.eventually.be.true; // Other key should remain | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_ORG, KEY_HASH_1)).to.eventually.be.true; // Same key on different domain should remain | ||
}); | ||
|
||
it('should emit KeyHashRevoked event', async function () { | ||
await expect(this.registry.$_revokeKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1)) | ||
.to.emit(this.registry, 'KeyHashRevoked') | ||
.withArgs(DOMAIN_EXAMPLE_COM); | ||
}); | ||
|
||
it('should handle revoking non-existent key hash', async function () { | ||
await this.registry.$_revokeKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_3); | ||
|
||
// Should not affect existing key hashes | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_2)).to.eventually.be.true; | ||
}); | ||
|
||
it('should handle revoking from non-existent domain', async function () { | ||
const nonExistentDomain = ethers.keccak256(ethers.toUtf8Bytes('nonexistent.com')); | ||
await this.registry.$_revokeKeyHash(nonExistentDomain, KEY_HASH_1); | ||
|
||
// Should not affect existing key hashes | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
}); | ||
|
||
it('should handle revoking zero hash', async function () { | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, ZERO_HASH); | ||
await this.registry.$_revokeKeyHash(DOMAIN_EXAMPLE_COM, ZERO_HASH); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, ZERO_HASH)).to.eventually.be.false; | ||
}); | ||
|
||
it('should allow re-setting a revoked key hash', async function () { | ||
await this.registry.$_revokeKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.false; | ||
|
||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
}); | ||
}); | ||
|
||
it('should handle multiple domains with overlapping key hashes', async function () { | ||
// Set same key hash for multiple domains | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
await this.registry.$_setKeyHash(DOMAIN_EXAMPLE_ORG, KEY_HASH_1); | ||
await this.registry.$_setKeyHash(DOMAIN_SUBDOMAIN, KEY_HASH_1); | ||
|
||
// Verify all are valid | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_ORG, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_SUBDOMAIN, KEY_HASH_1)).to.eventually.be.true; | ||
|
||
// Revoke from one domain only | ||
await this.registry.$_revokeKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_1); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.false; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_ORG, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_SUBDOMAIN, KEY_HASH_1)).to.eventually.be.true; | ||
}); | ||
|
||
it('should handle batch operations followed by individual revocations', async function () { | ||
const keyHashes = [KEY_HASH_1, KEY_HASH_2, KEY_HASH_3]; | ||
await this.registry.$_setKeyHashes(DOMAIN_EXAMPLE_COM, keyHashes); | ||
|
||
// Verify all are set | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_2)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_3)).to.eventually.be.true; | ||
|
||
// Revoke middle key hash | ||
await this.registry.$_revokeKeyHash(DOMAIN_EXAMPLE_COM, KEY_HASH_2); | ||
|
||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_1)).to.eventually.be.true; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_2)).to.eventually.be.false; | ||
await expect(this.registry.isKeyHashValid(DOMAIN_EXAMPLE_COM, KEY_HASH_3)).to.eventually.be.true; | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @0xknon, we generally mark contracts as
abstract
to make it clear that it's not our intention for people deploy them directly. Note it's missing thepublic
setKeyHash
,setKeyHashes
andrevokeKeyHash
functions that require access control.Alternatively, we can define this functions within the contract but add a modifier with an internal virtual
_checkKeyHashUpdate
:It would be still abstract, but maybe closer to a straightforward deployment. wdyt?