diff --git a/.gitmodules b/.gitmodules index 9d768b7..897eaa8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = packages/did-eth-registry/lib/forge-std url = https://github.com/foundry-rs/forge-std branch = v1.6.1 +[submodule "packages/did-eth-registry/lib/openzeppelin-contracts"] + path = packages/did-eth-registry/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/packages/did-eth-registry/.devcontainer/devcontainer.json b/packages/did-eth-registry/.devcontainer/devcontainer.json index 13dec7c..be63e7d 100644 --- a/packages/did-eth-registry/.devcontainer/devcontainer.json +++ b/packages/did-eth-registry/.devcontainer/devcontainer.json @@ -14,20 +14,22 @@ "JuanBlanco.solidity" ] } + }, + "containerEnv": { + "PRIVATE_KEY": "${localEnv:PRIVATE_KEY}", + "PUBLIC_KEY": "${localEnv:PUBLIC_KEY}", + "GOERLI_URL": "${localEnv:GOERLI_URL}", + "RPC_URL": "${localEnv:RPC_URL}", + "ETHERSCAN_API_KEY": "${localEnv:ETHERSCAN_API_KEY}" } - // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, - // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], - // Uncomment the next line to run commands after the container is created. // "postCreateCommand": "cat /etc/os-release", - // Configure tool-specific properties. // "customizations": {}, - // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "devcontainer" -} +} \ No newline at end of file diff --git a/packages/did-eth-registry/.dockerignore b/packages/did-eth-registry/.dockerignore index 4a52d8f..4e97f53 100644 --- a/packages/did-eth-registry/.dockerignore +++ b/packages/did-eth-registry/.dockerignore @@ -4,3 +4,5 @@ build/ typechain/ typechain-types/ dist/ +EIP1056Registry.json +DIDRegistry.bin diff --git a/packages/did-eth-registry/.gitignore b/packages/did-eth-registry/.gitignore index ac0db26..78ab5a8 100644 --- a/packages/did-eth-registry/.gitignore +++ b/packages/did-eth-registry/.gitignore @@ -183,3 +183,9 @@ dist .svelte-kit # End of https://www.toptal.com/developers/gitignore/api/node,intellij + +broadcast/ + +EIP1056Registry.json +EIP1056RegistryProxy.bin +DIDRegistry.bin diff --git a/packages/did-eth-registry/.solhint.json b/packages/did-eth-registry/.solhint.json index 9f50978..07665e4 100644 --- a/packages/did-eth-registry/.solhint.json +++ b/packages/did-eth-registry/.solhint.json @@ -3,7 +3,7 @@ "rules": { "compiler-version": [ "error", - "^0.8.15" + "^0.8.20" ], "func-visibility": [ "warn", diff --git a/packages/did-eth-registry/.vscode/tasks.json b/packages/did-eth-registry/.vscode/tasks.json index 5b09315..fce71f8 100644 --- a/packages/did-eth-registry/.vscode/tasks.json +++ b/packages/did-eth-registry/.vscode/tasks.json @@ -89,6 +89,97 @@ "isDefault": false } }, + { + "label": "dumpregistry", + "type": "shell", + "command": "yarn dumpregistry", + "options": { + "cwd": "${workspaceFolder}" + }, + "dependsOn": "test", + "group": { + "kind": "test", + "isDefault": false + } + }, + { + "label": "dumpproxy", + "type": "shell", + "command": "yarn dumpproxy", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CONTRACT_ROLE_ADMIN": "0x6CEb0bF1f28ca4165d5C0A04f61DC733987eD6ad", + "REGISTRY_ADDRESS": "0xD1D8360742139fD76C16c397A4d5ECA9E2A73c4b" + } + }, + "dependsOn": "test", + "group": { + "kind": "test", + "isDefault": false + } + }, + { + "label": "registryaddress", + "type": "shell", + "command": "yarn registryaddress", + "options": { + "cwd": "${workspaceFolder}" + }, + "dependsOn": "dumpregistry", + "group": { + "kind": "test", + "isDefault": false + } + }, + { + "label": "vanity", + "type": "shell", + "command": "yarn vanity", + "options": { + "cwd": "${workspaceFolder}" + }, + "dependsOn": "dumpproxy", + "group": { + "kind": "test", + "isDefault": false + } + }, + { + "label": "deployregistry", + "type": "shell", + "command": "forge script ./script/DeployRegistry.s.sol:DeployRegistry --sig 'deploy()' --slow --broadcast --rpc-url ${GOERLI_URL} --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_API_KEY} --verify", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CONTRACT_ROLE_ADMIN": "0x6CEb0bF1f28ca4165d5C0A04f61DC733987eD6ad", + "CONTRACT_ROLE_UPGRADE": "0x6CEb0bF1f28ca4165d5C0A04f61DC733987eD6ad,0x22A653801bB0bb85BE38765cC072144736635eE8", + "CONTRACT_SALT": "0xa650bcc7b18a1b8ee999056c89f3adbe79f5c69bcc68f164e9760a0e0e1b5960", + "REGISTRY_SALT": "0xa3cf9c5bc4043744d6ce20e743000b2a92113cff47d618a20365366f8729a5ba" + } + }, + "dependsOn": "test", + "group": { + "kind": "test", + "isDefault": false + }, + }, + { + "label": "upgraderegistry", + "type": "shell", + "command": "forge script ./script/DeployRegistry.s.sol:DeployRegistry --sig 'upgrade()' --slow --broadcast --rpc-url ${RPC_URL} --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_API_KEY} --verify", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "PROXY_ADDRESS": "0xd1D374DDE031075157fDb64536eF5cC13Ae75000" + } + }, + "dependsOn": "test", + "group": { + "kind": "test", + "isDefault": false + }, + }, { "label": "gas", "type": "shell", diff --git a/packages/did-eth-registry/README.md b/packages/did-eth-registry/README.md index 434a587..ec32c60 100644 --- a/packages/did-eth-registry/README.md +++ b/packages/did-eth-registry/README.md @@ -304,7 +304,14 @@ needed. The list of delegateTypes to include is still to be determined. Iterate through `DIDAttributeChanged` events for service entries, encrypted public keys, and other public names. The attribute names are still to be determined. -## Quick Start +## Deployment Address + +| Contract | Ethereum Address | Network | +| --------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------ | +| EIP1056Registry | 0xd1D374DDE031075157fDb64536eF5cC13Ae75000 | [Sepolia](https://sepolia.etherscan.io/address/0xd1d374dde031075157fdb64536ef5cc13ae75000) | +| EIP1056Registry | 0xd1D374DDE031075157fDb64536eF5cC13Ae75000 | [Görli](https://goerli.etherscan.io/address/0xd1d374dde031075157fdb64536ef5cc13ae75000) | + +## Quick Start (Development) ### Submodules @@ -314,9 +321,9 @@ First, init submodules from the project root $ git submodule update --recursive --init -f ``` -### Registry Development +### Dev Containers Development -This contract supports containerized development. From Visual Studio Code Remote Containers extension +This contract supports containerized development. From Visual Studio Code Dev Containers extension `Reopen in Container` @@ -328,28 +335,75 @@ Command line build using docker $ docker build packages/did-eth-registry -t did-eth:1 ``` -## Deploy contract +## Testing the Contracts -First run, +From the containerized environment: ```bash -$ scripts/generateDeployTxs.js +$ yarn install --frozen-lockfile +$ yarn prettier:check +$ yarn lint +$ forge test -vvv ``` -You will get the data needed to deploy as an output from this command. +## Initial Deployment -Copy the `senderAddress` and send `cost` amount of ether to that address on the Ethereum network you wish to deploy to. +### Using vanity address -Once this funding transaction is confirmed, simply send the `rawTx` to the same network. -`contractAddress` is the address of the deployed contract. +1\. dump out registry init code -`chainId` is intentionally not used in the transaction to make it simpler to deploy to the same address on all networks. +```bash +$ yarn dumpregistry +``` -## Testing the Contracts +2\. compute the address for the registry ```bash -$ yarn install --frozen-lockfile -$ yarn prettier:check -$ yarn lint -$ forge test -v +$ yarn registryaddress +``` + +Make a note of the registry address and registry salt. Convert the salt to a 32 byte hex string using `cast --tobase` + +```bash +$ cast --to-base 74093810807909736385031531802484957602545215186873255285560364360670735082938 16 +0xa3cf9c5bc4043744d6ce20e743000b2a92113cff47d618a20365366f8729a5ba +``` + +3\. dump out the proxy init code + +```bash +CONTRACT_ROLE_ADMIN=0x521DBc90a1687d0ed050Cf4ba47d5A04d8253f46 \ +REGISTRY_ADDRESS=0xD1D8360742139fD76C16c397A4d5ECA9E2A73c4b \ +yarn dumpproxy +``` + +4\. run the create2 function to choose a vanity address for the proxy + +```bash +$ yarn vanity +``` + +Make a note of the salt for the proxy contract. Convert the salt to a 32 byte hex string using cast --to-base + +```bash +cast --to-base 75226583542044164422273476230409956855417563147003477807955548159938739788128 16 +0xa650bcc7b18a1b8ee999056c89f3adbe79f5c69bcc68f164e9760a0e0e1b5960 +``` + +5. Deploy the registry and proxy + +```bash +$ CONTRACT_ROLE_ADMIN=0x521DBc90a1687d0ed050Cf4ba47d5A04d8253f46 \ +CONTRACT_ROLE_UPGRADE="0x521DBc90a1687d0ed050Cf4ba47d5A04d8253f46,0x2746bC0bE84D7CC9A63526C02746d12FA20621F3" \ +CONTRACT_SALT=0xa650bcc7b18a1b8ee999056c89f3adbe79f5c69bcc68f164e9760a0e0e1b5960 \ +REGISTRY_SALT=0xa3cf9c5bc4043744d6ce20e743000b2a92113cff47d618a20365366f8729a5ba \ +forge script ./script/DeployRegistry.s.sol:DeployRegistry --sig 'deploy()' --slow --broadcast --rpc-url ${RPC_URL} --private-key ${PRIVATE_KEY} --etherscan-api-key ${ETHERSCAN_API_KEY} --verify +``` + +### Upgrade Deployment + +Post initial deployment, the contract can be upgraded directly through the [UUPSUpgradeable](https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable) interface + +```solidity + registry.upgradeToAndCall(address(logic), ""); ``` diff --git a/packages/did-eth-registry/contracts/DIDRegistry.sol b/packages/did-eth-registry/contracts/DIDRegistry.sol index 5a07612..da323ca 100644 --- a/packages/did-eth-registry/contracts/DIDRegistry.sol +++ b/packages/did-eth-registry/contracts/DIDRegistry.sol @@ -1,6 +1,13 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +pragma solidity ^0.8.20; + +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; +import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import { EIP1056Registry } from "./EIP1056Registry.sol"; @@ -8,7 +15,9 @@ import { EIP1056Registry } from "./EIP1056Registry.sol"; * @title DIDRegistry * @dev The DIDRegistry contract is a registry of decentralized identifiers (DIDs) and their attributes. */ -contract DIDRegistry is EIP1056Registry { +contract DIDRegistry is EIP1056Registry, Initializable, UUPSUpgradeable, AccessControl { + bytes32 public constant REGISTRY_ADMIN_ROLE = keccak256("REGISTRY_ADMIN_ROLE"); + mapping(address => address) public owners; mapping(address => mapping(bytes32 => mapping(address => uint256))) public delegates; mapping(address => uint256) public changed; @@ -19,6 +28,16 @@ contract DIDRegistry is EIP1056Registry { _; } + /// @dev constructor is forbidden for upgradeable contracts + constructor() { + _disableInitializers(); + } + + /// @dev initializer is required for proxy contracts + function initialize(address _roleAdmin) public initializer { + _grantRole(DEFAULT_ADMIN_ROLE, _roleAdmin); + } + /** * Return the current owner of an identity. * @param identity The identity to check. @@ -280,6 +299,23 @@ contract DIDRegistry is EIP1056Registry { _revokeAttribute(identity, _checkSignature(identity, sigV, sigR, sigS, digest), name, value); } + /** + * @notice authorize code upgrade or revert + * @dev required by UUPSUpgradeable + */ + // solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade(address) internal override onlyRole(REGISTRY_ADMIN_ROLE) {} + + /** + * @dev required by ERC165 + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl) returns (bool) { + return + interfaceId == type(EIP1056Registry).interfaceId || + interfaceId == type(IAccessControl).interfaceId || + super.supportsInterface(interfaceId); + } + /** * @dev check signature of the owner * @param identity address of the identity diff --git a/packages/did-eth-registry/contracts/EIP1056Registry.sol b/packages/did-eth-registry/contracts/EIP1056Registry.sol index db6a4b5..9a0b903 100644 --- a/packages/did-eth-registry/contracts/EIP1056Registry.sol +++ b/packages/did-eth-registry/contracts/EIP1056Registry.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +pragma solidity ^0.8.20; /** * @title EIP1056Registry diff --git a/packages/did-eth-registry/contracts/EIP1056RegistryProxy.sol b/packages/did-eth-registry/contracts/EIP1056RegistryProxy.sol new file mode 100644 index 0000000..19b7096 --- /dev/null +++ b/packages/did-eth-registry/contracts/EIP1056RegistryProxy.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ERC1967Utils } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; + +import { EIP1056RegistryProxy } from "./EIP1056RegistryProxy.sol"; +import { DIDRegistry } from "./DIDRegistry.sol"; + +contract EIP1056RegistryProxy is ERC1967Proxy { + constructor(address _logic, address _roleAdmin) + ERC1967Proxy(_logic, abi.encodeWithSignature("initialize(address)", _roleAdmin)) + // solhint-disable-next-line no-empty-blocks + { + + } +} + +// solhint-disable-next-line func-visibility +function createEIP1056Registry(address _roleAdmin) returns (address) { + DIDRegistry logic = new DIDRegistry(); // logic contract + EIP1056RegistryProxy proxy = new EIP1056RegistryProxy(address(logic), _roleAdmin); + return address(proxy); +} diff --git a/packages/did-eth-registry/foundry.toml b/packages/did-eth-registry/foundry.toml index 07e9aa4..216de08 100644 --- a/packages/did-eth-registry/foundry.toml +++ b/packages/did-eth-registry/foundry.toml @@ -2,5 +2,6 @@ src = "contracts" out = "out" libs = ["node_modules", "lib"] +fs_permissions = [{access = "write", path = "./"}] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/packages/did-eth-registry/lib/openzeppelin-contracts b/packages/did-eth-registry/lib/openzeppelin-contracts new file mode 160000 index 0000000..932fddf --- /dev/null +++ b/packages/did-eth-registry/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 932fddf69a699a9a80fd2396fd1a2ab91cdda123 diff --git a/packages/did-eth-registry/package.json b/packages/did-eth-registry/package.json index 88930cd..f93ce79 100644 --- a/packages/did-eth-registry/package.json +++ b/packages/did-eth-registry/package.json @@ -3,6 +3,24 @@ "version": "0.0.1", "description": "A repository storing keys and other data about Ethereum Decentralized Identifiers (DIDs)", "main": "./dist/index.js", + "scripts": { + "build:js": "tsc", + "build:sol": "hardhat compile", + "build": "yarn build:sol && yarn build:js", + "test": "hardhat test", + "prettier:check": "prettier --check '(contracts|test|script)/**.sol'", + "prettier:fix": "prettier --write '(contracts|test|script)/**.sol'", + "lint:js": "npx eslint '**/*.{js,ts}' --fix", + "lint:sol": "npx solhint 'contracts/**/*.sol' --fix", + "lint": "yarn lint:sol && yarn lint:js", + "format": "prettier -w '**/*.{ts,js,json,md}'", + "prepublishOnly": "yarn test && yarn format && yarn lint", + "release": "semantic-release --debug", + "dumpregistry": "forge script ./script/DeployRegistry.s.sol --sig 'dumpRegistry()'", + "dumpproxy": "forge script ./script/DeployRegistry.s.sol --sig 'dumpProxy()'", + "registryaddress": "cast create2 --starts-with 0xd1d836 --deployer 0x4e59b44847b379578588920cA78FbF26c0B4956C --init-code $(cat DIDRegistry.bin)", + "vanity": "cast create2 --starts-with 0xd1d374 --ends-with 00 --deployer 0x4e59b44847b379578588920cA78FbF26c0B4956C --init-code $(cat EIP1056RegistryProxy.bin)" + }, "exports": "./dist/index.js", "typesVersions": { "*": { @@ -61,24 +79,11 @@ "typescript": "^4.7.4", "uint8arrays": "^3.0.0" }, - "scripts": { - "build:js": "tsc", - "build:sol": "hardhat compile", - "build": "yarn build:sol && yarn build:js", - "test": "hardhat test", - "prettier:check": "prettier --check '(contracts|test|script)/**.sol'", - "prettier:fix": "prettier --write '(contracts|test|script)/**.sol'", - "lint:js": "npx eslint '**/*.{js,ts}' --fix", - "lint:sol": "npx solhint 'contracts/**/*.sol' --fix", - "lint": "yarn lint:sol && yarn lint:js", - "format": "prettier -w '**/*.{ts,js,json,md}'", - "prepublishOnly": "yarn test && yarn format && yarn lint", - "release": "semantic-release --debug" - }, "author": "Pelle Braendgaard", "contributors": [ "Mircea Nistor ", - "Nick Reynolds " + "Nick Reynolds ", + "John Cairns " ], "license": "MIT", "repository": { diff --git a/packages/did-eth-registry/script/DeployRegistry.s.sol b/packages/did-eth-registry/script/DeployRegistry.s.sol new file mode 100644 index 0000000..0845bec --- /dev/null +++ b/packages/did-eth-registry/script/DeployRegistry.s.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import { Script } from "forge-std/Script.sol"; + +import { DIDRegistry } from "../contracts/DIDRegistry.sol"; +import { EIP1056RegistryProxy } from "../contracts/EIP1056RegistryProxy.sol"; +import { EIP1056Registry } from "../contracts/EIP1056Registry.sol"; + +/** + * @notice - This is the script for deploying DIDRegistry + */ +contract DeployRegistry is Script { + event RegistryDeployment(address proxy); + + /** + * @notice - save the bytecode for DIDRegistry to a file + * @dev requires fs_permissions + */ + function dumpRegistry() external { + bytes memory createCode = abi.encodePacked(type(DIDRegistry).creationCode); + vm.writeFile("./DIDRegistry.bin", vm.toString(createCode)); + } + + /** + * @notice - save the bytecode for EIP1056RegistryProxy to a file + * @dev requires fs_permissions + */ + function dumpProxy() external { + address _contractRoleAdmin = vm.envAddress("CONTRACT_ROLE_ADMIN"); + address _registryAddress = vm.envAddress("REGISTRY_ADDRESS"); + bytes memory createCode = abi.encodePacked( + type(EIP1056RegistryProxy).creationCode, + abi.encode(_registryAddress, _contractRoleAdmin) + ); + vm.writeFile("./EIP1056RegistryProxy.bin", vm.toString(createCode)); + } + + /** + * @notice - deploy DIDRegistry and Proxy + */ + function deploy() external { + vm.startBroadcast(); + address _contractRoleAdmin = vm.envAddress("CONTRACT_ROLE_ADMIN"); + address[] memory _contractUpgradeAdmin = vm.envAddress("CONTRACT_ROLE_UPGRADE", ","); + bytes32 _registrySalt = vm.envBytes32("REGISTRY_SALT"); + bytes32 _vanitySalt = vm.envBytes32("CONTRACT_SALT"); + DIDRegistry logic = new DIDRegistry{ salt: _registrySalt }(); + EIP1056RegistryProxy proxy = new EIP1056RegistryProxy{ salt: _vanitySalt }(address(logic), _contractRoleAdmin); + address proxyAddress = address(proxy); + DIDRegistry registry = DIDRegistry(proxyAddress); + for (uint256 i = 0; i < _contractUpgradeAdmin.length; i++) { + address admin = _contractUpgradeAdmin[i]; + registry.grantRole(logic.REGISTRY_ADMIN_ROLE(), admin); + } + emit RegistryDeployment(proxyAddress); + vm.stopBroadcast(); + } + + /** + * @notice - upgrade DIDRegistry Proxy + */ + function upgrade() external { + vm.startBroadcast(); + address _proxyAddress = vm.envAddress("PROXY_ADDRESS"); + DIDRegistry logic = new DIDRegistry(); + DIDRegistry registry = DIDRegistry(_proxyAddress); + registry.upgradeToAndCall(address(logic), ""); + vm.stopBroadcast(); + } +} diff --git a/packages/did-eth-registry/scripts/deploy.ts b/packages/did-eth-registry/scripts/deploy.ts deleted file mode 100644 index d63a658..0000000 --- a/packages/did-eth-registry/scripts/deploy.ts +++ /dev/null @@ -1,30 +0,0 @@ -// We require the Hardhat Runtime Environment explicitly here. This is optional -// but useful for running the script in a standalone fashion through `node