├── .editorconfig ├── .env.example ├── .github └── workflows │ └── on-push.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .solhint.json ├── .solhintignore ├── Dockerfile ├── LICENSE ├── README.md ├── abi └── index.ts ├── architcture.jpeg ├── contracts ├── ERC3668Resolver.sol ├── IExtendedResolver.sol ├── IMetadataResolver.sol ├── SupportsInterface.sol ├── coinType │ └── Ensip11CoinType.sol ├── test-contract │ └── ProofServiceTestContract.sol └── verifier │ ├── CcipResponseVerifier.sol │ ├── ICcipResponseVerifier.sol │ ├── optimism-bedrock │ ├── BedrockCcipVerifier.sol │ ├── BedrockProofVerifier.sol │ └── IBedrockProofVerifier.sol │ └── signature │ ├── SignatureCcipVerifier.sol │ └── SignatureVerifier.sol ├── deploy ├── 01_BedrockProofVerifier.ts ├── 02_BedrockCcipVerifier.ts ├── 03_ERC3668Resolver.ts └── 04_SignatureCcipVerifier.ts ├── docker-compose.yml ├── gateway ├── config │ ├── Config.ts │ └── ConfigReader.ts ├── handler │ ├── optimism-bedrock │ │ └── optimismBedrockHandler.ts │ └── signing │ │ ├── signAndEncodeResponse.ts │ │ └── signingHandler.ts ├── http │ └── ccipGateway.ts ├── index.ts └── service │ ├── encoding │ └── proof │ │ └── getProofParamType.ts │ └── proof │ ├── ProofService.ts │ ├── toRpcHexString.ts │ └── types.ts ├── hardhat.config.ts ├── index.js ├── package.json ├── renovate.json ├── scripts └── setupEnvironment.ts ├── test ├── chai-setup.ts ├── contracts │ ├── BedrockCcipVerifier.test.ts │ ├── ERC3668Resolver.test.ts │ └── SignatureCcipVerifier.test.ts ├── e2e │ ├── ProofService.test.ts │ └── optimismBedrockHandler.test.ts ├── gateway │ ├── readConfig.test.ts │ └── signatureHandler.test.ts └── helper │ ├── encodeEnsName.ts │ └── getGatewayUrl.ts ├── tsconfig-build.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | # EditorConfig is awesome: https://EditorConfig.org 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 4 10 | insert_final_newline = true 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | 2 | GOERLI_RPC_URL="" 3 | DEPLOYER_PRIVATE_KEY="" 4 | 5 | ETHERSCAN_API_KEY="" 6 | OPTIMISTIC_ETHERSCAN_API_KEY="" 7 | -------------------------------------------------------------------------------- /.github/workflows/on-push.yml: -------------------------------------------------------------------------------- 1 | name: Push Workflow 2 | on: push 3 | 4 | jobs: 5 | code-quality: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-node@v3 10 | with: 11 | registry-url: 'https://npm.pkg.github.com' 12 | node-version: 16.0.0 13 | cache: 'yarn' 14 | - name: Install 15 | run: yarn install 16 | env: 17 | NODE_AUTH_TOKEN: ${{ secrets.PACKAGE_PAT }} 18 | - name: Lint 19 | run: yarn lint 20 | - name: Format 21 | run: yarn format:fix 22 | 23 | build: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-node@v3 28 | with: 29 | registry-url: 'https://npm.pkg.github.com' 30 | node-version: 16.0.0 31 | cache: 'yarn' 32 | - name: Install 33 | run: yarn install 34 | 35 | gateway-tests: 36 | runs-on: ubuntu-latest 37 | needs: build 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v3 41 | with: 42 | registry-url: 'https://npm.pkg.github.com' 43 | node-version: 16.0.0 44 | cache: 'yarn' 45 | - run: yarn install 46 | - run: yarn test:gateway 47 | contract-tests: 48 | runs-on: ubuntu-latest 49 | needs: build 50 | steps: 51 | - uses: actions/checkout@v2 52 | - uses: actions/setup-node@v3 53 | with: 54 | registry-url: 'https://npm.pkg.github.com' 55 | node-version: 16.0.0 56 | cache: 'yarn' 57 | - run: yarn install 58 | - run: yarn test:contracts 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | .VSCodeCounter/** 4 | # hardhat 5 | artifacts 6 | cache 7 | deployments 8 | node_modules 9 | 10 | 11 | cache/ 12 | artifacts/ 13 | 14 | coverage* 15 | typechain/ 16 | 17 | .vscode/* 18 | !.vscode/settings.json.default 19 | !.vscode/launch.json.default 20 | !.vscode/extensions.json.default 21 | 22 | node_modules/ 23 | .env 24 | 25 | .yalc 26 | yalc.lock 27 | 28 | contractsInfo.json 29 | deployments/hardhat 30 | deployments/localhost 31 | 32 | # don't push the environment vars! 33 | .env 34 | 35 | # Built application files 36 | .DS* 37 | *.apk 38 | *.ap_ 39 | *.aab 40 | 41 | # Files for the ART/Dalvik VM 42 | *.dex 43 | 44 | # Java class files 45 | *.class 46 | 47 | # Generated files 48 | bin/ 49 | gen/ 50 | out/ 51 | # Uncomment the following line in case you need and you don't have the release build type files in your app 52 | # release/ 53 | 54 | # Gradle files 55 | .gradle/ 56 | build/ 57 | 58 | # Local configuration file (sdk path, etc) 59 | local.properties 60 | 61 | # Proguard folder generated by Eclipse 62 | proguard/ 63 | 64 | # Log Files 65 | *.log 66 | 67 | # Android Studio Navigation editor temp files 68 | .navigation/ 69 | 70 | # Android Studio captures folder 71 | captures/ 72 | 73 | # IntelliJ 74 | *.iml 75 | .idea/workspace.xml 76 | .idea/tasks.xml 77 | .idea/gradle.xml 78 | .idea/assetWizardSettings.xml 79 | .idea/dictionaries 80 | .idea/libraries 81 | # Android Studio 3 in .gitignore file. 82 | .idea/caches 83 | .idea/modules.xml 84 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 85 | .idea/navEditor.xml 86 | .idea 87 | 88 | # Keystore files 89 | # Uncomment the following lines if you do not want to check your keystore files in. 90 | #*.jks 91 | #*.keystore 92 | 93 | # External native build folder generated in Android Studio 2.2 and later 94 | .externalNativeBuild 95 | 96 | # Google Services (e.g. APIs or Firebase) 97 | # google-services.json 98 | 99 | # Freeline 100 | freeline.py 101 | freeline/ 102 | freeline_project_description.json 103 | 104 | # fastlane 105 | fastlane/report.xml 106 | fastlane/Preview.html 107 | fastlane/screenshots 108 | fastlane/test_output 109 | fastlane/readme.md 110 | 111 | # Version control 112 | vcs.xml 113 | 114 | # lint 115 | lint/intermediates/ 116 | lint/generated/ 117 | lint/outputs/ 118 | lint/tmp/ 119 | # lint/reports/ 120 | 121 | node_modules 122 | node_modules 123 | dist 124 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | export/ 2 | deployments/ 3 | artifacts/ 4 | cache/ 5 | coverage/ 6 | node_modules/ 7 | package.json 8 | typechain/ 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "arrowParens": "avoid", 6 | "overrides": [ 7 | { 8 | "files": "*.sol", 9 | "options": { 10 | "singleQuote": false 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": [ 6 | "error", 7 | { 8 | "endOfLine": "auto" 9 | } 10 | ], 11 | "code-complexity": ["error", 7], 12 | "compiler-version": ["error", "^0.8.2"], 13 | "const-name-snakecase": "off", 14 | "func-name-mixedcase": "off", 15 | "constructor-syntax": "error", 16 | "func-visibility": ["error", { "ignoreConstructors": true }], 17 | "not-rely-on-time": "off", 18 | "reason-string": ["warn", { "maxLength": 64 }] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | export/ 2 | deployments/ 3 | artifacts/ 4 | cache/ 5 | coverage/ 6 | node_modules/ 7 | typechain/ 8 | scripts/ 9 | tasks/ 10 | test/ 11 | .vscode/ 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 node:18-alpine 2 | WORKDIR /app 3 | COPY . . 4 | RUN apk add --no-cache git openssh g++ make py3-pip 5 | RUN yarn install 6 | RUN yarn build 7 | CMD yarn start 8 | EXPOSE 8081 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022,2023, corpus.ventures GmbH, dm3 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Generic ERC3668 Resolver 2 | 3 | Storing data on Ethereum Mainnet is expensive hence it is appealing to use other storage solutions that offer more compelling rates. To facilitate this https://eips.ethereum.org/EIPS/eip-3668 introduces CCIP a standard that can be used to securely retrieve external data. 4 | 5 | This repository contains contracts implementing the ERC-3668 standard to resolve data from an arbitrary data source. This might be based on another EVM Chain like Optimism, a Database like Postgress or any other way you might want to store data for your app. 6 | Visit the App-specific Handler section to learn how to write a handler for your app. 7 | 8 | # Architecture 9 | 10 | ## Smart Contracts 11 | 12 | ### ERC3668Resolver 13 | 14 | The core contract implementing the Ccip Interface. It delegates the actual implementation to an instance of the CcipVerifier contract. Every Ens name owner can specify its verifier. This allows the Ens-name owner to declare different Data sources associated with their domain. 15 | 16 | ### CcipResponseVerifier 17 | 18 | An App Specific Handler needs a CcipResponseVerifier contract that implements the `resolveWithProof` function accordingly. For example, the handler related to the optimism-Bedrock validates the Merkle proof for the returned data and ensures it is part of the Optimism Network. 19 | 20 | ### BedrockCcipVerifier 21 | 22 | An implementation of the CcipResponseVerifier Interface. This contract is supposed to prove data from a certain contract on Optimism Bedrock. 23 | 24 | ## Gateway 25 | 26 | ![Resolver architecture](./architcture.jpeg) 27 | 28 | The Gateway is a Node.js app resolving incoming requests made by the CCIP Reader. It queries the Type Specific handler defined in the Config to retrieve the desired data. You can host your gateway by simply using our docker file and providing your config file. 29 | 30 | ### Type Specific Handler 31 | 32 | When CCIP is used the recipient that made a request has to trust the gateway that the returned data is correct. This is because the recipient doesn't have access to the underlying data source to look this up by themself. To handle this the CCIP flow contains the `resolveWithProof` function that can be used to verify the integrity of the data returned. 33 | A Type Specific handler fetches the data from the data source and prepares the proof so it can be verified using `resolveWithProof`. At the moment we're supporting two different Handlers _Signature_ and _Optimism-Bedrock_ 34 | 35 | **Signature** 36 | 37 | When the Signature Handler is used the Gateway computes a signature of the response data using a predefined private Key. When retrieving the data the recipient can use ECRecover to ensure the given signature was computed by the address defined in the CcipVerifer contract. 38 | When this handler is used the Gateway is trusted. If the Gateway is corrupted the returned data might be corrupted as well. 39 | 40 | **Optimism-Bedrock** 41 | 42 | The Optimim Bedrock handler leverages the Optimism Bedrock protocol and enables a trustless way to store data on L2. The Optimism Node posts a hash to Ethereum Mainnet that contains the Stateroot of the entire Optimism Network. That Stateroot can be used to prove that certain data existed in account storage using Merkle proofs. 43 | 44 | ### App Specific Handler 45 | 46 | App Specific handlers implement the actual data source and provide a REST interface so the Gateway can query it. An example using an L2 ENS Public Resolver contract as a data source can be found here: https://github.com/corpus-io/ENS-Bedrock-Resolver 47 | 48 | ## Resources 49 | 50 | - **Ethereum Improvement Proposals ERC-3668:** CCIP Read: Secure offchain data retrieval CCIP Read provides a mechanism to allow a contract to fetch external data. 51 | - **ENSIP-10: Wildcard Resolution:** 52 | Provides a mechanism to support wildcard resolution of ENS names (formerly EIP-2544). (36 kB) 53 | https://docs.ens.domains/ens-improvement-proposals/ensip-10-wildcard-resolution 54 | - **ENSIP-11: Wildcard Resolution:** 55 | Introduces coinType for EVM compatible chains (amending ENSIP9). 56 | https://docs.ens.domains/ens-improvement-proposals/ensip-11-evmchain-address-resolution 57 | 58 | ## Installation 59 | 60 | 1. Clone the repository `git clone https://github.com/corpus-io/Optimism-Resolver` 61 | 2. Install dependencies using `yarn install` 62 | 63 | ## Tests 64 | 65 | This repository contains 3 different test suites. There are tests for the Smart contracts the Gateway server and the bedrock-related parts. 66 | All tests associated with Bedrock require access to the Bedrock local development environment and the execution of setupEnvironment as described in the Bedrock test section. This might not be always feasible for example when a test should be executed in a CI Pipeline. 67 | Thus the test suites can be executed separately 68 | 69 | ### Gateway tests 70 | 71 | Run `yarn run test:gateway` 72 | 73 | ### Contract test 74 | 75 | Run `yarn run test:contracts` 76 | 77 | ### Bedrock tests 78 | 79 | The tests are based on the Optimism local development environment. To run them you've to run this environment on your machine. 80 | Visit https://community.optimism.io/docs/developers/build/dev-node/ for setup instructions. 81 | 82 | 1. After you have set up the optimism development environment run `make devnet-up` to start it. 83 | 2. Wait at least 5 minutes until everything is set up. This is mandatory because the local development environment contains different containers that are started independently from each other. 84 | 3. Run `yarn run e2e:setup` to set up the environment required by the tests. This deploys the contracts and creates the initial data we're later going to prove. 85 | 4. Wait again for a few minutes. The rollup needs to commit the changes made. If you see the error "Account is not part of the provided state root" or "Provided proof is invalid" The commit is still pending 86 | 5. Run the test using `yarn test` 87 | 88 | ## Implementation 89 | The CCIP-Resolver has already been used by the following projects 90 | 91 | ### ENS 92 | 93 | ENS uses the CCIP-Resolver to store records on Optimism 94 | 95 | https://github.com/corpus-io/ENS-Bedrock-Resolver 96 | 97 | ## Deployments 98 | 99 | ### Bedrock ProofVerifier 100 | 101 | Deploy the BedrockProofVerifier Contract using `yarn run deploy:bedrock-proof-verifier-goerli` 102 | 103 | ### ERC3668 Resolver 104 | 105 | Deploy the ERC3668 Contract using `yarn run deploy:ccip-resolver-goerli` 106 | 107 | ### Bedrock CCIP Verifier 108 | 109 | The BedrockCCIPVerifier just supports one contract on Optimism. If you want to use a different contract you have to deploy a new instance of that contract. 110 | 111 | ### Goerli 112 | 113 | ERC3668 Resolver: 0xaeB973dA621Ed58F0D8bfD6299031E8a2Ac39FD4 114 | 115 | #### Optimism Verifier 116 | 117 | BedrockProofVerifier : 0x2231dA800580E27cAA4C45C43b2B6c1D1487eC6F 118 | -------------------------------------------------------------------------------- /abi/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProofServiceTestContract__factory, 3 | BedrockCcipVerifier__factory, 4 | BedrockProofVerifier__factory, 5 | IBedrockProofVerifier__factory, 6 | SignatureCcipVerifier__factory, CcipResponseVerifier__factory, 7 | ICcipResponseVerifier__factory, 8 | ERC3668Resolver__factory, 9 | IExtendedResolver__factory, 10 | IMetadataResolver__factory, 11 | SupportsInterface__factory 12 | } from "../typechain" 13 | 14 | export default { 15 | ProofServiceTestContract: ProofServiceTestContract__factory.abi, 16 | BedrockCcipVerifier: BedrockCcipVerifier__factory.abi, 17 | BedrockProofVerifier: BedrockProofVerifier__factory.abi, 18 | IBedrockProofVerifier: IBedrockProofVerifier__factory.abi, 19 | SignatureCcipVerifier: SignatureCcipVerifier__factory.abi, 20 | CcipResponseVerifier: CcipResponseVerifier__factory.abi, 21 | ICcipResponseVerifier: ICcipResponseVerifier__factory.abi, 22 | ERC3668Resolver: ERC3668Resolver__factory.abi, 23 | IExtendedResolver: IExtendedResolver__factory.abi, 24 | IMetadataResolver: IMetadataResolver__factory.abi, 25 | SupportsInterface: SupportsInterface__factory.abi 26 | } 27 | -------------------------------------------------------------------------------- /architcture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dm3-org/Ccip-Resolver/ab80756f20141b6d5654bfc46b0e5352523a3721/architcture.jpeg -------------------------------------------------------------------------------- /contracts/ERC3668Resolver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {IExtendedResolver, IResolverService} from "./IExtendedResolver.sol"; 5 | import {IMetadataResolver} from "./IMetadataResolver.sol"; 6 | import {SupportsInterface} from "./SupportsInterface.sol"; 7 | import {CcipResponseVerifier, ICcipResponseVerifier} from "./verifier/CcipResponseVerifier.sol"; 8 | import {ENSRegistry} from "@ensdomains/ens-contracts/contracts/registry/ENSRegistry.sol"; 9 | import {INameWrapper} from "@ensdomains/ens-contracts/contracts/wrapper/INameWrapper.sol"; 10 | import {BytesUtils} from "@ensdomains/ens-contracts/contracts/wrapper/BytesUtils.sol"; 11 | 12 | import {BytesLib} from "solidity-bytes-utils/contracts/BytesLib.sol"; 13 | 14 | /* 15 | * Implements an ENS resolver that directs all queries to a CCIP read gateway. 16 | * Callers must implement EIP 3668 and ENSIP 10. 17 | */ 18 | 19 | contract ERC3668Resolver is IExtendedResolver, IMetadataResolver, SupportsInterface { 20 | using BytesUtils for bytes; 21 | 22 | struct CcipVerifier { 23 | string[] gatewayUrls; 24 | ICcipResponseVerifier verifierAddress; 25 | } 26 | /** 27 | * The idnetifier to store the default verifier 28 | */ 29 | bytes32 private constant DEFAULT_VERIFIER = bytes32(0); 30 | 31 | /* 32 | * -------------------------------------------------- 33 | * EVENTS 34 | * -------------------------------------------------- 35 | */ 36 | 37 | event VerifierAdded(bytes32 indexed node, address verifierAddress, string[] gatewayUrls); 38 | /* 39 | * -------------------------------------------------- 40 | * Errors 41 | * -------------------------------------------------- 42 | */ 43 | 44 | error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData); 45 | 46 | /* 47 | * -------------------------------------------------- 48 | * State Variables 49 | * -------------------------------------------------- 50 | */ 51 | 52 | ENSRegistry public immutable ensRegistry; 53 | INameWrapper public immutable nameWrapper; 54 | 55 | mapping(bytes32 => CcipVerifier) public ccipVerifier; 56 | 57 | /* 58 | * -------------------------------------------------- 59 | * Constructor 60 | * -------------------------------------------------- 61 | */ 62 | 63 | constructor( 64 | // The ENS registry 65 | ENSRegistry _ensRegistry, 66 | // The name wrapper 67 | INameWrapper _nameWrapper, 68 | //Address of the default CCIP Verifier 69 | address _defaultVerifier, 70 | string[] memory _gatewayUrls 71 | ) { 72 | ensRegistry = _ensRegistry; 73 | nameWrapper = _nameWrapper; 74 | 75 | /** 76 | * If a default verifier is set, that verifier will be used by every child address that doesn't have a specific verifier set. 77 | * 78 | */ 79 | if (_defaultVerifier != address(0)) { 80 | _setVerifierForDomain(DEFAULT_VERIFIER, _defaultVerifier, _gatewayUrls); 81 | } 82 | } 83 | 84 | /* 85 | * -------------------------------------------------- 86 | * External functions 87 | * -------------------------------------------------- 88 | */ 89 | 90 | /** 91 | * @notice Sets a Cross-chain Information Protocol (CCIP) Verifier for a specific domain node. 92 | * @param node The domain node for which the CCIP Verifier is set. 93 | * @param verifierAddress The address of the CcipResponseVerifier contract. 94 | * @param urls The gateway url that should handle the OffchainLookup. 95 | */ 96 | function setVerifierForDomain(bytes32 node, address verifierAddress, string[] memory urls) external { 97 | /* 98 | * Only the node owner can set the verifier for a node. NameWrapper profiles are supported too. 99 | */ 100 | require(node != bytes32(0), "node is 0x0"); 101 | require(msg.sender == getNodeOwner(node), "only node owner"); 102 | _setVerifierForDomain(node, verifierAddress, urls); 103 | } 104 | 105 | /** 106 | * Resolves arbitrary data for a particular name, as specified by ENSIP 10. 107 | * @param name The DNS-encoded name to resolve. 108 | * @param data The ABI encoded data for the underlying resolution function (Eg, addr(bytes32), text(bytes32,string), etc). 109 | * @return The return data, ABI encoded identically to the underlying function. 110 | */ 111 | function resolve(bytes calldata name, bytes calldata data) external view override returns (bytes memory) { 112 | /* 113 | * Get the verifier for the given name. 114 | * reverts if no verifier was set in advance 115 | */ 116 | (CcipVerifier memory _verifier, bytes32 node) = getVerifierOfDomain(name); 117 | /* 118 | * Retrieves the owner of the node. NameWrapper profiles are supported too. This will be the context of the request. 119 | */ 120 | address nodeOwner = getNameOwner(name, 0); 121 | bytes memory context = abi.encodePacked(nodeOwner); 122 | 123 | /* 124 | * The calldata the gateway has to resolve 125 | */ 126 | bytes memory callData = abi.encodeWithSelector( 127 | IResolverService.resolveWithContext.selector, 128 | name, 129 | data, 130 | context 131 | ); 132 | revert OffchainLookup( 133 | address(this), 134 | _verifier.gatewayUrls, 135 | callData, 136 | ERC3668Resolver.resolveWithProof.selector, 137 | callData 138 | ); 139 | } 140 | 141 | /** 142 | * @dev Function to resolve a domain name with proof using an off-chain callback mechanism. 143 | * @param response The response received from off-chain resolution. 144 | * @param extraData The actual calldata that was called on the gateway. 145 | * @return the result of the offchain lookup 146 | */ 147 | function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory) { 148 | /* 149 | * decode the calldata that was encoded in the resolve function for IResolverService.resolveWithContext() 150 | * bytes memory callData = abi.encodeWithSelector(IResolverService.resolveWithContext.selector, name, data context); 151 | */ 152 | (bytes memory name, bytes memory data) = abi.decode(extraData[4:], (bytes, bytes)); 153 | /* 154 | * Get the verifier for the given name. 155 | * reverts if no verifier was set in advance 156 | */ 157 | (CcipVerifier memory _ccipVerifier, ) = getVerifierOfDomain(name); 158 | /* 159 | * to enable the ERC3668Resolver to return data other than bytes it might be possible to override the 160 | * resolvewithProofCallback function. 161 | */ 162 | bytes4 callBackSelector = ICcipResponseVerifier(_ccipVerifier.verifierAddress).onResolveWithProof(name, data); 163 | /* 164 | * reverts when no callback selector was found. This should normally never happen because setVerifier() checks * that the verifierAddress implements the ICcipResponseVerifier interface. However, it might be possible by 165 | * overriding the onResolveWithProof function and return 0x. In that case, the contract reverts here. 166 | */ 167 | require(callBackSelector != bytes4(0), "No callback selector found"); 168 | 169 | /* 170 | * staticcall to the callback function on the verifier contract. 171 | * This function always returns bytes even the called function returns a Fixed type due to the return type of staticcall in solidity. 172 | * So you might want to decode the result using abi.decode(resolveWithProofResponse, (bytes)) 173 | */ 174 | (bool success, bytes memory resolveWithProofResponse) = address(_ccipVerifier.verifierAddress).staticcall( 175 | abi.encodeWithSelector(callBackSelector, response, extraData) 176 | ); 177 | /* 178 | * Reverts if the call is not successful 179 | */ 180 | require(success, "staticcall to verifier failed"); 181 | return resolveWithProofResponse; 182 | } 183 | 184 | /** 185 | * @notice Get metadata about the CCIP Resolver 186 | * @dev This function provides metadata about the CCIP Resolver, including its name, coin type, GraphQL URL, storage type, and encoded information. 187 | * @param name The domain name in format (dnsEncoded) 188 | * @return name The name of the resolver ("CCIP RESOLVER") 189 | * @return coinType Resolvers coin type (60 for Ethereum) 190 | * @return graphqlUrl The GraphQL URL used by the resolver 191 | * @return storageType Storage Type (0 for EVM) 192 | * @return storageLocation The storage identifier. For EVM chains, this is the address of the resolver contract. 193 | * @return context can be l2 resolver contract address for evm chain but can be any l2 storage identifier for non-evm chain 194 | */ 195 | function metadata( 196 | bytes calldata name 197 | ) external view returns (string memory, uint256, string memory, uint8, bytes memory, bytes memory) { 198 | /* 199 | * Get the verifier for the given name. 200 | * reverts if no verifier was set in advance 201 | */ 202 | (CcipVerifier memory _ccipVerifier, ) = getVerifierOfDomain(name); 203 | 204 | /* 205 | * Get the metadata from the verifier contract 206 | */ 207 | ( 208 | string memory resolverName, 209 | uint256 coinType, 210 | string memory graphqlUrl, 211 | uint8 storageType, 212 | bytes memory storageLocation, 213 | 214 | ) = ICcipResponseVerifier(_ccipVerifier.verifierAddress).metadata(name); 215 | 216 | /* 217 | * To determine the context of the request, we need to get the owner of the node. 218 | */ 219 | address nodeOwner = getNameOwner(name, 0); 220 | bytes memory context = abi.encodePacked(nodeOwner); 221 | 222 | return (resolverName, coinType, graphqlUrl, storageType, storageLocation, context); 223 | } 224 | 225 | /* 226 | * -------------------------------------------------- 227 | * Public Functions 228 | * -------------------------------------------------- 229 | */ 230 | 231 | /** 232 | * @notice Get the CCIP Verifier and node for a given domain name 233 | * @dev This function allows retrieving the CCIP Verifier and its associated node for a given domain name. For subdomains, it will return the CCIP Verifier of the closest parent. 234 | * @param name The domain name in bytes (dnsEncoded) 235 | * @return _ccipVerifier The CCIP Verifier associated with the given domain name 236 | * @return node The node associated with the given domain name 237 | */ 238 | function getVerifierOfDomain(bytes memory name) public view returns (CcipVerifier memory, bytes32) { 239 | return getVerifierOfSegment(name, 0, name.namehash(0)); 240 | } 241 | 242 | /** 243 | * @notice Check if the contract supports a specific interface 244 | * @dev Implements the ERC-165 standard to check for interface support. 245 | * @param interfaceID The interface identifier to check 246 | * @return True if the contract supports the given interface, otherwise false 247 | */ 248 | function supportsInterface(bytes4 interfaceID) public pure override returns (bool) { 249 | return interfaceID == type(IExtendedResolver).interfaceId || super.supportsInterface(interfaceID); 250 | } 251 | 252 | /* 253 | * -------------------------------------------------- 254 | * Internal Functions 255 | * -------------------------------------------------- 256 | */ 257 | 258 | /** 259 | * @notice Get the owner of the ENS node either from the ENS registry or the NameWrapper contract 260 | * @dev This function adds support for ENS nodes owned by the NameWrapper contract. 261 | * @param node The ENS node to query for the owner 262 | * @return nodeOwner The address of the owner of the ENS node 263 | */ 264 | function getNodeOwner(bytes32 node) internal view returns (address nodeOwner) { 265 | nodeOwner = ensRegistry.owner(node); 266 | if (nodeOwner == address(nameWrapper)) { 267 | nodeOwner = nameWrapper.ownerOf(uint256(node)); 268 | } 269 | } 270 | 271 | /** 272 | * @notice Get the owner of the ENS name either from the ENS registry or the NameWrapper contract 273 | * @dev This function adds support for ENS nodes owned by the NameWrapper contract. 274 | * @param name The domain name in bytes (dnsEncoded) 275 | * @param offset The current offset in the name being processed 276 | * @return nodeOwner The address of the owner of the ENS node 277 | */ 278 | function getNameOwner(bytes memory name, uint256 offset) internal view returns (address nodeOwner) { 279 | bytes32 node = name.namehash(offset); 280 | (, offset) = name.readLabel(offset); 281 | nodeOwner = ensRegistry.owner(node); 282 | if(offset >= name.length) { 283 | return address(0); 284 | }else if (nodeOwner == address(0)){ 285 | return getNameOwner(name, offset); 286 | }else if(nodeOwner == address(nameWrapper)){ 287 | return nameWrapper.ownerOf(uint256(node)); 288 | } 289 | return nodeOwner; 290 | } 291 | 292 | /* 293 | * -------------------------------------------------- 294 | * Private Functions 295 | * -------------------------------------------------- 296 | * 297 | */ 298 | 299 | /** 300 | * @notice Sets a Cross-chain Information Protocol (CCIP) Verifier for a specific domain node. 301 | * @param node The domain node for which the CCIP Verifier is set. 302 | * @param verifierAddress The address of the CcipResponseVerifier contract. 303 | * @param urls The gateway url that should handle the OffchainLookup. 304 | */ 305 | function _setVerifierForDomain(bytes32 node, address verifierAddress, string[] memory urls) private { 306 | require(verifierAddress != address(0), "verifierAddress is 0x0"); 307 | /* 308 | * We're doing a staticcall here to check if the verifierAddress implements the ICcipResponseVerifier interface. 309 | * This is done to prevent the user from setting an arbitrary address as the verifierAddress. 310 | */ 311 | (bool success, bytes memory response) = verifierAddress.staticcall( 312 | abi.encodeWithSignature("supportsInterface(bytes4)", type(ICcipResponseVerifier).interfaceId) 313 | ); 314 | 315 | /* 316 | * A successful static call will return 0x0000000000000000000000000000000000000000000000000000000000000001 317 | * Hence we've to check that the last bit is set. 318 | */ 319 | require( 320 | success && response.length == 32 && (response[response.length - 1] & 0x01) == 0x01, 321 | "verifierAddress is not a CCIP Verifier" 322 | ); 323 | /* 324 | * Check that the url is non-null. 325 | * Although it may not be a sufficient url check, it prevents users from passing undefined or empty strings. 326 | */ 327 | require(urls.length > 0, "at least one gateway url has to be provided"); 328 | 329 | /* 330 | * Set the new verifier for the given node. 331 | */ 332 | CcipVerifier memory _ccipVerifier = CcipVerifier(urls, ICcipResponseVerifier(verifierAddress)); 333 | ccipVerifier[node] = _ccipVerifier; 334 | 335 | emit VerifierAdded(node, verifierAddress, urls); 336 | } 337 | 338 | /** 339 | * @dev Recursively searches for a verifier associated with a segment of the given domain name. 340 | * If a verifier is found, it returns the verifier and the corresponding node. 341 | * 342 | * @param name The domain name in bytes 343 | * @param offset The current offset in the name being processed 344 | * @param node The current node being processed 345 | * @return The CcipVerifier associated with the domain segment, and the corresponding node 346 | * 347 | * @notice This function searches for a verifier starting from the given offset in the domain name. 348 | * It checks if a verifier is set for the current node, and if not, it continues with the next label. 349 | * If the end of the name is reached and no verifier is found, it reverts with an UnknownVerifier error. 350 | */ 351 | function getVerifierOfSegment( 352 | bytes memory name, 353 | uint256 offset, 354 | bytes32 node 355 | ) private view returns (CcipVerifier memory, bytes32) { 356 | /* 357 | * If we reached the root node and there is no verifier set, we revert with UnknownVerifier 358 | */ 359 | if (offset >= name.length - 1) { 360 | /* 361 | *If no specific verifier is set for the given node, we return the default verifier 362 | */ 363 | CcipVerifier memory defaultCcipVerifier = ccipVerifier[DEFAULT_VERIFIER]; 364 | return (defaultCcipVerifier, name.namehash(0)); 365 | } 366 | 367 | CcipVerifier memory _ccipVerifier = ccipVerifier[node]; 368 | /* 369 | * If the verifier is set for the given node, we return it and break the recursion 370 | */ 371 | if (address(_ccipVerifier.verifierAddress) != address(0)) { 372 | return (_ccipVerifier, name.namehash(0)); 373 | } 374 | /* 375 | * Otherwise, continue with the next label 376 | */ 377 | (, offset) = name.readLabel(offset); 378 | return getVerifierOfSegment(name, offset, name.namehash(offset)); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /contracts/IExtendedResolver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | interface IExtendedResolver { 5 | function resolve(bytes memory name, bytes memory data) external view returns (bytes memory); 6 | } 7 | 8 | interface IResolverService { 9 | function resolveWithContext( 10 | bytes calldata name, 11 | bytes calldata data, 12 | bytes calldata context 13 | ) external view returns (bytes memory result); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/IMetadataResolver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | interface IMetadataResolver { 5 | /** 6 | * @notice Get metadata about the CCIP Resolver 7 | * @dev This function provides metadata about the CCIP Resolver, including its name, coin type, GraphQL URL, storage type, and encoded information. 8 | * @param name The domain name in format (dnsEncoded) 9 | * @return name The name of the resolver ("CCIP RESOLVER") 10 | * @return coinType Resolvers coin type (60 for Ethereum) 11 | * @return graphqlUrl The GraphQL URL used by the resolver 12 | * @return storageType Storage Type (0 for EVM) 13 | * @return storageLocation The storage identifier. For EVM chains, this is the address of the resolver contract. 14 | * @return context can be l2 resolver contract address for evm chain but can be any l2 storage identifier for non evm chain 15 | * 16 | */ 17 | function metadata(bytes calldata name) external view returns (string memory, uint256, string memory, uint8, bytes memory, bytes memory); 18 | } 19 | -------------------------------------------------------------------------------- /contracts/SupportsInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | interface ISupportsInterface { 5 | function supportsInterface(bytes4 interfaceID) external pure returns (bool); 6 | } 7 | 8 | abstract contract SupportsInterface is ISupportsInterface { 9 | function supportsInterface(bytes4 interfaceID) public pure virtual override returns (bool) { 10 | return interfaceID == type(ISupportsInterface).interfaceId; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/coinType/Ensip11CoinType.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | function convertEVMChainIdToCoinType(uint256 chainId) pure returns (uint256) { 5 | return (0x80000000 | chainId) >> 0; 6 | } 7 | 8 | function convertCoinTypeToEVMChainId(uint256 coinType) pure returns (uint256) { 9 | return (0x7fffffff & coinType) >> 0; 10 | } 11 | -------------------------------------------------------------------------------- /contracts/test-contract/ProofServiceTestContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | contract ProofServiceTestContract { 5 | bool public testBool; 6 | bytes32 public testBytes32; 7 | address public testAddress; 8 | uint256 public testUnit; 9 | 10 | bytes public testBytes; 11 | string public testString; 12 | string public longString; 13 | 14 | function setBool(bool _testBool) external { 15 | testBool = _testBool; 16 | } 17 | 18 | function setAddress(address _testAddress) external { 19 | testAddress = _testAddress; 20 | } 21 | 22 | function setUint256(uint256 _testUint) external { 23 | testUnit = _testUint; 24 | } 25 | 26 | function setBytes32(bytes32 _testBytes32) external { 27 | testBytes32 = _testBytes32; 28 | } 29 | 30 | function setBytes(bytes calldata _testBytes) external { 31 | testBytes = _testBytes; 32 | } 33 | 34 | function setString(string calldata _testString) external { 35 | testString = _testString; 36 | } 37 | 38 | function setLongString(string calldata _longString) external { 39 | longString = _longString; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/verifier/CcipResponseVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {ICcipResponseVerifier} from "./ICcipResponseVerifier.sol"; 5 | import {SupportsInterface, ISupportsInterface} from "../SupportsInterface.sol"; 6 | 7 | abstract contract CcipResponseVerifier is ICcipResponseVerifier, SupportsInterface { 8 | /* 9 | * -------------------------------------------------- 10 | * EVENTS 11 | * -------------------------------------------------- 12 | */ 13 | 14 | event GraphQlUrlChanged(string newGraphQlUrl); 15 | event OwnerChanged(address newOwner); 16 | 17 | /* 18 | * -------------------------------------------------- 19 | * State Variables 20 | * -------------------------------------------------- 21 | */ 22 | 23 | /** 24 | * @notice The owner of the contract 25 | * The owner of the contract can set the graphQlUrl and determine a new owner 26 | */ 27 | address public owner; 28 | 29 | /** 30 | * @notice The graphql endpoint url 31 | * The graphQlUrl is used to provide metadata of the resolver 32 | */ 33 | string public graphqlUrl; 34 | 35 | /** 36 | * @notice The resolver name 37 | * The name of the resolver 38 | */ 39 | string public resolverName; 40 | /** 41 | * @notice the chainId at which the resolver resolves data from 42 | * @dev should be 0 if storageLocation is offChain 43 | */ 44 | uint256 public l2ResolverChainID; 45 | 46 | /* 47 | * -------------------------------------------------- 48 | * Constructor 49 | * -------------------------------------------------- 50 | */ 51 | 52 | constructor(address _owner, string memory _graphqlUrl, string memory _resolverName, uint256 _l2ResolverChainID) { 53 | owner = _owner; 54 | graphqlUrl = _graphqlUrl; 55 | resolverName = _resolverName; 56 | l2ResolverChainID = _l2ResolverChainID; 57 | } 58 | 59 | /* 60 | * -------------------------------------------------- 61 | * Modifier 62 | * -------------------------------------------------- 63 | */ 64 | 65 | modifier onlyOwner() { 66 | require(msg.sender == owner, "only owner"); 67 | _; 68 | } 69 | 70 | /** 71 | * @notice Set the GraphQL endpoint URL for the contract 72 | * @dev This function can only be called by the current owner. 73 | * @param _graphqlUrl The new GraphQL endpoint URL to be set 74 | */ 75 | function setGraphUrl(string memory _graphqlUrl) external onlyOwner { 76 | graphqlUrl = _graphqlUrl; 77 | emit GraphQlUrlChanged(_graphqlUrl); 78 | } 79 | 80 | /** 81 | * @notice Set the new owner of the contract 82 | * @dev This function can only be called by the current owner. 83 | * @param _owner The address of the new owner 84 | */ 85 | function setOwner(address _owner) external onlyOwner { 86 | owner = _owner; 87 | emit OwnerChanged(_owner); 88 | } 89 | 90 | /* 91 | * -------------------------------------------------- 92 | * External functions 93 | * -------------------------------------------------- 94 | */ 95 | 96 | /** 97 | * @notice Check if the contract supports the given interface 98 | * @dev This function checks if the contract supports the provided interface by comparing the `interfaceID` with the supported interface IDs. 99 | * @param interfaceID The interface ID to check for support 100 | * @return true if the contract supports the interface, false otherwise 101 | */ 102 | function supportsInterface( 103 | bytes4 interfaceID 104 | ) public pure override(SupportsInterface, ISupportsInterface) returns (bool) { 105 | /* 106 | * Supports both ICcipResponseVerifier and ISupportsInterfacef 107 | */ 108 | return interfaceID == type(ICcipResponseVerifier).interfaceId || super.supportsInterface(interfaceID); 109 | } 110 | 111 | /** 112 | * @notice To support other to be resolved than just bytes it is possible to override this function. In that case the function selector of the overridden function should be returned. The default implementation returns the function selector of the `resolveWithProof` function. 113 | * @return The function selector of the `resolveWithProof` function 114 | */ 115 | function onResolveWithProof(bytes calldata, bytes calldata) public pure virtual override returns (bytes4) { 116 | return this.resolveWithProof.selector; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /contracts/verifier/ICcipResponseVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | import {ISupportsInterface} from "../SupportsInterface.sol"; 4 | 5 | interface ICcipResponseVerifier is ISupportsInterface { 6 | function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory); 7 | 8 | function onResolveWithProof(bytes calldata name, bytes calldata data) external pure returns (bytes4); 9 | 10 | function metadata( 11 | bytes calldata name 12 | ) external view returns (string memory, uint256, string memory, uint8, bytes memory, bytes memory); 13 | } 14 | -------------------------------------------------------------------------------- /contracts/verifier/optimism-bedrock/BedrockCcipVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {CcipResponseVerifier} from "../CcipResponseVerifier.sol"; 5 | import {IBedrockProofVerifier} from "./IBedrockProofVerifier.sol"; 6 | import {convertEVMChainIdToCoinType} from "../../coinType/Ensip11CoinType.sol"; 7 | 8 | contract BedrockCcipVerifier is CcipResponseVerifier { 9 | IBedrockProofVerifier public immutable bedrockProofVerifier; 10 | address public immutable target; 11 | 12 | constructor( 13 | address _owner, 14 | string memory _graphQlUrl, 15 | string memory _resolverName, 16 | uint256 _l2ResolverChainID, 17 | IBedrockProofVerifier _bedrockProofVerifier, 18 | address _target 19 | ) CcipResponseVerifier(_owner, _graphQlUrl, _resolverName, _l2ResolverChainID) { 20 | bedrockProofVerifier = _bedrockProofVerifier; 21 | target = _target; 22 | } 23 | 24 | /** 25 | * @notice Resolve a response with a proof 26 | * @dev This function allows resolving a response along with a proof provided by IBedrockProofVerifier. 27 | * @param response The response data along with the associated proof 28 | * @param extraData The original data passed to the request 29 | * @return The resolved response data encoded as bytes 30 | */ 31 | function resolveWithProof( 32 | bytes calldata response, 33 | bytes calldata extraData 34 | ) public view virtual override returns (bytes memory) { 35 | /* 36 | * The response is expected to be an array of bytes containing more than one proof to extend the functionality * of the resolver. 37 | */ 38 | bytes[] memory responses = abi.decode(response, (bytes[])); 39 | bytes[] memory proofs = new bytes[](responses.length); 40 | 41 | for (uint256 i = 0; i < responses.length; i++) { 42 | /* 43 | * @dev Decode the response and proof from the response bytes 44 | */ 45 | (bytes memory responseEncoded, IBedrockProofVerifier.BedrockStateProof memory proof) = abi.decode( 46 | responses[i], 47 | (bytes, IBedrockProofVerifier.BedrockStateProof) 48 | ); 49 | /* 50 | * Revert if the proof target does not match the resolver. This is to prevent a malicious resolver from using a * proof intended for another address. 51 | */ 52 | require(proof.target == target, "proof target does not match resolver"); 53 | /* 54 | * bedrockProofVerifier.getProofValue(proof) always returns the packed result. 55 | * However, libraries like ethers.js expect the result to be encoded in bytes. 56 | * Hence, the gateway needs to encode the result before returning it to the client. 57 | * To ensure responseEncoded matches the value returned by bedrockProofVerifier.getProofValue(proof), 58 | * we need to check the layout of the proof and encode the result accordingly, so we can compare the two values * using the keccak256 hash. 59 | */ 60 | 61 | require( 62 | proof.layout == 0 63 | ? keccak256(bedrockProofVerifier.getProofValue(proof)) == keccak256(responseEncoded) 64 | : keccak256(abi.encode(bedrockProofVerifier.getProofValue(proof))) == keccak256(responseEncoded), 65 | "proof does not match response" 66 | ); 67 | 68 | proofs[i] = responseEncoded; 69 | } 70 | 71 | return abi.encode(proofs); 72 | } 73 | 74 | /** 75 | * @notice Get metadata about the CCIP Resolver 76 | * @dev This function provides metadata about the CCIP Resolver, including its name, coin type, GraphQL URL, storage type, and encoded information. 77 | * @return name The name of the resolver ("CCIP RESOLVER") 78 | * @return coinType Resolvers coin type (60 for Ethereum) 79 | * @return graphqlUrl The GraphQL URL used by the resolver 80 | * @return storageType Storage Type (0 for EVM) 81 | * @return storageLocation The storage identifier. For EVM chains, this is the address of the resolver contract. 82 | * @return context the owner of the name. Always returns address(0) since the owner is determined by the erc3668Resolver contract. 83 | */ 84 | function metadata( 85 | bytes calldata 86 | ) external view override returns (string memory, uint256, string memory, uint8, bytes memory, bytes memory) { 87 | return ( 88 | resolverName, // the name of the resolver 89 | convertEVMChainIdToCoinType(l2ResolverChainID), // coinType according to ENSIP-11 for chain id 420 90 | this.graphqlUrl(), // the GraphQL Url 91 | uint8(0), // storage Type 0 => EVM 92 | abi.encodePacked(address(target)), // storage location => resolver address 93 | abi.encodePacked(address(0)) // context => l2 resolver address 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /contracts/verifier/optimism-bedrock/BedrockProofVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {IBedrockProofVerifier, IL2OutputOracle} from "./IBedrockProofVerifier.sol"; 5 | 6 | import {RLPReader} from "@eth-optimism/contracts-bedrock/contracts/libraries/rlp/RLPReader.sol"; 7 | import {Hashing} from "@eth-optimism/contracts-bedrock/contracts/libraries/Hashing.sol"; 8 | 9 | import {Lib_SecureMerkleTrie} from "@eth-optimism/contracts/libraries/trie/Lib_SecureMerkleTrie.sol"; 10 | 11 | import {BytesLib} from "solidity-bytes-utils/contracts/BytesLib.sol"; 12 | 13 | contract BedrockProofVerifier is IBedrockProofVerifier { 14 | IL2OutputOracle public immutable l2OutputOracle; 15 | 16 | constructor(address _l2OutputOracle) { 17 | l2OutputOracle = IL2OutputOracle(_l2OutputOracle); 18 | } 19 | 20 | /** 21 | * @notice Get the proof value for the provided BedrockStateProof 22 | * @dev This function validates the provided BedrockStateProof and returns the value of the slot or slots included in the proof. 23 | * @param proof The BedrockStateProof struct containing the necessary proof data 24 | * @return result The value of the slot or slots included in the proof 25 | */ 26 | function getProofValue(BedrockStateProof memory proof) public view override returns (bytes memory) { 27 | /* 28 | * Validate the provided output root is valid 29 | * See https://github.com/ethereum-optimism/optimism/blob/4611198bf8bfd16563cc6bdf49bb35eed2e46801/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L261 30 | */ 31 | require( 32 | l2OutputOracle.getL2Output(proof.l2OutputIndex).outputRoot == Hashing.hashOutputRootProof(proof.outputRootProof), 33 | "Invalid output root" 34 | ); 35 | 36 | bytes memory result = getMultipleStorageProofs(proof); 37 | 38 | /* 39 | * If the storage layout is fixed, the result doesn't need to be trimmed 40 | */ 41 | if (proof.layout == 0) { 42 | return result; 43 | } 44 | return trimResult(result, proof.length); 45 | } 46 | 47 | /** 48 | * @notice Get the storage root for the provided BedrockStateProof 49 | * @dev This private function retrieves the storage root based on the provided BedrockStateProof. 50 | * @param proof The BedrockStateProof struct containing the necessary proof data 51 | * @return The storage root retrieved from the provided state root 52 | */ 53 | function getStorageRoot(BedrockStateProof memory proof) private pure returns (bytes32) { 54 | (bool exists, bytes memory encodedResolverAccount) = Lib_SecureMerkleTrie.get( 55 | abi.encodePacked(proof.target), 56 | proof.stateTrieWitness, 57 | proof.outputRootProof.stateRoot 58 | ); 59 | /* 60 | * The account storage root has to be part of the provided state root 61 | * It might take some time for the state root to be posted on L1 after the transaction is included in a block 62 | * Until then, the account might not be part of the state root 63 | */ 64 | require(exists, "Account is not part of the provided state root"); 65 | RLPReader.RLPItem[] memory accountState = RLPReader.readList(encodedResolverAccount); 66 | return bytes32(RLPReader.readBytes(accountState[2])); 67 | } 68 | 69 | /** 70 | * The slot values are padded with 0 so that they are 32 bytes long. This padding has to be returned so the returned value is the same length as the original value 71 | * @param result The concatenated result of all storage slots 72 | * @param length The length of the original value 73 | */ 74 | function trimResult(bytes memory result, uint256 length) private pure returns (bytes memory) { 75 | if (length == 0) { 76 | return result; 77 | } 78 | return BytesLib.slice(result, 0, length); 79 | } 80 | 81 | /** 82 | * @notice Get multiple storage proofs for the provided BedrockStateProof 83 | * @dev Dynamic Types like bytes, strings, or arrays are spread over multiple storage slots. This proves every storage slot the dynamic type contains and returns the concatenated result 84 | * @param proof The BedrockStateProof struct containing the necessary proof data 85 | * @return result The concatenated storage proofs for the provided BedrockStateProof 86 | */ 87 | function getMultipleStorageProofs(BedrockStateProof memory proof) private pure returns (bytes memory) { 88 | bytes memory result = new bytes(0); 89 | /* 90 | * The storage root of the account 91 | */ 92 | bytes32 storageRoot = getStorageRoot(proof); 93 | 94 | /* 95 | * For each sub storage proof, we are proving that the slot is included in the account root of the account 96 | */ 97 | for (uint256 i = 0; i < proof.storageProofs.length; i++) { 98 | bytes memory slotValue = getSingleStorageProof(storageRoot, proof.storageProofs[i]); 99 | /* 100 | * Attach the current slot to the result 101 | */ 102 | result = BytesLib.concat(result, slotValue); 103 | } 104 | return result; 105 | } 106 | 107 | /** 108 | * @notice prove whether the provided storage slot is part of the storageRoot 109 | * @param storageRoot the storage root for the account that contains the storage slot 110 | * @param storageProof the StorageProof struct containing the necessary proof data 111 | * @return the retrieved storage proof value or 0x if the storage slot is empty 112 | */ 113 | function getSingleStorageProof(bytes32 storageRoot, StorageProof memory storageProof) private pure returns (bytes memory) { 114 | (bool storageExists, bytes memory retrievedValue) = Lib_SecureMerkleTrie.get( 115 | abi.encodePacked(storageProof.key), 116 | storageProof.storageTrieWitness, 117 | storageRoot 118 | ); 119 | /* 120 | * this means the storage slot is empty. So we can directly return 0x without RLP encoding it. 121 | */ 122 | if (!storageExists) { 123 | return retrievedValue; 124 | } 125 | return RLPReader.readBytes(retrievedValue); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /contracts/verifier/optimism-bedrock/IBedrockProofVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | import {Types} from "@eth-optimism/contracts-bedrock/contracts/libraries/Types.sol"; 4 | 5 | interface IBedrockProofVerifier { 6 | struct BedrockStateProof { 7 | uint8 layout; 8 | // the address of the contract we are trying to prove 9 | address target; 10 | // the length of the result 11 | uint256 length; 12 | // the state root of the account we are trying to prove 13 | bytes32 storageHash; 14 | // the accountProof RLP-Encoded 15 | bytes stateTrieWitness; 16 | // the output index the proof refers to 17 | uint256 l2OutputIndex; 18 | // the bedrock output RootProof struct 19 | Types.OutputRootProof outputRootProof; 20 | // the storage proofs for each slot included in the proof 21 | StorageProof[] storageProofs; 22 | } 23 | struct StorageProof { 24 | // the slot address 25 | bytes32 key; 26 | // the storageProof RLP-Encoded 27 | bytes storageTrieWitness; 28 | } 29 | 30 | /// @notice returns the value of one or more storage slots given the provided proof is correct 31 | /// @param proof BedrockStateProof 32 | /// @return the value of all included slots concatenated 33 | function getProofValue(BedrockStateProof memory proof) external view returns (bytes memory); 34 | } 35 | 36 | interface IL2OutputOracle { 37 | function getL2Output(uint256 _l2OutputIndex) external view returns (Types.OutputProposal memory); 38 | } 39 | -------------------------------------------------------------------------------- /contracts/verifier/signature/SignatureCcipVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.17; 3 | 4 | import {CcipResponseVerifier} from "../CcipResponseVerifier.sol"; 5 | import {SignatureVerifier} from "./SignatureVerifier.sol"; 6 | import {convertEVMChainIdToCoinType} from "../../coinType/Ensip11CoinType.sol"; 7 | 8 | contract SignatureCcipVerifier is CcipResponseVerifier { 9 | address public immutable resolver; 10 | 11 | mapping(address => bool) public signers; 12 | 13 | event NewOwner(address newOwner); 14 | event NewSigners(address[] signers); 15 | event SignerRemoved(address removedSigner); 16 | 17 | constructor( 18 | address _owner, 19 | string memory _graphQlUrl, 20 | string memory _resolverName, 21 | uint256 _l2ResolverChainID, 22 | address _resolver, 23 | address[] memory _signers 24 | ) CcipResponseVerifier(_owner, _graphQlUrl, _resolverName, _l2ResolverChainID) { 25 | resolver = _resolver; 26 | 27 | for (uint256 i = 0; i < _signers.length; i++) { 28 | signers[_signers[i]] = true; 29 | } 30 | emit NewSigners(_signers); 31 | } 32 | 33 | /** 34 | * @notice Add new signers 35 | * @dev This function can only be called by the current contract owner. It adds the provided addresses as new signers. 36 | * @param _signers An array of addresses representing the new signers to be added 37 | */ 38 | function addSigners(address[] memory _signers) external onlyOwner { 39 | for (uint256 i = 0; i < _signers.length; i++) { 40 | signers[_signers[i]] = true; 41 | } 42 | emit NewSigners(_signers); 43 | } 44 | 45 | /** 46 | * @notice Remove signers 47 | * @dev This function can only be called by the current contract owner. It removes the provided addresses from the list of authorized signers. 48 | * @param _signers An array of addresses representing the signers to be removed 49 | */ 50 | function removeSigners(address[] memory _signers) external onlyOwner { 51 | for (uint256 i = 0; i < _signers.length; i++) { 52 | /* 53 | * Without this if check, it's possible to add a signer to the SignerRemoved Event 54 | * that never was a signer in the first place. 55 | * This may cause failures at indexing services that are trying to delete a non-existing signer... 56 | */ 57 | if (signers[_signers[i]]) { 58 | signers[_signers[i]] = false; 59 | emit SignerRemoved(_signers[i]); 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * @notice Resolve with Proof 66 | * @dev This function is used to resolve a response with a proof using the SignatureVerifier. 67 | * @param response The response data returned from the SignatureVerifier 68 | * @param extraData Additional data needed for verification 69 | * @return The decoded response data 70 | * Note: It's essential to handle access control mechanisms properly to ensure that only authorized signers can resolve responses with proofs. 71 | */ 72 | function resolveWithProof( 73 | bytes calldata response, 74 | bytes calldata extraData 75 | ) external view override returns (bytes memory) { 76 | (address signer, bytes memory result) = SignatureVerifier.verify(resolver, extraData, response); 77 | require(signers[signer], "SignatureVerifier: Invalid signature"); 78 | /** 79 | * @dev Because this function is meant to be called via staticcall, we need to decode the response data before returning it. 80 | */ 81 | bytes memory decodedResponse = abi.decode(result, (bytes)); 82 | return decodedResponse; 83 | } 84 | 85 | /** 86 | * @param response The response bytes received from the AddrResolver. 87 | * @return The Ethereum address resolved from the response bytes. 88 | * @dev The AddrResolver stores addresses as bytes instead of Ethereum addresses. 89 | * This is done to support other blockchain addresses, not just EVM addresses. 90 | * However, the return type of `addr(bytes32)` is `address`, 91 | * which means the client library expects an Ethereum address to be returned. 92 | * To meet this expectation, we convert the bytes into an Ethereum address and return it. 93 | */ 94 | function resolveWithAddress(bytes calldata response, bytes calldata extraData) public view returns (address) { 95 | (address signer, bytes memory result) = SignatureVerifier.verify(resolver, extraData, response); 96 | require(signers[signer], "SignatureVerifier: Invalid signature"); 97 | 98 | /** 99 | * The AddrResolver stores addresses as bytes instead of Ethereum addresses. 100 | * This is to support other blockchain addresses and not just EVM addresses. 101 | * However, the return type of `addr(bytes32)` is `address`, 102 | * so the client library expects an Ethereum address to be returned. 103 | * For that reason, we have to convert the bytes into an address. 104 | */ 105 | return address(bytes20(result)); 106 | } 107 | 108 | /** 109 | * @dev Can be called to determine what function to use to handle resolveWithProof. Returns the selector that then can be called via staticcall 110 | * @return The four-byte function selector of the corresponding resolution function.. 111 | */ 112 | function onResolveWithProof(bytes calldata, bytes calldata data) public pure override returns (bytes4) { 113 | /** 114 | * if the function addr(bytes32) is called, return the selector of resolveWithAddress. 115 | */ 116 | if (bytes4(data) == 0x3b3b57de) { 117 | return this.resolveWithAddress.selector; 118 | } 119 | /** 120 | * any other selector will be handled by the default resolveWithProof function. 121 | */ 122 | return this.resolveWithProof.selector; 123 | } 124 | 125 | /** 126 | * @notice Get metadata about the CCIP Resolver 127 | * @dev This function provides metadata about the CCIP Resolver, including its name, coin type, GraphQL URL, storage type, and encoded information. 128 | * @return name The name of the resolver ("CCIP RESOLVER") 129 | * @return coinType Resolvers coin type (60 for Ethereum) 130 | * @return graphqlUrl The GraphQL URL used by the resolver 131 | * @return storageType Storage Type (0 for EVM) 132 | * @return storageLocation The storage identifier. For EVM chains, this is the address of the resolver contract. 133 | * @return context The owner of the name. Always returns address(0) since the owner is determined by the erc3668Resolver contract. 134 | */ 135 | function metadata( 136 | bytes calldata 137 | ) external view override returns (string memory, uint256, string memory, uint8, bytes memory, bytes memory) { 138 | return ( 139 | resolverName, // The name of the resolver 140 | convertEVMChainIdToCoinType(60), // Resolvers coin type => Ethereum 141 | this.graphqlUrl(), // The GraphQL Url 142 | uint8(1), // Storage Type 0 => Offchain Database 143 | "Postgres", // Storage Location => Resolver Address 144 | abi.encodePacked(address(0)) // Context => Owner Address 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /contracts/verifier/signature/SignatureVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.17; 4 | 5 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | 7 | library SignatureVerifier { 8 | /** 9 | * @dev Generates a hash for signing/verifying. 10 | * @param target: The address the signature is for.(RESOLVER) 11 | * @param request: The original request that was sent. 12 | * @param result: The `result` field of the response (not including the signature part). 13 | */ 14 | function makeSignatureHash(address target, uint64 expires, bytes memory request, bytes memory result) internal pure returns (bytes32) { 15 | return keccak256(abi.encodePacked(hex"1900", target, expires, keccak256(request), keccak256(result))); 16 | } 17 | 18 | /** 19 | * @dev Verifies a signed message returned from a callback. 20 | * @param resolver: The address the signature is for.(RESOLVER) 21 | * @param request: The original request that was sent. 22 | * @param response: An ABI encoded tuple of `(bytes result, uint64 expires, bytes sig)`, where `result` is the data to return 23 | * to the caller, and `sig` is the (r,s,v) encoded message signature. 24 | * @return signer: The address that signed this message. 25 | * @return result: The `result` decoded from `response`. 26 | */ 27 | function verify(address resolver, bytes calldata request, bytes calldata response) internal view returns (address, bytes memory) { 28 | (bytes memory result, uint64 expires, bytes memory sig) = abi.decode(response, (bytes, uint64, bytes)); 29 | address signer = ECDSA.recover(ECDSA.toEthSignedMessageHash(makeSignatureHash(resolver, expires, request, result)), sig); 30 | require(expires >= block.timestamp, "SignatureVerifier: Signature expired"); 31 | return (signer, result); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /deploy/01_BedrockProofVerifier.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from 'hardhat'; 2 | 3 | // https://community.optimism.io/docs/useful-tools/networks/ 4 | const L2_OUTPUT_ORALCE_GOERLI = '0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0'; 5 | const L2_OUTPUT_ORALCE_MAINNET = '0xdfe97868233d1aa22e815a266982f2cf17685a27'; 6 | async function main() { 7 | const chainId = await hre.getChainId(); 8 | const l2OutputOracleAddress = chainId === '1' ? L2_OUTPUT_ORALCE_MAINNET : L2_OUTPUT_ORALCE_GOERLI; 9 | 10 | const BedrockProofVerifierFactory = await ethers.getContractFactory('BedrockProofVerifier'); 11 | const deployTx = await BedrockProofVerifierFactory.deploy(l2OutputOracleAddress, { 12 | gasLimit: 5000000, 13 | }); 14 | 15 | await deployTx.deployed(); 16 | 17 | console.log(`BedrockProofVerifier deployed at ${deployTx.address}`); 18 | 19 | console.log( 20 | `Verify the contract using npx hardhat verify --network ${hre.network.name} ${deployTx.address} ${l2OutputOracleAddress} `, 21 | ); 22 | } 23 | 24 | // We recommend this pattern to be able to use async/await everywhere 25 | // and properly handle errors. 26 | main().catch(error => { 27 | console.error(error); 28 | process.exitCode = 1; 29 | }); 30 | module.exports.default = main; 31 | -------------------------------------------------------------------------------- /deploy/02_BedrockCcipVerifier.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from 'hardhat'; 2 | 3 | async function main() { 4 | const [owner] = await ethers.getSigners(); 5 | 6 | const graphQlUrl = 'http://localhost:8081/graphql'; 7 | const resolverName = 'Optimism Goerli'; 8 | const resolverChainID = 420; 9 | const bedrockProofVerifierAddress = '0x49FA2e3dc397d6AcA8e2DAe402eB2fD6164EebAC'; 10 | const l2ResolverAddress = '0x39Dc8A3A607970FA9F417D284E958D4cA69296C8'; 11 | 12 | const BedrockProofVerifierFactory = await ethers.getContractFactory('BedrockCcipVerifier'); 13 | const deployTx = await BedrockProofVerifierFactory.deploy( 14 | owner.address, 15 | graphQlUrl, 16 | resolverName, 17 | resolverChainID, 18 | bedrockProofVerifierAddress, 19 | l2ResolverAddress, 20 | ); 21 | await deployTx.deployed(); 22 | 23 | console.log(`BedrockCcipVerifier deployed at ${deployTx.address}`); 24 | console.log( 25 | `Verify the contract using npx hardhat verify --network ${hre.network.name} ${deployTx.address} ${owner.address} ${graphQlUrl} ${bedrockProofVerifierAddress} ${l2ResolverAddress} `, 26 | ); 27 | } 28 | 29 | // We recommend this pattern to be able to use async/await everywhere 30 | // and properly handle errors. 31 | main().catch(error => { 32 | console.error(error); 33 | process.exitCode = 1; 34 | }); 35 | module.exports.default = main; 36 | -------------------------------------------------------------------------------- /deploy/03_ERC3668Resolver.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from 'hardhat'; 2 | 3 | const NAMEWRAPPER_GOERLI = '0x114D4603199df73e7D157787f8778E21fCd13066'; 4 | const NAMEWRAPPER_MAINNET = '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401'; 5 | const ENS_REGISTRY = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'; 6 | 7 | const DEFAULT_VERIFIER_ADDRESS = '0x183C1F81D0159794973c157694627a689DEB9F72'; 8 | const DEFAULT_VERIFIER_URL = 'http://localhost:8081/{sender}/{data}'; 9 | 10 | async function main() { 11 | const chainId = await hre.getChainId(); 12 | const namewrapper = chainId === '1' ? NAMEWRAPPER_MAINNET : NAMEWRAPPER_GOERLI; 13 | const OptimismResolverFactory = await ethers.getContractFactory('ERC3668Resolver'); 14 | 15 | const deployTx = await OptimismResolverFactory.deploy(ENS_REGISTRY, namewrapper, DEFAULT_VERIFIER_ADDRESS, [ 16 | DEFAULT_VERIFIER_URL, 17 | ]); 18 | 19 | await deployTx.deployed(); 20 | 21 | console.log('ERC3668Resolver deployed to:', deployTx.address); 22 | } 23 | 24 | // We recommend this pattern to be able to use async/await everywhere 25 | // and properly handle errors. 26 | main().catch(error => { 27 | console.error(error); 28 | process.exitCode = 1; 29 | }); 30 | module.exports.default = main; 31 | -------------------------------------------------------------------------------- /deploy/04_SignatureCcipVerifier.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from 'hardhat'; 2 | 3 | const CCIP_RESOLVER_ADDRESS = '0x49e0AeC78ec0dF50852E99116E524a43bE91B789'; 4 | const RESOLVER_NAME = 'SignatureCcipVerifier'; 5 | const RESOLVER_CHAINID = 60; 6 | const GraphQlUrl = 'http://localhost:8081/graphql'; 7 | async function main() { 8 | const [signer] = await ethers.getSigners(); 9 | 10 | const SignatureVerifier = await ethers.getContractFactory('SignatureCcipVerifier'); 11 | const deployTx = await SignatureVerifier.deploy( 12 | signer.address, 13 | GraphQlUrl, 14 | RESOLVER_CHAINID, 15 | RESOLVER_NAME, 16 | CCIP_RESOLVER_ADDRESS, 17 | [signer.address], 18 | ); 19 | await deployTx.deployed(); 20 | 21 | console.log(`SignatureCcipVerifier deployed at ${deployTx.address}`); 22 | } 23 | 24 | // We recommend this pattern to be able to use async/await everywhere 25 | // and properly handle errors. 26 | main().catch(error => { 27 | console.error(error); 28 | process.exitCode = 1; 29 | }); 30 | module.exports.default = main; 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | 3 | services: 4 | ens-data-source: 5 | image: ens_bedrock_resolver:latest 6 | restart: always 7 | environment: 8 | L2_RESOLVER_ADDRESS: "0x123456" 9 | 10 | 11 | -------------------------------------------------------------------------------- /gateway/config/Config.ts: -------------------------------------------------------------------------------- 1 | export interface SigningConfigEntry { 2 | type: 'signing'; 3 | handlerUrl: string; 4 | } 5 | 6 | export interface OptimismBedrockConfigEntry { 7 | type: 'optimism-bedrock'; 8 | handlerUrl: string; 9 | l1ProviderUrl: string; 10 | l2ProviderUrl: string; 11 | l1chainId: string; 12 | l2chainId: string; 13 | } 14 | /** 15 | * Every supported Config by the Gateway. 16 | */ 17 | export type ConfigEntry = SigningConfigEntry | OptimismBedrockConfigEntry; 18 | 19 | /** 20 | * Checks wether the provided ConfigEntry is a SigningConfigEntry. 21 | */ 22 | export function isSigningConfigEntry(configEntry: ConfigEntry): configEntry is SigningConfigEntry { 23 | return configEntry.type === 'signing'; 24 | } 25 | 26 | /** 27 | * Checks wether the provided ConfigEntry is a OptimismBedrockConfigEntry. 28 | */ 29 | export function isOptimismBedrockConfigEntry(configEntry: ConfigEntry): configEntry is OptimismBedrockConfigEntry { 30 | return configEntry.type === 'optimism-bedrock'; 31 | } 32 | /** 33 | * The gateway can be configured to support multiple resolvers. 34 | */ 35 | export type Config = Record; 36 | -------------------------------------------------------------------------------- /gateway/config/ConfigReader.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | 3 | import { Config, ConfigEntry } from './Config'; 4 | 5 | export function getConfigReader(_config?: string) { 6 | if (!_config) { 7 | throw new Error('CONFIG IS MISSING'); 8 | } 9 | 10 | let config: Config; 11 | 12 | try { 13 | config = JSON.parse(_config); 14 | } catch (e) { 15 | throw new Error('Invalid JSON'); 16 | } 17 | 18 | Object.keys(config).forEach((address: string) => { 19 | if (!ethers.utils.isAddress(address)) { 20 | throw new Error(`Invalid address ${address}`); 21 | } 22 | const normalizedAddress = ethers.utils.getAddress(address); 23 | config[normalizedAddress] = config[address] as ConfigEntry; 24 | }); 25 | console.log(config); 26 | 27 | function getConfigForResolver(resolverAddr: string): ConfigEntry { 28 | const normalizedAddr = ethers.utils.getAddress(resolverAddr); 29 | return config[normalizedAddr]; 30 | } 31 | return { 32 | getConfigForResolver, 33 | }; 34 | } 35 | 36 | export type ConfigReader = { 37 | getConfigForResolver: (resolverAddr: string) => ConfigEntry; 38 | }; 39 | -------------------------------------------------------------------------------- /gateway/handler/optimism-bedrock/optimismBedrockHandler.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ethers } from 'ethers'; 3 | 4 | import { OptimismBedrockConfigEntry } from '../../config/Config'; 5 | import { getProofParamType } from '../../service/encoding/proof/getProofParamType'; 6 | import { ProofService, StorageLayout } from '../../service/proof/ProofService'; 7 | 8 | export async function optimismBedrockHandler( 9 | calldata: string, 10 | resolverAddr: string, 11 | configEntry: OptimismBedrockConfigEntry, 12 | ) { 13 | /** 14 | * The optimism-handler has to return the following data: 15 | * 1. The target contract address. This is the contract deployed on Optimism that contains the state we want to resolve. 16 | * 2. The slot of the state we want to resolve. 17 | * 3. The layout of the state we want to resolve. This can be either fixed(address,bytes32,uint256) or dynamic(string,bytes,array). 18 | */ 19 | const proofRequests = (await axios.get(`${configEntry.handlerUrl}/${resolverAddr}/${calldata}`)).data; 20 | 21 | const l1Provider = new ethers.providers.StaticJsonRpcProvider(configEntry.l1ProviderUrl); 22 | const l2Provider = new ethers.providers.StaticJsonRpcProvider(configEntry.l2ProviderUrl); 23 | 24 | /** 25 | * Detect the network of the providers. This is required to create the proof. 26 | */ 27 | await Promise.all([l1Provider.detectNetwork(), l2Provider.detectNetwork()]); 28 | 29 | // for each proof request, create a proof 30 | const proofs = await Promise.all( 31 | proofRequests.map( 32 | async ({ 33 | target, 34 | slot, 35 | layout, 36 | result, 37 | }: { 38 | target: string; 39 | slot: string; 40 | layout: StorageLayout; 41 | result: string; 42 | }) => { 43 | if (!target || !slot || layout === undefined) { 44 | throw new Error('optimismBedrockHandler : Invalid data source response'); 45 | } 46 | 47 | const { proof, result: proofResult } = await new ProofService(l1Provider, l2Provider).createProof( 48 | target, 49 | slot, 50 | layout, 51 | ); 52 | 53 | console.log('Proof result: ', proofResult); 54 | console.log('Handler result: ', result); 55 | console.log(proof); 56 | 57 | const proofParamType = await getProofParamType(); 58 | return ethers.utils.defaultAbiCoder.encode(['bytes', proofParamType], [result, proof]); 59 | }, 60 | ), 61 | ); 62 | // return the proofs as bytes array 63 | return ethers.utils.defaultAbiCoder.encode(['bytes[]'], [proofs]); 64 | } 65 | -------------------------------------------------------------------------------- /gateway/handler/signing/signAndEncodeResponse.ts: -------------------------------------------------------------------------------- 1 | import { ethers, Signer } from 'ethers'; 2 | 3 | /** 4 | * @param signer A signer to sign the request. The address of the signer HAS to be part of 5 | * the signers array of the Offchain Resolver otherwise {@see resolveWithProof} will fail 6 | * @param resolverAddr The addrees of the Offchain Resolver smart contract 7 | * @param result The actual data 8 | * @param request The calldata the resolve method of the OffchainProcessor returns {@see decodeCalldata} 9 | * @param ttl the time to life to calculate validUntil. 10 | * @returns the encoded response 11 | */ 12 | export async function signAndEncodeResponse( 13 | signer: Signer, 14 | resolverAddr: string, 15 | result: string, 16 | calldata: string, 17 | ttl: number = 30000, 18 | ): Promise { 19 | const validUntil = Math.floor(Date.now() / 1000 + ttl); 20 | global.logger.debug({ message: 'signAndEncodeResponse', signer, resolverAddr, result, calldata, validUntil }); 21 | /** 22 | * This hash has to be compiled the same way as at the OffchainResolver.makeSignatureHash method 23 | * since it'll be compared within the {@see resolveWithProof} function 24 | */ 25 | 26 | const messageHash = ethers.utils.solidityKeccak256( 27 | ['bytes', 'address', 'uint64', 'bytes32', 'bytes32'], 28 | ['0x1900', resolverAddr, validUntil, ethers.utils.keccak256(calldata), ethers.utils.keccak256(result)], 29 | ); 30 | 31 | const msgHashDigest = ethers.utils.arrayify(messageHash); 32 | /** 33 | * The signature is used to verify onchain if this response object was indeed signed by a valid signer 34 | */ 35 | const sig = await signer.signMessage(msgHashDigest); 36 | 37 | global.logger.debug({ message: 'signAndEncodeResponse result', result, validUntil, sig }); 38 | return ethers.utils.defaultAbiCoder.encode(['bytes', 'uint64', 'bytes'], [result, validUntil, sig]); 39 | } 40 | -------------------------------------------------------------------------------- /gateway/handler/signing/signingHandler.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ethers } from 'ethers'; 3 | import { Logger } from 'winston'; 4 | 5 | import { SigningConfigEntry } from '../../config/Config'; 6 | 7 | import { signAndEncodeResponse } from './signAndEncodeResponse'; 8 | 9 | /** 10 | * Signs the provided calldata using the resolver address and returns the signed and encoded response. 11 | * @param calldata - The calldata to be signed. 12 | * @param resolverAddr - The resolver address. 13 | * @param configEntry - The signing configuration entry. 14 | * @returns The signed and encoded response. 15 | */ 16 | export async function signingHandler(calldata: string, resolverAddr: string, configEntry: SigningConfigEntry) { 17 | /** 18 | * Fetches the result from the data source. 19 | */ 20 | 21 | let result; 22 | try { 23 | result = (await axios.get(`${configEntry.handlerUrl}/${resolverAddr}/${calldata}`)).data; 24 | } catch (e) { 25 | throw new Error('signingHandler : Invalid data source response'); 26 | } 27 | /** 28 | * Read the private key from the environment variable. 29 | */ 30 | const singerPk = process.env.SIGNER_PRIVATE_KEY; 31 | 32 | if (!singerPk) { 33 | throw new Error('signingHandler : no private key provided'); 34 | } 35 | 36 | /** 37 | * Sign and encode the response the signingHandler has returned using the private key from the environment variable. 38 | */ 39 | const signer = new ethers.Wallet(singerPk); 40 | global.logger.info({ message: 'signingHandler', signer: signer.address }); 41 | return signAndEncodeResponse(signer, resolverAddr, result, calldata); 42 | } 43 | -------------------------------------------------------------------------------- /gateway/http/ccipGateway.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Logger } from 'winston'; 3 | 4 | import { ConfigReader } from '../config/ConfigReader'; 5 | import { optimismBedrockHandler } from '../handler/optimism-bedrock/optimismBedrockHandler'; 6 | import { signingHandler } from '../handler/signing/signingHandler'; 7 | 8 | /** 9 | * Creates an Express router to handle requests for the CCIP gateway. 10 | * @param configReader - The configuration reader that provides the necessary config entries. 11 | * @returns The Express router for the CCIP gateway. 12 | */ 13 | export function ccipGateway(configReader: ConfigReader) { 14 | const router = express.Router(); 15 | 16 | router.get('/:resolverAddr/:calldata', async (req: express.Request, res: express.Response) => { 17 | const { resolverAddr } = req.params; 18 | const calldata = req.params.calldata.replace('.json', ''); 19 | 20 | try { 21 | // eslint-disable max-line-length 22 | /** 23 | * To get the right handler for a resolverAddr, we need to look up the config entry 24 | * for that resolverAddr. 25 | * The host of the gateway has to specifiy in the CONFIG environment variable which config file to use. 26 | * The config file is a JSON file that maps resolverAddr to config entries. The config entries are either 27 | * signing or optimism-bedrock config entries. The config entries contain the handlerUrl, 28 | * which is the URL of the handler that should be used for that resolverAddr. 29 | */ 30 | const configEntry = configReader.getConfigForResolver(resolverAddr); 31 | 32 | if (!configEntry) { 33 | /** 34 | * If there is no config entry for the resolverAddr, we return a 404. As there is no way for the gateway to resolve the request 35 | */ 36 | global.logger.warn(`Unknown resolver selector pair for resolverAddr: ${resolverAddr}`); 37 | 38 | res.status(404).send({ 39 | message: 'Unknown resolver selector pair', 40 | }); 41 | return; 42 | } 43 | /** 44 | * To get the data from the offchain resolver we make a request to the corosspeding handlerUrl. 45 | * That handler has to return the data in the format that the resolver expects. 46 | */ 47 | switch (configEntry.type) { 48 | case 'signing': { 49 | global.logger.info({ type: 'signing' }); 50 | global.logger.debug({ type: 'signing', calldata, resolverAddr, configEntry }); 51 | const response = await signingHandler(calldata, resolverAddr, configEntry); 52 | res.status(200).send({ data: response }); 53 | break; 54 | } 55 | case 'optimism-bedrock': { 56 | global.logger.info({ type: 'optimism-bedrock' }); 57 | global.logger.debug({ type: 'optimism-bedrock', calldata, resolverAddr, configEntry }); 58 | const response = await optimismBedrockHandler(calldata, resolverAddr, configEntry); 59 | 60 | res.status(200).send({ data: response }); 61 | break; 62 | } 63 | 64 | default: 65 | res.status(404).send({ 66 | message: 'Unsupported entry type', 67 | }); 68 | } 69 | } catch (e) { 70 | global.logger.warn((e as Error).message); 71 | res.status(400).send({ message: 'ccip gateway error ,' + e }); 72 | } 73 | }); 74 | return router; 75 | } 76 | -------------------------------------------------------------------------------- /gateway/index.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | import bodyParser from "body-parser"; 3 | import express from "express"; 4 | import http from "http"; 5 | import cors from "cors"; 6 | import winston from "winston"; 7 | 8 | import { ccipGateway } from "./http/ccipGateway"; 9 | import { getConfigReader } from "./config/ConfigReader"; 10 | 11 | dotenv.config(); 12 | 13 | declare global { 14 | var logger: winston.Logger 15 | } 16 | 17 | global.logger = winston.createLogger({ 18 | level: process.env.LOG_LEVEL ?? 'info', 19 | transports: [new winston.transports.Console()], 20 | }); 21 | 22 | const app = express(); 23 | app.use(express.json({ limit: "50mb" })); 24 | app.use(express.urlencoded({ limit: "50mb" })); 25 | 26 | const server = http.createServer(app); 27 | 28 | app.use(cors()); 29 | app.use(bodyParser.json()); 30 | 31 | (async () => { 32 | const config = getConfigReader(process.env.CONFIG); 33 | app.use("/", ccipGateway(config)); 34 | })(); 35 | const port = process.env.PORT || "8081"; 36 | server.listen(port, () => { 37 | global.logger.info("[Server] listening at port " + port + " and dir " + __dirname); 38 | }); 39 | -------------------------------------------------------------------------------- /gateway/service/encoding/proof/getProofParamType.ts: -------------------------------------------------------------------------------- 1 | // This code instantiates the BedrockProofVerifierFactory contract and returns the type of the L2StateProofParamType 2 | 3 | import { ethers } from 'ethers'; 4 | 5 | import { BedrockProofVerifier__factory } from '../../../../typechain'; 6 | 7 | // This function returns the BedrockProofVerifier contract's "getProofValue" interface. This interface can 8 | // be used to get the L2StateProof, which is the type of the "proof" param in the "getProofValue" 9 | // function. 10 | 11 | export async function getProofParamType() { 12 | const iFace = new ethers.utils.Interface(BedrockProofVerifier__factory.abi); 13 | const [_, getProofValueFragment] = iFace.fragments; 14 | const [L2StateProofParamType] = getProofValueFragment.inputs; 15 | return L2StateProofParamType; 16 | } 17 | -------------------------------------------------------------------------------- /gateway/service/proof/ProofService.ts: -------------------------------------------------------------------------------- 1 | import { asL2Provider, CrossChainMessenger, L2Provider } from '@eth-optimism/sdk'; 2 | import { BigNumber, ethers } from 'ethers'; 3 | import { keccak256 } from 'ethers/lib/utils'; 4 | 5 | import { toRpcHexString } from './toRpcHexString'; 6 | import { CreateProofResult, EthGetProofResponse, StorageProof } from './types'; 7 | 8 | export enum StorageLayout { 9 | /** 10 | * address,uint,bytes32,bool 11 | */ 12 | FIXED, 13 | /** 14 | * array,bytes,string 15 | */ 16 | DYNAMIC, 17 | } 18 | /** 19 | * The proofService class can be used to calculate proofs for a given target and slot on the Optimism Bedrock network. 20 | * It's also capable of proofing long types such as mappings or string by using all included slots in the proof. 21 | * 22 | */ 23 | export class ProofService { 24 | private readonly l1Provider: ethers.providers.StaticJsonRpcProvider; 25 | private readonly l2Provider: L2Provider; 26 | private readonly crossChainMessenger: CrossChainMessenger; 27 | 28 | constructor(l1Provider: ethers.providers.StaticJsonRpcProvider, l2Provider: ethers.providers.JsonRpcProvider) { 29 | this.l1Provider = l1Provider; 30 | this.l2Provider = asL2Provider(l2Provider); 31 | 32 | this.crossChainMessenger = new CrossChainMessenger({ 33 | l1ChainId: l1Provider.network.chainId, 34 | l2ChainId: l2Provider.network.chainId, 35 | l1SignerOrProvider: this.l1Provider, 36 | l2SignerOrProvider: this.l2Provider, 37 | bedrock: true, 38 | }); 39 | } 40 | 41 | /** 42 | * Creates a {@see CreateProofResult} for a given target and slot. 43 | * @param target The address of the smart contract that contains the storage slot 44 | * @param slot The storage slot the proof should be created for 45 | */ 46 | 47 | public async createProof( 48 | target: string, 49 | slot: string, 50 | layout: StorageLayout = StorageLayout.DYNAMIC, 51 | ): Promise { 52 | /** 53 | * use the most recent block,posted to L1, to build the proof 54 | */ 55 | const { l2OutputIndex, number, stateRoot, hash } = await this.getLatestProposedBlock(); 56 | 57 | const { storageProof, storageHash, accountProof, length } = await this.getProofForSlot( 58 | slot, 59 | number, 60 | target, 61 | layout, 62 | ); 63 | 64 | /** 65 | * The messengePasserStorageRoot is important for the verification on chain 66 | */ 67 | const messagePasserStorageRoot = await this.getMessagePasserStorageRoot(number); 68 | 69 | const proof = { 70 | layout, 71 | // The contract address of the slot beeing proofed 72 | target, 73 | // The length actual length of the value 74 | length, 75 | // RLP encoded account proof 76 | stateTrieWitness: ethers.utils.RLP.encode(accountProof), 77 | // The state output the proof is beeing created for 78 | l2OutputIndex, 79 | // The storage hash of the target 80 | storageHash, 81 | // Bedrock OutputRootProof type 82 | outputRootProof: { 83 | version: ethers.constants.HashZero, 84 | stateRoot, 85 | messagePasserStorageRoot, 86 | latestBlockhash: hash, 87 | }, 88 | // RLP encoded storage proof for every slot 89 | storageProofs: storageProof, 90 | }; 91 | 92 | // The result is not part of the proof but its convenient to have it i:E in tests 93 | const result = storageProof 94 | .reduce((agg, cur) => agg + cur.value.substring(2), '0x') 95 | .substring(0, length * 2 + 2); 96 | return { result, proof }; 97 | } 98 | /** 99 | * Retrieves the latest proposed block. 100 | * @returns An object containing the state root, hash, number, and L2 output index of the latest proposed block. 101 | * @throws An error if the state root for the block is not found. 102 | */ 103 | private async getLatestProposedBlock() { 104 | /** 105 | * Get the latest ouput from the L2Oracle. We're building the proove with this batch 106 | * We go 5 batches backwards to avoid errors like delays between nodes 107 | * 108 | */ 109 | 110 | const l2OutputIndex = (await this.crossChainMessenger.contracts.l1.L2OutputOracle.latestOutputIndex()).sub(5); 111 | /** 112 | * struct OutputProposal { 113 | * bytes32 outputRoot; 114 | * uint128 timestamp; 115 | * uint128 l2BlockNumber; 116 | * } 117 | */ 118 | const outputProposal = await this.crossChainMessenger.contracts.l1.L2OutputOracle.getL2Output(l2OutputIndex); 119 | 120 | /** 121 | * We're getting the block for that the output was created for. The stateRoot contains the storageRoot of the account we're prooving. 122 | */ 123 | const { stateRoot, hash } = (await this.l2Provider.getBlock(outputProposal.l2BlockNumber.toNumber())) as any; 124 | 125 | /** 126 | * Although the stateRoot is not part of the ethers.Bock type it'll be returned by the Optimism RPC 127 | */ 128 | if (!stateRoot) { 129 | throw new Error(`StateRoot for block ${outputProposal.l2BlockNumber.toNumber()} not found`); 130 | } 131 | 132 | return { 133 | stateRoot, 134 | hash, 135 | number: outputProposal.l2BlockNumber.toNumber(), 136 | l2OutputIndex: l2OutputIndex.toNumber(), 137 | }; 138 | } 139 | 140 | /** 141 | * Gets the storage proof for a given slot based on the provided layout. 142 | * @param initalSlot - The initial slot. 143 | * @param blockNr - The block number. 144 | * @param resolverAddr - The resolver address. 145 | * @param layout - The storage layout (fixed or dynamic). 146 | * To get an better understanding how the storage layout looks like visit {@link https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html} 147 | * @returns The storage proof and related information. 148 | */ 149 | private async getProofForSlot( 150 | initalSlot: string, 151 | blockNr: number, 152 | resolverAddr: string, 153 | layout: StorageLayout, 154 | ): Promise<{ 155 | storageProof: (StorageProof & { value: string })[]; 156 | accountProof: string; 157 | storageHash: string; 158 | length: number; 159 | }> { 160 | if (layout === StorageLayout.FIXED) { 161 | /** 162 | * A fixed slot always is a single slot 163 | */ 164 | return this.handleShortType(resolverAddr, initalSlot, blockNr, 32); 165 | } 166 | 167 | /** 168 | * The initial value. We used it to determine how many slots we need to proof 169 | * See https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html#mappings-and-dynamic-arrays 170 | */ 171 | const slotValue = await this.l2Provider.getStorageAt(resolverAddr, initalSlot, blockNr); 172 | /** 173 | * The length of the value is encoded in the last byte of the slot 174 | */ 175 | const length = this.decodeLength(slotValue); 176 | 177 | /** 178 | * Handle slots at most 31 bytes long 179 | */ 180 | if (length <= 31) { 181 | return this.handleShortType(resolverAddr, initalSlot, blockNr, length); 182 | } 183 | return this.handleLongType(initalSlot, resolverAddr, blockNr, length); 184 | } 185 | /** 186 | * Decodes the length of a storage slot based on the provided slot value. This is important to determine weher the slot is a short or long type. 187 | * @param slot - The storage slot value as a hexadecimal string. 188 | * @returns The decoded length of the storage slot. 189 | */ 190 | private decodeLength(slot: string) { 191 | const lastByte = slot.substring(slot.length - 2); 192 | const lastBit = parseInt(lastByte, 16) % 2; 193 | 194 | /** 195 | * If the last bit is not set it is a short type 196 | */ 197 | if (lastBit === 0) { 198 | /** 199 | * For short types the length can be encoded by calculating length / 2 200 | */ 201 | return BigNumber.from('0x' + lastByte) 202 | .div(2) 203 | .toNumber(); 204 | } 205 | /** 206 | * For long types the length can be encoded by calculating length *2+1 207 | */ 208 | return BigNumber.from(slot).sub(1).div(2).toNumber(); 209 | } 210 | /** 211 | * Handles the short type of storage layout (length <= 31). 212 | * @param resolverAddr - The resolver address. 213 | * @param slot - The storage slot value as a hexadecimal string. 214 | * @param blockNr - The block number. 215 | * @param length - The length of the slot (up to 31 bytes). 216 | * @returns An object containing the account proof, storage proof, storage hash, and length. 217 | */ 218 | private async handleShortType(resolverAddr: string, slot: string, blockNr: number, length: number) { 219 | /** 220 | * Proving the short type is simple all we have to do is to call eth_getProof 221 | */ 222 | const { storageProof, accountProof, storageHash } = await this.makeGetProofRpcCall( 223 | resolverAddr, 224 | [slot], 225 | blockNr, 226 | ); 227 | 228 | return { 229 | accountProof, 230 | storageProof: this.rlpEncodeStroageProof(storageProof), 231 | storageHash, 232 | length, 233 | }; 234 | } 235 | /** 236 | * Handles the long type of storage layout (length > 31). 237 | * @param initialSlot - The initial slot as a hexadecimal string, which contains the length of the entire data structure. 238 | * @param resolverAddr - The resolver address. 239 | * @param blockNr - The block number. 240 | * @param length - The length of the slot (greater than 31 bytes). 241 | * @returns An object containing the account proof, storage proof, storage hash, and length. 242 | */ 243 | private async handleLongType(initialSlot: string, resolverAddr: string, blocknr: number, length: number) { 244 | /** 245 | * At first we need do determine how many slots we need to proof. The initial slot just contains the length of the entire data structure. 246 | * We're using this information to calculate the number of slots we need to request. 247 | */ 248 | const totalSlots = Math.ceil((length * 2 + 1) / 64); 249 | 250 | /** 251 | * The first slot is the keccak256 hash of the initial slot. After that the slots are calculated by adding 1 to the previous slot. 252 | */ 253 | const firstSlot = keccak256(initialSlot); 254 | 255 | /** 256 | * Computing the addresses of every other slot 257 | */ 258 | const slots = [...Array(totalSlots).keys()].map(i => BigNumber.from(firstSlot).add(i).toHexString()); 259 | 260 | /* 261 | * After we know every slot that has to be proven we can call eth_getProof. 262 | */ 263 | const { accountProof, storageProof, storageHash } = await this.makeGetProofRpcCall( 264 | resolverAddr, 265 | slots, 266 | blocknr, 267 | ); 268 | 269 | return { 270 | accountProof, 271 | storageProof: this.rlpEncodeStroageProof(storageProof), 272 | storageHash, 273 | length, 274 | }; 275 | } 276 | /** 277 | * Retrieves the storage hash for the L2ToL1MessagePassercontract. This hash is part of every outputRoot posted by the L2OutputOracle. 278 | * To learn more about Bedrock commitments visit 279 | * @link {https://github.com/ethereum-optimism/optimism/blob/develop/specs/proposals.md#l2-output-commitment-construction} 280 | * @param blockNr The block number for which to fetch the storage hash. 281 | * @returns A promise that resolves to the storage hash. 282 | */ 283 | private async getMessagePasserStorageRoot(blockNr: number) { 284 | const { storageHash } = await this.makeGetProofRpcCall( 285 | this.crossChainMessenger.contracts.l2.BedrockMessagePasser.address, 286 | [], 287 | blockNr, 288 | ); 289 | 290 | return storageHash; 291 | } 292 | /** 293 | * Makes an RPC call to retrieve the proof for the specified resolver address, slots, and block number. 294 | * @param resolverAddr The resolver address for which to fetch the proof. 295 | * @param slots The slots for which to fetch the proof. 296 | * @param blocknr The block number for which to fetch the proof. 297 | * @returns A promise that resolves to the proof response. 298 | */ 299 | private async makeGetProofRpcCall( 300 | resolverAddr: string, 301 | slots: string[], 302 | blocknr: number, 303 | ): Promise { 304 | return this.l2Provider.send('eth_getProof', [resolverAddr, slots, toRpcHexString(blocknr)]); 305 | } 306 | /** 307 | * RLP encodes the storage proof 308 | * @param storageProofs The storage proofs to be mapped. 309 | * @returns An array of mapped storage proofs. 310 | */ 311 | private rlpEncodeStroageProof( 312 | storageProofs: EthGetProofResponse['storageProof'], 313 | ): (StorageProof & { value: string })[] { 314 | return storageProofs.map(({ key, proof, value }) => ({ 315 | key, 316 | value, 317 | // The contracts needs the merkle proof RLP encoded 318 | storageTrieWitness: ethers.utils.RLP.encode(proof), 319 | })); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /gateway/service/proof/toRpcHexString.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers'; 2 | 3 | /** 4 | * Converts a number or BigNumber to a hexadecimal string in RPC format. 5 | * Copied from the Optimism core-utils package to avoid including the entire package as a dependency. 6 | * @see https://github.com/ethereum-optimism/optimism/blob/7eda941967549aab449c21b2a2e4c10de792bd0b/packages/core-utils/src/common/hex-strings.ts#L65 7 | * @param n - The number or BigNumber to convert to a hexadecimal string. 8 | * @returns A hexadecimal string in RPC format. 9 | */ 10 | export const toRpcHexString = (n: number | BigNumber) => { 11 | let num; 12 | if (typeof n === 'number') { 13 | num = '0x' + n.toString(16); 14 | } else { 15 | num = n.toHexString(); 16 | } 17 | if (num === '0x0') { 18 | return num; 19 | } else { 20 | return num.replace(/^0x0/, '0x'); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /gateway/service/proof/types.ts: -------------------------------------------------------------------------------- 1 | import { StorageLayout } from './ProofService'; 2 | 3 | /** 4 | * Response of the eth_getProof RPC method. 5 | */ 6 | export interface EthGetProofResponse { 7 | accountProof: string; 8 | balance: string; 9 | codeHash: string; 10 | nonce: string; 11 | storageHash: string; 12 | storageProof: { 13 | key: string; 14 | value: string; 15 | proof: string[]; 16 | }[]; 17 | } 18 | /** 19 | * The ProofInputObject that will be passed to the BedrockProofVerifier contract 20 | */ 21 | export interface ProofInputObject { 22 | layout: StorageLayout; 23 | target: string; 24 | length: number; 25 | storageHash: string; 26 | stateTrieWitness: string; 27 | l2OutputIndex: number; 28 | outputRootProof: OutputRootProof; 29 | storageProofs: StorageProof[]; 30 | } 31 | export interface StorageProof { 32 | key: string; 33 | storageTrieWitness: string; 34 | } 35 | 36 | export interface CreateProofResult { 37 | proof: ProofInputObject; 38 | result: string; 39 | } 40 | 41 | type bytes32 = string; 42 | export interface OutputRootProof { 43 | version: bytes32; 44 | stateRoot: bytes32; 45 | messagePasserStorageRoot: bytes32; 46 | latestBlockhash: bytes32; 47 | } 48 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type import('hardhat/config').HardhatUserConfig 3 | */ 4 | 5 | import '@nomiclabs/hardhat-ethers'; 6 | import '@nomiclabs/hardhat-etherscan'; 7 | import '@nomiclabs/hardhat-solhint'; 8 | import '@typechain/hardhat'; 9 | import 'dotenv/config'; 10 | import { ethers } from 'ethers'; 11 | import 'hardhat-abi-exporter'; 12 | import 'hardhat-deploy'; 13 | import 'hardhat-storage-layout'; 14 | import 'hardhat-tracer'; 15 | import 'solidity-coverage'; 16 | 17 | const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; 18 | const OPTIMISTIC_ETHERSCAN_API_KEY = process.env.OPTIMISTIC_ETHERSCAN_API_KEY; 19 | 20 | const GOERLI_URL = process.env.GOERLI_RPC_URL ?? ''; 21 | const DEPLOYER_PRIVATE_KEY = process.env.DEPLOYER_PRIVATE_KEY ?? ethers.Wallet.createRandom().privateKey; 22 | 23 | const hardhat = 24 | process.env.CI || process.env.npm_lifecycle_event !== 'test:e2e' 25 | ? {} 26 | : { 27 | forking: { 28 | url: 'http://localhost:8545', 29 | }, 30 | }; 31 | 32 | module.exports = { 33 | defaultNetwork: 'hardhat', 34 | networks: { 35 | hardhat, 36 | optimismGoerli: { 37 | url: 'https://goerli.optimism.io', 38 | accounts: [DEPLOYER_PRIVATE_KEY], 39 | }, 40 | goerli: { 41 | url: GOERLI_URL, 42 | accounts: [DEPLOYER_PRIVATE_KEY], 43 | }, 44 | localhost: {}, 45 | }, 46 | etherscan: { 47 | apiKey: ETHERSCAN_API_KEY, 48 | }, 49 | namedAccounts: { 50 | deployer: { 51 | default: 0, // here this will by default take the first account as deployer 52 | }, 53 | }, 54 | solidity: { 55 | compilers: [ 56 | { 57 | version: '0.8.17', 58 | settings: { 59 | viaIR: true, 60 | optimizer: { 61 | enabled: true, 62 | details: { 63 | yulDetails: { 64 | optimizerSteps: 'u', 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | ], 71 | }, 72 | mocha: { 73 | timeout: 100000, 74 | }, 75 | typechain: { 76 | outDir: 'typechain', 77 | target: 'ethers-v5', 78 | }, 79 | abiExporter: { 80 | path: './build/contracts', 81 | runOnCompile: true, 82 | clear: true, 83 | flat: true, 84 | except: [], 85 | spacing: 2, 86 | pretty: true, 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const ProofServiceTestContract = require("./build/contracts/test-contract/ProofServiceTestContract"); 2 | 3 | const BedrockCcipVerifier = require("./build/contracts/verifier/optimism-bedrock/BedrockCcipVerifier"); 4 | const BedrockProofVerifier = require("./build/contracts/verifier/optimism-bedrock/BedrockProofVerifier"); 5 | const IBedrockProofVerifier = require("./build/contracts/verifier/optimism-bedrock/IBedrockProofVerifier"); 6 | 7 | const SignatureVerifier = require("./build/contracts/verifier/signature/SignatureVerifier"); 8 | const SignatureCcipVerifier = require("./build/contracts/verifier/signature/SignatureCcipVerifier"); 9 | 10 | const CcipResponseVerifier = require("./build/contracts/verifier/CcipResponseVerifier"); 11 | const ICcipResponseVerifier = require("./build/contracts/verifier/ICcipResponseVerifier"); 12 | 13 | const ERC3668Resolver = require("./build/contracts/ERC3668Resolver"); 14 | const IContextResolver = require("./build/contracts/IContextResolver"); 15 | const IExtendedResolver = require("./build/contracts/IExtendedResolver"); 16 | const Supportsinterface = require("./build/contracts/Supportsinterface"); 17 | 18 | module.exports = { 19 | ProofServiceTestContract, 20 | 21 | BedrockCcipVerifier, 22 | BedrockProofVerifier, 23 | IBedrockProofVerifier, 24 | 25 | SignatureVerifier, 26 | SignatureCcipVerifier, 27 | 28 | CcipResponseVerifier, 29 | ICcipResponseVerifier, 30 | 31 | ERC3668Resolver, 32 | IContextResolver, 33 | IExtendedResolver, 34 | Supportsinterface, 35 | }; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ccip-resolver", 3 | "version": "0.2.8", 4 | "description": "", 5 | "types": "dist/index.d.ts", 6 | "scripts": { 7 | "compile": "hardhat compile", 8 | "test:contracts": "hardhat test ./test/contracts/**", 9 | "test:gateway": "hardhat test ./test/gateway/**", 10 | "test:e2e-setup": "npx hardhat run ./scripts/setupEnvironment.ts", 11 | "test:e2e": "hardhat test ./test/e2e/** ", 12 | "test": "yarn run test:contracts && yarn run test:gateway ", 13 | "lint:fix": "prettier --write 'scripts/**/*.{js,ts}' 'test/**/*.{js,ts}' 'deploy/**/*.{js,ts}' && tslint --fix --config tslint.json --project tsconfig.json && solhint contracts/**/*.sol", 14 | "lint": "tslint --config tslint.json --project tsconfig.json && solhint contracts/**/*.sol", 15 | "format": "prettier --check \"**/*.{ts,js,sol}\"", 16 | "format:fix": "prettier --write \"**/*.{ts,js,sol}\"", 17 | "deploy:bedrock-proof-verifier-goerli": "npx hardhat run ./deploy/01_BedrockProofVerifier.ts --network goerli", 18 | "deploy:bedrock-proof-verifier-mainnet": "npx hardhat run ./deploy/01_BedrockProofVerifier.ts --network mainnet", 19 | "deploy:ccip-resolver-goerli": "npx hardhat run ./deploy/02_ERC3668Resolver.ts --network goerli", 20 | "deploy:ccip-resolver-mainnet": "npx hardhat run ./deploy/02_ERC3668Resolver.ts --network mainnet", 21 | "docker:build": "docker build -t ccip-resolver -f ./Dockerfile .", 22 | "build": "yarn hardhat compile && yarn tsc -p tsconfig-build.json", 23 | "start": "node dist/gateway/index.js" 24 | }, 25 | "files": [ 26 | "dist", 27 | "LICENSE", 28 | "README.md", 29 | "build", 30 | "contracts/**/*.sol", 31 | "abi", 32 | "artifacts", 33 | "typechain" 34 | ], 35 | "author": "Alex Plutta alex.plutta@googlemail.com", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.0", 39 | "@nomicfoundation/hardhat-verify": "^1.0.3", 40 | "@nomiclabs/hardhat-ethers": "^2.0.3", 41 | "@nomiclabs/hardhat-etherscan": "^2.1.8", 42 | "@nomiclabs/hardhat-solhint": "^2.0.0", 43 | "@nomiclabs/hardhat-web3": "^2.0.0", 44 | "@openzeppelin/contracts": "^4.4.0", 45 | "@pinata/sdk": "^1.1.23", 46 | "@typechain/ethers-v5": "^8.0.5", 47 | "@typechain/hardhat": "^3.0.0", 48 | "@types/body-parser": "^1.19.2", 49 | "@types/chai": "^4.3.0", 50 | "@types/cors": "^2.8.13", 51 | "@types/express": "^4.17.17", 52 | "@types/mocha": "^9.0.0", 53 | "@types/node": "^16.11.12", 54 | "axios-mock-adapter": "^1.21.5", 55 | "babel-eslint": "^10.1.0", 56 | "chai": "^4.3.4", 57 | "chai-bignumber": "^3.0.0", 58 | "chai-bn": "^0.3.0", 59 | "chai-ethers": "^0.0.1", 60 | "dns-packet": "^5.6.0", 61 | "dotenv": "^10.0.0", 62 | "ethers": "^5.5.2", 63 | "hardhat": "^2.7.1", 64 | "hardhat-abi-exporter": "^2.10.1", 65 | "hardhat-deploy": "^0.9.14", 66 | "hardhat-storage-layout": "^0.1.7", 67 | "hardhat-tracer": "^1.2.1", 68 | "merkle-patricia-tree": "^4.2.4", 69 | "mocha-skip-if": "^0.0.3", 70 | "prettier": "^2.5.1", 71 | "prettier-plugin-solidity": "^1.0.0-beta.19", 72 | "random-bytes-seed": "^1.0.3", 73 | "rlp": "^3.0.0", 74 | "solhint": "^3.3.6", 75 | "solhint-plugin-prettier": "^0.0.5", 76 | "solidity-coverage": "^0.7.17", 77 | "supertest": "^6.3.3", 78 | "transform": "^1.1.2", 79 | "ts-generator": "^0.1.1", 80 | "ts-node": "^10.4.0", 81 | "tslint": "^6.1.3", 82 | "tslint-config-prettier": "^1.18.0", 83 | "tslint-plugin-prettier": "^2.3.0", 84 | "typechain": "^6.0.5", 85 | "typescript": "^4.5.3", 86 | "web3": "^1.6.1" 87 | }, 88 | "mocha": { 89 | "timeout": 10000000 90 | }, 91 | "dependencies": { 92 | "@defi-wonderland/smock": "^2.3.4", 93 | "@ensdomains/buffer": "0.0.10", 94 | "@ensdomains/dnssec-oracle": "0.1.2", 95 | "@ensdomains/ens-contracts": "^0.0.19", 96 | "@eth-optimism/contracts": "^0.6.0", 97 | "@eth-optimism/sdk": "^2.1.0", 98 | "axios": "^1.4.0", 99 | "body-parser": "^1.20.2", 100 | "express": "^4.18.2", 101 | "solidity-bytes-utils": "^0.8.0", 102 | "winston": "^3.9.0" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /scripts/setupEnvironment.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { ProofServiceTestContract, ProofServiceTestContract__factory } from 'typechain'; 3 | /* 4 | * This script is used to setup the environment for the e2e tests. 5 | * It asumes that you have set up the local development environment for OP bedrock 6 | * https://community.optimism.io/docs/developers/build/dev-node/ 7 | */ 8 | 9 | // 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 10 | const whale = new ethers.Wallet('ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'); 11 | 12 | // 0x8111DfD23B99233a7ae871b7c09cCF0722847d89 13 | const alice = new ethers.Wallet('0xfd9f3842a10eb01ccf3109d4bd1c4b165721bf8c26db5db7570c146f9fad6014'); 14 | 15 | const l1Provider = new ethers.providers.StaticJsonRpcProvider('http://localhost:8545'); 16 | const l2Provider = new ethers.providers.StaticJsonRpcProvider('http://localhost:9545'); 17 | async function main() { 18 | // Test proof logic 19 | let proofServiceTestContract: ProofServiceTestContract; 20 | let foreignProofServiceTestContract: ProofServiceTestContract; 21 | 22 | const l2Whale = whale.connect(l2Provider); 23 | 24 | // Verifiy that the local development environment is set up correctly 25 | if ((await l1Provider.getNetwork()).chainId !== 900 || (await l2Provider.getNetwork()).chainId !== 901) { 26 | console.error("Please ensure that you're running the local development environment for OP bedrock"); 27 | return; 28 | } 29 | const proofServiceTestContractFactory = (await ethers.getContractFactory( 30 | 'ProofServiceTestContract', 31 | )) as ProofServiceTestContract__factory; 32 | 33 | proofServiceTestContract = await proofServiceTestContractFactory.connect(l2Whale).deploy(); 34 | foreignProofServiceTestContract = await proofServiceTestContractFactory.connect(l2Whale).deploy(); 35 | 36 | console.log(`ProofServiceTestContract deployed at ${proofServiceTestContract.address}`); 37 | console.log(`ForeignProofServiceTestContract deployed at ${foreignProofServiceTestContract.address}`); 38 | 39 | const prepateProofServiceTestContractBytes32 = async () => { 40 | await proofServiceTestContract.setBool(true); 41 | await proofServiceTestContract.setAddress(alice.address); 42 | await proofServiceTestContract.setUint256(123); 43 | await proofServiceTestContract.setBytes32(ethers.utils.namehash('alice.eth')); 44 | await proofServiceTestContract.setBytes(ethers.utils.namehash('alice.eth')); 45 | await proofServiceTestContract.setString('Hello from Alice'); 46 | // prettier-ignore 47 | await proofServiceTestContract.setLongString( 48 | "Praesent elementum ligula et dolor varius lobortis. Morbi eget eleifend augue.Nunc quis lectus in augue feugiat malesuada vitae sed lacus. Aenean suscipit tristique mauris a blandit. Maecenas ut lectus quis metus commodo tincidunt. Aliquam erat volutpat. Fusce non erat malesuada, consequat mauris id, tincidunt nisi. Aenean a pulvinar ex. Mauris ullamcorper eget odio nec eleifend. Donec pellentesque et tellus id consectetur. Nullam dictum, felis sit amet consectetur convallis, leo lorem rhoncus nulla, eget tincidunt leo erat sit amet urna. Quisque urna turpis, lobortis laoreet nunc at, fermentum ultrices neque.Proin dignissim enim non arcu elementum tempor.Donec convallis turpis at erat luctus, eget pellentesque augue blandit.In faucibus rhoncus mollis.Phasellus malesuada, mauris ut finibus venenatis, ex risus consectetur dolor, quis suscipit augue magna iaculis leo.Proin non nibh at justo porttitor sollicitudin.Pellentesque id malesuada tellus.Aliquam condimentum accumsan ex eu vulputate.Aenean faucibus a quam vitae tincidunt.Fusce vitae mollis nunc, at volutpat ex.Vestibulum sed tellus urna.Donec ac urna lectus.Phasellus a dolor elit.Aenean bibendum hendrerit elit, in cursus sem maximus id.Sed porttitor nulla non consectetur vehicula.Fusce elementum, urna in gravida accumsan, lectus arcu congue augue, at rhoncus purus nunc ac libero." 49 | ); 50 | }; 51 | await prepateProofServiceTestContractBytes32(); 52 | console.log('Environment setup complete wait a few minutes until everything is set'); 53 | } 54 | 55 | // We recommend this pattern to be able to use async/await everywhere 56 | // and properly handle errors. 57 | main().catch(error => { 58 | console.error(error); 59 | process.exitCode = 1; 60 | }); 61 | -------------------------------------------------------------------------------- /test/chai-setup.ts: -------------------------------------------------------------------------------- 1 | import chaiModule from 'chai'; 2 | import { chaiEthers } from 'chai-ethers'; 3 | chaiModule.use(chaiEthers); 4 | export = chaiModule; 5 | -------------------------------------------------------------------------------- /test/contracts/BedrockCcipVerifier.test.ts: -------------------------------------------------------------------------------- 1 | import { FakeContract, smock } from '@defi-wonderland/smock'; 2 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 3 | import { BigNumber, ethers } from 'ethers'; 4 | import { dnsEncode } from 'ethers/lib/utils'; 5 | import { ethers as hreEthers } from 'hardhat'; 6 | 7 | import { expect } from '../../test/chai-setup'; 8 | import { BedrockCcipVerifier__factory, BedrockProofVerifier } from '../../typechain'; 9 | 10 | describe('Bedrock CcipVerifier', () => { 11 | let owner: SignerWithAddress; 12 | let signer1: SignerWithAddress; 13 | let signer2: SignerWithAddress; 14 | let rando: SignerWithAddress; 15 | let alice: SignerWithAddress; 16 | let resolver: SignerWithAddress; 17 | 18 | let bedrockProofVerifier: FakeContract; 19 | 20 | beforeEach(async () => { 21 | // Get signers 22 | [owner, signer1, signer2, rando, alice, resolver] = await hreEthers.getSigners(); 23 | 24 | bedrockProofVerifier = (await smock.fake('BedrockProofVerifier')) as FakeContract; 25 | }); 26 | describe('Constructor', () => { 27 | it('Initially set the owner,url and signers using the constructor ', async () => { 28 | const bedrockCcipVerifier = await new BedrockCcipVerifier__factory() 29 | .connect(owner) 30 | .deploy( 31 | owner.address, 32 | 'http://localhost:8080/graphql', 33 | 'Optimism Resolver', 34 | 420, 35 | bedrockProofVerifier.address, 36 | resolver.address, 37 | ); 38 | 39 | const actualOwner = await bedrockCcipVerifier.owner(); 40 | const actualGraphQlUrl = await bedrockCcipVerifier.graphqlUrl(); 41 | const actualTaget = await bedrockCcipVerifier.target(); 42 | const actualBedrockProofVerifier = await bedrockCcipVerifier.bedrockProofVerifier(); 43 | 44 | expect(actualOwner).to.equal(owner.address); 45 | expect(actualGraphQlUrl).to.equal('http://localhost:8080/graphql'); 46 | expect(actualTaget).to.equal(resolver.address); 47 | expect(actualBedrockProofVerifier).to.equal(bedrockProofVerifier.address); 48 | }); 49 | }); 50 | describe('setOwner', () => { 51 | it('Owner can set a new Owner ', async () => { 52 | const bedrockCcipVerifier = await new BedrockCcipVerifier__factory() 53 | .connect(owner) 54 | .deploy( 55 | owner.address, 56 | 'http://localhost:8080/graphql', 57 | 'Optimism Resolver', 58 | 420, 59 | bedrockProofVerifier.address, 60 | resolver.address, 61 | ); 62 | 63 | const actualOwner = await bedrockCcipVerifier.owner(); 64 | expect(actualOwner).to.equal(owner.address); 65 | 66 | const tx = await bedrockCcipVerifier.setOwner(signer1.address); 67 | const receipt = await tx.wait(); 68 | 69 | const [NewOwnerEvent] = receipt.events; 70 | 71 | const [newOwner] = NewOwnerEvent.args; 72 | 73 | expect(await bedrockCcipVerifier.owner()).to.equal(signer1.address); 74 | expect(newOwner).to.equal(signer1.address); 75 | }); 76 | it("Rando can't set a new owner ", async () => { 77 | const bedrockCcipVerifier = await new BedrockCcipVerifier__factory() 78 | .connect(owner) 79 | .deploy( 80 | owner.address, 81 | 'http://localhost:8080/graphql', 82 | 'Optimism Resolver', 83 | 420, 84 | bedrockProofVerifier.address, 85 | resolver.address, 86 | ); 87 | 88 | const actualOwner = await bedrockCcipVerifier.owner(); 89 | expect(actualOwner).to.equal(owner.address); 90 | 91 | try { 92 | await bedrockCcipVerifier.connect(rando).setOwner(signer1.address, { gasLimit: 1000000 }); 93 | expect.fail('should have reverted'); 94 | } catch (e) { 95 | expect(e.toString()).to.include('only owner'); 96 | } 97 | }); 98 | }); 99 | describe('set GrapgQlUrl', () => { 100 | it('Owner can set a new Url ', async () => { 101 | const bedrockCcipVerifier = await new BedrockCcipVerifier__factory() 102 | .connect(owner) 103 | .deploy( 104 | owner.address, 105 | 'http://localhost:8080/graphql', 106 | 'Optimism Resolver', 107 | 420, 108 | bedrockProofVerifier.address, 109 | resolver.address, 110 | ); 111 | 112 | const actualUrl = await bedrockCcipVerifier.graphqlUrl(); 113 | expect(actualUrl).to.equal('http://localhost:8080/graphql'); 114 | 115 | const tx = await bedrockCcipVerifier.setGraphUrl('http://foo.io/graphql'); 116 | const receipt = await tx.wait(); 117 | 118 | const [GraphQlUrlChanged] = receipt.events; 119 | 120 | const [newUrl] = GraphQlUrlChanged.args; 121 | 122 | expect(await bedrockCcipVerifier.graphqlUrl()).to.equal('http://foo.io/graphql'); 123 | expect(newUrl).to.equal('http://foo.io/graphql'); 124 | }); 125 | it("Rando can't set a new owner ", async () => { 126 | const bedrockCcipVerifier = await new BedrockCcipVerifier__factory() 127 | .connect(owner) 128 | .deploy( 129 | owner.address, 130 | 'http://localhost:8080/graphql', 131 | 'Optimism Resolver', 132 | 420, 133 | bedrockProofVerifier.address, 134 | resolver.address, 135 | ); 136 | 137 | const actualUrl = await bedrockCcipVerifier.graphqlUrl(); 138 | expect(actualUrl).to.equal('http://localhost:8080/graphql'); 139 | 140 | try { 141 | await bedrockCcipVerifier.connect(rando).setGraphUrl('http://foo.io/graphql'); 142 | expect.fail('should have reverted'); 143 | } catch (e) { 144 | expect(e.toString()).to.include('only owner'); 145 | } 146 | }); 147 | }); 148 | describe('Metadata', () => { 149 | it('returns metadata', async () => { 150 | const convertCoinTypeToEVMChainId = (_coinType: number) => { 151 | return (0x7fffffff & _coinType) >> 0; 152 | }; 153 | const bedrockCcipVerifier = await new BedrockCcipVerifier__factory() 154 | .connect(owner) 155 | .deploy( 156 | owner.address, 157 | 'http://localhost:8080/graphql', 158 | 'Optimism Resolver', 159 | 420, 160 | bedrockProofVerifier.address, 161 | resolver.address, 162 | ); 163 | 164 | const [name, coinType, graphqlUrl, storageType, storageLocation, context] = 165 | await bedrockCcipVerifier.metadata(dnsEncode('alice.eth')); 166 | expect(name).to.equal('Optimism Resolver'); 167 | expect(convertCoinTypeToEVMChainId(BigNumber.from(coinType).toNumber())).to.equal(420); 168 | expect(graphqlUrl).to.equal('http://localhost:8080/graphql'); 169 | expect(storageType).to.equal(storageType); 170 | expect(ethers.utils.getAddress(storageLocation)).to.equal(resolver.address); 171 | expect(ethers.utils.getAddress(context)).to.equal(ethers.constants.AddressZero); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/contracts/ERC3668Resolver.test.ts: -------------------------------------------------------------------------------- 1 | import { FakeContract, smock } from '@defi-wonderland/smock'; 2 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 3 | import { expect } from 'chai'; 4 | import { BigNumber, ethers } from 'ethers'; 5 | import { dnsEncode } from 'ethers/lib/utils'; 6 | import { ethers as hreEthers } from 'hardhat'; 7 | import { 8 | BedrockCcipVerifier, 9 | BedrockCcipVerifier__factory, 10 | BedrockProofVerifier, 11 | BedrockProofVerifier__factory, 12 | CcipResponseVerifier, 13 | ENS, 14 | ERC3668Resolver, 15 | INameWrapper, 16 | SignatureCcipVerifier, 17 | SignatureCcipVerifier__factory, 18 | } from 'typechain'; 19 | 20 | import { signAndEncodeResponse } from '../../gateway/handler/signing/signAndEncodeResponse'; 21 | describe('ERC3668Resolver Test', () => { 22 | let owner: SignerWithAddress; 23 | // Example user alice 24 | let alice: SignerWithAddress; 25 | // Singer for signing responses 26 | let signer: ethers.Wallet; 27 | 28 | // ENS 29 | let ensRegistry: FakeContract; 30 | // NameWrapper 31 | let nameWrapper: FakeContract; 32 | // Resolver 33 | 34 | let erc3668Resolver: ERC3668Resolver; 35 | // Bedrock Proof Verifier 36 | let bedrockProofVerifier: BedrockProofVerifier; 37 | // Bedrock CCIP resolver 38 | let bedrockCcipVerifier: BedrockCcipVerifier; 39 | 40 | let signatureVerifier: SignatureCcipVerifier; 41 | // Dummy contract to test if the reverts when callback selector is not supported 42 | let verifierWithoutCallbackSelector: FakeContract; 43 | 44 | beforeEach(async () => { 45 | [owner, alice] = await hreEthers.getSigners(); 46 | signer = ethers.Wallet.createRandom(); 47 | /** 48 | * MOCK ENS Registry 49 | */ 50 | ensRegistry = (await smock.fake( 51 | '@ensdomains/ens-contracts/contracts/registry/ENS.sol:ENS', 52 | )) as FakeContract; 53 | ensRegistry.owner.whenCalledWith(ethers.utils.namehash('alice.eth')).returns(alice.address); 54 | /** 55 | * MOCK NameWrapper 56 | */ 57 | nameWrapper = (await smock.fake( 58 | '@ensdomains/ens-contracts/contracts/wrapper/INameWrapper.sol:INameWrapper', 59 | )) as FakeContract; 60 | ensRegistry.owner.whenCalledWith(ethers.utils.namehash('namewrapper.alice.eth')).returns(nameWrapper.address); 61 | nameWrapper.ownerOf.whenCalledWith(ethers.utils.namehash('namewrapper.alice.eth')).returns(alice.address); 62 | 63 | const BedrockProofVerifierFactory = (await hreEthers.getContractFactory( 64 | 'BedrockProofVerifier', 65 | )) as BedrockProofVerifier__factory; 66 | bedrockProofVerifier = await BedrockProofVerifierFactory.deploy('0x6900000000000000000000000000000000000000'); 67 | 68 | const BedrockCcipVerifierFactory = (await hreEthers.getContractFactory( 69 | 'BedrockCcipVerifier', 70 | )) as BedrockCcipVerifier__factory; 71 | 72 | bedrockCcipVerifier = await BedrockCcipVerifierFactory.deploy( 73 | owner.address, 74 | 'http://localhost:8081/graphql', 75 | 'Optimism Goerli', 76 | 420, 77 | bedrockProofVerifier.address, 78 | '0x5FbDB2315678afecb367f032d93F642f64180aa3', 79 | ); 80 | 81 | verifierWithoutCallbackSelector = (await smock.fake( 82 | 'CcipResponseVerifier', 83 | )) as FakeContract; 84 | // Supports CCIPVerifierInterface 85 | verifierWithoutCallbackSelector.supportsInterface.whenCalledWith('0x79f6f27a').returns(true); 86 | 87 | const OptimismResolverFactory = await hreEthers.getContractFactory('ERC3668Resolver'); 88 | erc3668Resolver = (await OptimismResolverFactory.deploy( 89 | ensRegistry.address, 90 | nameWrapper.address, 91 | bedrockCcipVerifier.address, 92 | ['http://localhost:8080/{sender}/{data}'], 93 | { 94 | gasLimit: 10000000, 95 | }, 96 | )) as ERC3668Resolver; 97 | 98 | const SignatureCcipVerifierFactory = (await hreEthers.getContractFactory( 99 | 'SignatureCcipVerifier', 100 | )) as SignatureCcipVerifier__factory; 101 | 102 | signatureVerifier = await SignatureCcipVerifierFactory.deploy( 103 | owner.address, 104 | 'http://localhost:8081/graphql', 105 | 'Signature Ccip Resolver', 106 | 420, 107 | erc3668Resolver.address, 108 | [signer.address], 109 | ); 110 | }); 111 | 112 | describe('setVerifierForDomain', () => { 113 | it('reverts if resolverAddress is 0x0', async () => { 114 | await erc3668Resolver 115 | .connect(alice) 116 | .setVerifierForDomain(ethers.utils.namehash('alice.eth'), ethers.constants.AddressZero, [ 117 | 'http://localhost:8080/{sender}/{data}', 118 | ]) 119 | .then(res => { 120 | expect.fail('Should have thrown an error'); 121 | }) 122 | .catch(e => { 123 | expect(e.message).to.contains('verifierAddress is 0x0'); 124 | }); 125 | }); 126 | it('reverts if msg.sender is not the profile owner', async () => { 127 | await erc3668Resolver 128 | .setVerifierForDomain(ethers.utils.namehash('vitalik.eth'), bedrockCcipVerifier.address, [ 129 | 'http://localhost:8080/{sender}/{data}', 130 | ]) 131 | .then(res => { 132 | expect.fail('Should have thrown an error'); 133 | }) 134 | .catch(e => { 135 | expect(e.message).to.contains('only node owner'); 136 | }); 137 | }); 138 | 139 | it('reverts if resolverAddress does not support resolveWithProofInterface', async () => { 140 | await erc3668Resolver 141 | .connect(alice) 142 | .setVerifierForDomain( 143 | ethers.utils.namehash('alice.eth'), 144 | // Alice is an EOA, so this is not a valid resolver 145 | bedrockProofVerifier.address, 146 | ['http://localhost:8080/{sender}/{data}'], 147 | ) 148 | .then(res => { 149 | expect.fail('Should have thrown an error'); 150 | }) 151 | .catch(e => { 152 | expect(e.message).to.contains('verifierAddress is not a CCIP Verifier'); 153 | }); 154 | }); 155 | it('reverts if url string is empty', async () => { 156 | await erc3668Resolver 157 | .connect(alice) 158 | .setVerifierForDomain( 159 | ethers.utils.namehash('alice.eth'), 160 | // Alice is an EOA, so this is not a valid resolver 161 | bedrockCcipVerifier.address, 162 | [], 163 | ) 164 | .then(res => { 165 | expect.fail('Should have thrown an error'); 166 | }) 167 | .catch(e => { 168 | expect(e.message).to.contains('at least one gateway url has to be provided'); 169 | }); 170 | }); 171 | it('adds verifier + event contains node, url, and resolverAddress', async () => { 172 | const tx = await erc3668Resolver.connect(alice).setVerifierForDomain( 173 | ethers.utils.namehash('alice.eth'), 174 | // Alice is an EOA, so this is not a valid resolver 175 | bedrockCcipVerifier.address, 176 | ['http://localhost:8080/{sender}/{data}'], 177 | ); 178 | 179 | const receipt = await tx.wait(); 180 | 181 | const [ResolverAddedEvent] = receipt.events; 182 | 183 | const [node, resolverAddress, gatewayUrls] = ResolverAddedEvent.args; 184 | 185 | expect(node).to.equal(ethers.utils.namehash('alice.eth')); 186 | expect(gatewayUrls).to.eql(['http://localhost:8080/{sender}/{data}']); 187 | expect(resolverAddress).to.equal(bedrockCcipVerifier.address); 188 | }); 189 | it('adds verifier + event contains node, url, and resolverAddress for NameWrapperProfile', async () => { 190 | const tx = await erc3668Resolver.connect(alice).setVerifierForDomain( 191 | ethers.utils.namehash('namewrapper.alice.eth'), 192 | // Alice is an EOA, so this is not a valid resolver 193 | bedrockCcipVerifier.address, 194 | ['http://localhost:8080/{sender}/{data}'], 195 | ); 196 | 197 | const receipt = await tx.wait(); 198 | 199 | const [ResolverAddedEvent] = receipt.events; 200 | 201 | const [node, resolverAddress, gatewayUrls] = ResolverAddedEvent.args; 202 | 203 | expect(node).to.equal(ethers.utils.namehash('namewrapper.alice.eth')); 204 | expect(gatewayUrls).to.eql(['http://localhost:8080/{sender}/{data}']); 205 | expect(resolverAddress).to.equal(bedrockCcipVerifier.address); 206 | }); 207 | }); 208 | describe('resolve', () => { 209 | it('returns offchain lookup via default verifier for unknown TLD ', async () => { 210 | const iface = new ethers.utils.Interface([ 211 | 'function onResolveWithProof(bytes calldata name, bytes calldata data) public pure returns (bytes4)', 212 | 'function addr(bytes32 node) external view returns (address)', 213 | 'error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)', 214 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 215 | 216 | 'function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory)', 217 | ]); 218 | 219 | const name = ethers.utils.dnsEncode('foo.bar'); 220 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('foo.bar')]); 221 | 222 | let errorString; 223 | try { 224 | await erc3668Resolver.resolve(name, data); 225 | } catch (e) { 226 | errorString = e.data; 227 | } 228 | 229 | const decodedError = iface.decodeErrorResult('OffchainLookup', errorString); 230 | const [sender, urls, callData, callbackFunction, extraData] = decodedError; 231 | 232 | expect(sender).to.equal(erc3668Resolver.address); 233 | expect(urls).to.eql(['http://localhost:8080/{sender}/{data}']); 234 | expect(callData).to.equal( 235 | iface.encodeFunctionData('resolveWithContext', [name, data, ethers.constants.AddressZero]), 236 | ); 237 | expect(callbackFunction).to.equal(iface.getSighash('resolveWithProof')); 238 | expect(extraData).to.equal( 239 | iface.encodeFunctionData('resolveWithContext', [name, data, ethers.constants.AddressZero]), 240 | ); 241 | }); 242 | it('returns Offchain lookup for parent domain', async () => { 243 | await erc3668Resolver.connect(alice).setVerifierForDomain( 244 | ethers.utils.namehash('alice.eth'), 245 | // Alice is an EOA, so this is not a valid resolver 246 | bedrockCcipVerifier.address, 247 | ['http://localhost:8080/{sender}/{data}'], 248 | ); 249 | 250 | const iface = new ethers.utils.Interface([ 251 | 'function onResolveWithProof(bytes calldata name, bytes calldata data) public pure returns (bytes4)', 252 | 'function addr(bytes32 node) external view returns (address)', 253 | 'error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)', 254 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 255 | 256 | 'function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory)', 257 | ]); 258 | 259 | const name = ethers.utils.dnsEncode('alice.eth'); 260 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('alice.eth')]); 261 | 262 | let errorString; 263 | try { 264 | await erc3668Resolver.resolve(name, data); 265 | } catch (e) { 266 | errorString = e.data; 267 | } 268 | 269 | const decodedError = iface.decodeErrorResult('OffchainLookup', errorString); 270 | const [sender, urls, callData, callbackFunction, extraData] = decodedError; 271 | 272 | expect(sender).to.equal(erc3668Resolver.address); 273 | expect(urls).to.eql(['http://localhost:8080/{sender}/{data}']); 274 | expect(callData).to.equal(iface.encodeFunctionData('resolveWithContext', [name, data, alice.address])); 275 | expect(callbackFunction).to.equal(iface.getSighash('resolveWithProof')); 276 | expect(extraData).to.equal(iface.encodeFunctionData('resolveWithContext', [name, data, alice.address])); 277 | }); 278 | it('returns Offchain via defautl verifier for domain without custom verifier', async () => { 279 | const iface = new ethers.utils.Interface([ 280 | 'function onResolveWithProof(bytes calldata name, bytes calldata data) public pure returns (bytes4)', 281 | 'function addr(bytes32 node) external view returns (address)', 282 | 'error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)', 283 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 284 | 285 | 'function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory)', 286 | ]); 287 | 288 | const name = ethers.utils.dnsEncode('alice.eth'); 289 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('alice.eth')]); 290 | 291 | let errorString; 292 | try { 293 | await erc3668Resolver.resolve(name, data); 294 | } catch (e) { 295 | errorString = e.data; 296 | } 297 | 298 | const decodedError = iface.decodeErrorResult('OffchainLookup', errorString); 299 | const [sender, urls, callData, callbackFunction, extraData] = decodedError; 300 | 301 | expect(sender).to.equal(erc3668Resolver.address); 302 | expect(urls).to.eql(['http://localhost:8080/{sender}/{data}']); 303 | expect(callData).to.equal(iface.encodeFunctionData('resolveWithContext', [name, data, alice.address])); 304 | expect(callbackFunction).to.equal(iface.getSighash('resolveWithProof')); 305 | expect(extraData).to.equal(iface.encodeFunctionData('resolveWithContext', [name, data, alice.address])); 306 | }); 307 | it('returns Offchain lookup for sub domain', async () => { 308 | await erc3668Resolver.connect(alice).setVerifierForDomain( 309 | ethers.utils.namehash('alice.eth'), 310 | // Alice is an EOA, so this is not a valid resolver 311 | bedrockCcipVerifier.address, 312 | ['http://localhost:8080/{sender}/{data}'], 313 | ); 314 | 315 | const iface = new ethers.utils.Interface([ 316 | 'function onResolveWithProof(bytes calldata name, bytes calldata data) public pure returns (bytes4)', 317 | 'function addr(bytes32 node) external view returns (address)', 318 | 'error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)', 319 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 320 | 321 | 'function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory)', 322 | ]); 323 | 324 | const name = ethers.utils.dnsEncode('sub.alice.eth'); 325 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('alice.eth')]); 326 | 327 | let errorString; 328 | try { 329 | await erc3668Resolver.resolve(name, data); 330 | } catch (e) { 331 | errorString = e.data; 332 | } 333 | 334 | const decodedError = iface.decodeErrorResult('OffchainLookup', errorString); 335 | const [sender, urls, callData, callbackFunction, extraData] = decodedError; 336 | 337 | expect(sender).to.equal(erc3668Resolver.address); 338 | expect(urls).to.eql(['http://localhost:8080/{sender}/{data}']); 339 | expect(callData).to.equal(iface.encodeFunctionData('resolveWithContext', [name, data, alice.address])); 340 | expect(callbackFunction).to.equal(iface.getSighash('resolveWithProof')); 341 | expect(extraData).to.equal(iface.encodeFunctionData('resolveWithContext', [name, data, alice.address])); 342 | }); 343 | it('returns Offchain lookup for sub domain for default verifier', async () => { 344 | const iface = new ethers.utils.Interface([ 345 | 'function onResolveWithProof(bytes calldata name, bytes calldata data) public pure returns (bytes4)', 346 | 'function addr(bytes32 node) external view returns (address)', 347 | 'error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)', 348 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 349 | 350 | 'function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory)', 351 | ]); 352 | 353 | const name = ethers.utils.dnsEncode('sub.alice.eth'); 354 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('alice.eth')]); 355 | 356 | let errorString; 357 | try { 358 | await erc3668Resolver.resolve(name, data); 359 | } catch (e) { 360 | errorString = e.data; 361 | } 362 | 363 | const decodedError = iface.decodeErrorResult('OffchainLookup', errorString); 364 | const [sender, urls, callData, callbackFunction, extraData] = decodedError; 365 | 366 | expect(sender).to.equal(erc3668Resolver.address); 367 | expect(urls).to.eql(['http://localhost:8080/{sender}/{data}']); 368 | expect(callData).to.equal(iface.encodeFunctionData('resolveWithContext', [name, data, alice.address])); 369 | expect(callbackFunction).to.equal(iface.getSighash('resolveWithProof')); 370 | expect(extraData).to.equal(iface.encodeFunctionData('resolveWithContext', [name, data, alice.address])); 371 | }); 372 | it('returns Offchain lookup for namewrapper', async () => { 373 | await erc3668Resolver.connect(alice).setVerifierForDomain( 374 | ethers.utils.namehash('namewrapper.alice.eth'), 375 | // Alice is an EOA, so this is not a valid resolver 376 | bedrockCcipVerifier.address, 377 | ['http://localhost:8080/{sender}/{data}'], 378 | ); 379 | 380 | const iface = new ethers.utils.Interface([ 381 | 'function onResolveWithProof(bytes calldata name, bytes calldata data) public pure returns (bytes4)', 382 | 'function addr(bytes32 node) external view returns (address)', 383 | 'error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)', 384 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 385 | 386 | 'function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory)', 387 | ]); 388 | 389 | const name = ethers.utils.dnsEncode('namewrapper.alice.eth'); 390 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('alice.eth')]); 391 | 392 | let errorString; 393 | try { 394 | await erc3668Resolver.resolve(name, data); 395 | } catch (e) { 396 | errorString = e.data; 397 | } 398 | 399 | const decodedError = iface.decodeErrorResult('OffchainLookup', errorString); 400 | const [sender, urls, callData, callbackFunction, extraData] = decodedError; 401 | 402 | expect(sender).to.equal(erc3668Resolver.address); 403 | expect(urls).to.eql(['http://localhost:8080/{sender}/{data}']); 404 | expect(callData).to.equal(iface.encodeFunctionData('resolveWithContext', [name, data, alice.address])); 405 | expect(callbackFunction).to.equal(iface.getSighash('resolveWithProof')); 406 | expect(extraData).to.equal(iface.encodeFunctionData('resolveWithContext', [name, data, alice.address])); 407 | }); 408 | }); 409 | describe('resolveWithProof', () => { 410 | it('Revert if ccip verifier returns no callback selector', async () => { 411 | await erc3668Resolver 412 | .connect(alice) 413 | .setVerifierForDomain(ethers.utils.namehash('alice.eth'), verifierWithoutCallbackSelector.address, [ 414 | 'http://localhost:8080/{sender}/{data}', 415 | ]); 416 | 417 | const iface = new ethers.utils.Interface([ 418 | 'function addr(bytes32)', 419 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 420 | ]); 421 | 422 | const name = ethers.utils.dnsEncode('alice.eth'); 423 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('alice.eth')]); 424 | const extraData = iface.encodeFunctionData('resolveWithContext', [name, data, alice.address]); 425 | const response = ethers.utils.defaultAbiCoder.encode(['address'], [alice.address]); 426 | 427 | let errorString; 428 | 429 | try { 430 | await erc3668Resolver.resolveWithProof(response, extraData); 431 | } catch (e) { 432 | errorString = e.errorArgs[0]; 433 | } 434 | 435 | expect(errorString).to.equal('No callback selector found'); 436 | }); 437 | it('Revert if resolveWithProofCall fails', async () => { 438 | await erc3668Resolver.connect(alice).setVerifierForDomain( 439 | ethers.utils.namehash('alice.eth'), 440 | // Alice is an EOA, so this is not a valid resolver 441 | bedrockCcipVerifier.address, 442 | ['http://localhost:8080/{sender}/{data}'], 443 | ); 444 | 445 | const iface = new ethers.utils.Interface([ 446 | 'function addr(bytes32)', 447 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 448 | ]); 449 | 450 | const name = ethers.utils.dnsEncode('alice.eth'); 451 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('alice.eth')]); 452 | const extraData = iface.encodeFunctionData('resolveWithContext', [name, data, alice.address]); 453 | const response = ethers.utils.defaultAbiCoder.encode(['address'], [alice.address]); 454 | 455 | let errorString; 456 | 457 | try { 458 | await erc3668Resolver.resolveWithProof(response, extraData); 459 | } catch (e) { 460 | errorString = e.errorArgs[0]; 461 | } 462 | 463 | expect(errorString).to.equal('staticcall to verifier failed'); 464 | }); 465 | it('ResolveWithProf for parentDomain using verifier ', async () => { 466 | await erc3668Resolver.connect(alice).setVerifierForDomain( 467 | ethers.utils.namehash('alice.eth'), 468 | // Alice is an EOA, so this is not a valid resolver 469 | signatureVerifier.address, 470 | ['http://localhost:8080/{sender}/{data}'], 471 | ); 472 | 473 | const iface = new ethers.utils.Interface([ 474 | 'function addr(bytes32)', 475 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 476 | ]); 477 | 478 | const result = ethers.utils.hexlify(alice.address); 479 | 480 | const name = ethers.utils.dnsEncode('alice.eth'); 481 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('alice.eth')]); 482 | const extraData = iface.encodeFunctionData('resolveWithContext', [name, data, alice.address]); 483 | const response = await signAndEncodeResponse(signer, erc3668Resolver.address, result, extraData); 484 | 485 | const resolvedResponse = await erc3668Resolver.resolveWithProof(response, extraData); 486 | console.log(resolvedResponse); 487 | 488 | const ethersFormated = new ethers.providers.Formatter().callAddress(resolvedResponse); 489 | 490 | expect(ethers.utils.getAddress(ethersFormated)).to.equal(alice.address); 491 | }); 492 | it('ResolveWithProf for sub domain using verifier ', async () => { 493 | await erc3668Resolver.connect(alice).setVerifierForDomain( 494 | ethers.utils.namehash('alice.eth'), 495 | // Alice is an EOA, so this is not a valid resolver 496 | signatureVerifier.address, 497 | ['http://localhost:8080/{sender}/{data}'], 498 | ); 499 | 500 | const iface = new ethers.utils.Interface([ 501 | 'function addr(bytes32)', 502 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 503 | ]); 504 | 505 | const result = ethers.utils.hexlify(alice.address); 506 | 507 | const name = ethers.utils.dnsEncode('foo.alice.eth'); 508 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('foo.alice.eth')]); 509 | const extraData = iface.encodeFunctionData('resolveWithContext', [name, data, alice.address]); 510 | const response = await signAndEncodeResponse(signer, erc3668Resolver.address, result, extraData); 511 | 512 | const resolvedResponse = await erc3668Resolver.resolveWithProof(response, extraData); 513 | 514 | const ethersFormated = new ethers.providers.Formatter().callAddress(resolvedResponse); 515 | 516 | expect(ethers.utils.getAddress(ethersFormated)).to.equal(alice.address); 517 | }); 518 | }); 519 | describe('Metadata', () => { 520 | const convertCoinTypeToEVMChainId = (_coinType: number) => { 521 | return (0x7fffffff & _coinType) >> 0; 522 | }; 523 | 524 | it('returns metadata', async () => { 525 | await erc3668Resolver 526 | .connect(alice) 527 | .setVerifierForDomain(ethers.utils.namehash('alice.eth'), bedrockCcipVerifier.address, [ 528 | 'http://localhost:8080/{sender}/{data}', 529 | ]); 530 | const [name, coinType, graphqlUrl, storageType, storageLocation, context] = await erc3668Resolver.metadata( 531 | dnsEncode('alice.eth'), 532 | ); 533 | expect(name).to.equal('Optimism Goerli'); 534 | expect(convertCoinTypeToEVMChainId(BigNumber.from(coinType).toNumber())).to.equal(420); 535 | expect(graphqlUrl).to.equal('http://localhost:8081/graphql'); 536 | expect(storageType).to.equal(0); 537 | expect(ethers.utils.getAddress(storageLocation)).to.equal('0x5FbDB2315678afecb367f032d93F642f64180aa3'); 538 | expect(ethers.utils.getAddress(context)).to.equal(alice.address); 539 | }); 540 | it('returns metadata context of the parent if subname does not exit', async () => { 541 | await erc3668Resolver 542 | .connect(alice) 543 | .setVerifierForDomain(ethers.utils.namehash('alice.eth'), bedrockCcipVerifier.address, [ 544 | 'http://localhost:8080/{sender}/{data}', 545 | ]); 546 | const [name, coinType, graphqlUrl, storageType, storageLocation, context] = await erc3668Resolver.metadata( 547 | dnsEncode('sub.alice.eth'), 548 | ); 549 | expect(name).to.equal('Optimism Goerli'); 550 | expect(convertCoinTypeToEVMChainId(BigNumber.from(coinType).toNumber())).to.equal(420); 551 | expect(graphqlUrl).to.equal('http://localhost:8081/graphql'); 552 | expect(storageType).to.equal(0); 553 | expect(ethers.utils.getAddress(storageLocation)).to.equal('0x5FbDB2315678afecb367f032d93F642f64180aa3'); 554 | expect(ethers.utils.getAddress(context)).to.equal(alice.address); 555 | }); 556 | }); 557 | }); 558 | -------------------------------------------------------------------------------- /test/contracts/SignatureCcipVerifier.test.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 2 | import exp from 'constants'; 3 | import { BigNumber, ethers } from 'ethers'; 4 | import { dnsEncode } from 'ethers/lib/utils'; 5 | import { ethers as hreEthers } from 'hardhat'; 6 | import winston from 'winston'; 7 | 8 | import { signAndEncodeResponse } from '../../gateway/handler/signing/signAndEncodeResponse'; 9 | import { expect } from '../../test/chai-setup'; 10 | import { SignatureCcipVerifier__factory } from '../../typechain'; 11 | 12 | describe('Signature Ccip Verifier', () => { 13 | let owner: SignerWithAddress; 14 | let signer1: SignerWithAddress; 15 | let signer2: SignerWithAddress; 16 | let rando: SignerWithAddress; 17 | let alice: SignerWithAddress; 18 | let resolver: SignerWithAddress; 19 | 20 | global.logger = winston.createLogger({ 21 | level: process.env.LOG_LEVEL ?? 'info', 22 | transports: [new winston.transports.Console()], 23 | }); 24 | 25 | beforeEach(async () => { 26 | // Get signers 27 | [owner, signer1, signer2, rando, alice, resolver] = await hreEthers.getSigners(); 28 | }); 29 | describe('Constructor', () => { 30 | it('Initially set the owner,url and signers using the constructor ', async () => { 31 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 32 | .connect(owner) 33 | .deploy( 34 | owner.address, 35 | 'http://localhost:8080/graphql', 36 | 'Signature Ccip Resolver', 37 | 60, 38 | resolver.address, 39 | [signer1.address], 40 | ); 41 | 42 | const actualOwner = await signatureCcipVerifier.owner(); 43 | const actualGraphQlUrl = await signatureCcipVerifier.graphqlUrl(); 44 | const actualSigner = await signatureCcipVerifier.signers(signer1.address); 45 | 46 | expect(actualOwner).to.equal(owner.address); 47 | expect(actualGraphQlUrl).to.equal('http://localhost:8080/graphql'); 48 | 49 | expect(actualSigner).to.equal(true); 50 | }); 51 | }); 52 | describe('setOwner', () => { 53 | it('Owner can set a new Owner ', async () => { 54 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 55 | .connect(owner) 56 | .deploy( 57 | owner.address, 58 | 'http://localhost:8080/graphql', 59 | 'Signature Ccip Resolver', 60 | 60, 61 | resolver.address, 62 | [signer1.address], 63 | ); 64 | 65 | const actualOwner = await signatureCcipVerifier.owner(); 66 | expect(actualOwner).to.equal(owner.address); 67 | 68 | const tx = await signatureCcipVerifier.setOwner(signer1.address); 69 | const receipt = await tx.wait(); 70 | 71 | const [NewOwnerEvent] = receipt.events; 72 | 73 | const [newOwner] = NewOwnerEvent.args; 74 | 75 | expect(await signatureCcipVerifier.owner()).to.equal(signer1.address); 76 | expect(newOwner).to.equal(signer1.address); 77 | }); 78 | it("Rando can't set a new owner ", async () => { 79 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 80 | .connect(owner) 81 | .deploy( 82 | owner.address, 83 | 'http://localhost:8080/graphql', 84 | 'Signature Ccip Resolver', 85 | 60, 86 | resolver.address, 87 | [signer1.address], 88 | ); 89 | 90 | const actualOwner = await signatureCcipVerifier.owner(); 91 | expect(actualOwner).to.equal(owner.address); 92 | 93 | try { 94 | await signatureCcipVerifier.connect(rando).setOwner(signer1.address, { gasLimit: 1000000 }); 95 | expect.fail('should have reverted'); 96 | } catch (e) { 97 | expect(e.toString()).to.include('only owner'); 98 | } 99 | }); 100 | }); 101 | describe('set GrapgQlUrl', () => { 102 | it('Owner can set a new Url ', async () => { 103 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 104 | .connect(owner) 105 | .deploy( 106 | owner.address, 107 | 'http://localhost:8080/graphql', 108 | 'Signature Ccip Resolver', 109 | 60, 110 | resolver.address, 111 | [signer1.address], 112 | ); 113 | 114 | const actualUrl = await signatureCcipVerifier.graphqlUrl(); 115 | expect(actualUrl).to.equal('http://localhost:8080/graphql'); 116 | 117 | const tx = await signatureCcipVerifier.setGraphUrl('http://foo.io/graphql'); 118 | const receipt = await tx.wait(); 119 | 120 | const [GraphQlUrlChanged] = receipt.events; 121 | 122 | const [newUrl] = GraphQlUrlChanged.args; 123 | 124 | expect(await signatureCcipVerifier.graphqlUrl()).to.equal('http://foo.io/graphql'); 125 | expect(newUrl).to.equal('http://foo.io/graphql'); 126 | }); 127 | it("Rando can't set a new owner ", async () => { 128 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 129 | .connect(owner) 130 | .deploy( 131 | owner.address, 132 | 'http://localhost:8080/graphql', 133 | 'Signature Ccip Resolver', 134 | 60, 135 | resolver.address, 136 | [signer1.address], 137 | ); 138 | 139 | const actualUrl = await signatureCcipVerifier.graphqlUrl(); 140 | expect(actualUrl).to.equal('http://localhost:8080/graphql'); 141 | 142 | try { 143 | await signatureCcipVerifier.connect(rando).setGraphUrl('http://foo.io/graphql'); 144 | expect.fail('should have reverted'); 145 | } catch (e) { 146 | expect(e.toString()).to.include('only owner'); 147 | } 148 | }); 149 | }); 150 | describe('addSigners', () => { 151 | it('Owner can add new signers', async () => { 152 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 153 | .connect(owner) 154 | .deploy( 155 | owner.address, 156 | 'http://localhost:8080/graphql', 157 | 'Signature Ccip Resolver', 158 | 60, 159 | resolver.address, 160 | [signer1.address], 161 | ); 162 | 163 | const tx = await signatureCcipVerifier.addSigners([signer1.address, signer2.address]); 164 | const receipt = await tx.wait(); 165 | 166 | const [NewSignersEvent] = receipt.events; 167 | 168 | const [newSigners] = NewSignersEvent.args; 169 | 170 | const [eventNewSigner1, eventNewSigner2] = newSigners; 171 | 172 | expect(eventNewSigner1).to.equal(signer1.address); 173 | expect(eventNewSigner2).to.equal(signer2.address); 174 | 175 | const isSigner1Enabled = await signatureCcipVerifier.signers(signer1.address); 176 | 177 | const isSigner2Enabled = await signatureCcipVerifier.signers(signer2.address); 178 | 179 | expect(isSigner1Enabled && isSigner2Enabled).to.equal(true); 180 | }); 181 | it("Rando can't add new signers", async () => { 182 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 183 | .connect(owner) 184 | .deploy( 185 | owner.address, 186 | 'http://localhost:8080/graphql', 187 | 'Signature Ccip Resolver', 188 | 60, 189 | resolver.address, 190 | [signer1.address], 191 | ); 192 | 193 | try { 194 | await signatureCcipVerifier.connect(rando).addSigners([signer1.address, signer2.address]); 195 | expect.fail('should have reverted'); 196 | } catch (e) { 197 | expect(e.toString()).to.include('only owner'); 198 | } 199 | }); 200 | }); 201 | describe('removeSigners', () => { 202 | it('Owner can remove signers', async () => { 203 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 204 | .connect(owner) 205 | .deploy( 206 | owner.address, 207 | 'http://localhost:8080/graphql', 208 | 'Signature Ccip Resolver', 209 | 60, 210 | resolver.address, 211 | [signer1.address], 212 | ); 213 | 214 | const signerIsEnabled = await signatureCcipVerifier.signers(signer1.address); 215 | expect(signerIsEnabled).to.equal(true); 216 | 217 | const tx = await signatureCcipVerifier.removeSigners([signer1.address]); 218 | 219 | const receipt = await tx.wait(); 220 | const [SignerRemovedEvent] = receipt.events; 221 | 222 | const [signerRemoved] = SignerRemovedEvent.args; 223 | expect(signerRemoved).to.equal(signer1.address); 224 | 225 | const signerIsStillEnabled = await signatureCcipVerifier.signers(signer1.address); 226 | 227 | expect(signerIsStillEnabled).to.equal(false); 228 | }); 229 | it('Only remove signers that were already created before', async () => { 230 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 231 | .connect(owner) 232 | .deploy( 233 | owner.address, 234 | 'http://localhost:8080/graphql', 235 | 'Signature Ccip Resolver', 236 | 60, 237 | resolver.address, 238 | [signer1.address], 239 | ); 240 | 241 | const signerIsEnabled = await signatureCcipVerifier.signers(signer1.address); 242 | expect(signerIsEnabled).to.equal(true); 243 | 244 | const tx = await signatureCcipVerifier.removeSigners([signer1.address, signer2.address]); 245 | 246 | const receipt = await tx.wait(); 247 | 248 | const events = receipt.events!; 249 | // The contract should just have thrown only one event, despite beeing called with two args 250 | expect(events.length).to.equal(1); 251 | 252 | expect(events[0].decode!(events[0].data)[0]).to.equal(signer1.address); 253 | 254 | const signerIsStillEnabled = await signatureCcipVerifier.signers(signer1.address); 255 | expect(signerIsStillEnabled).to.equal(false); 256 | }); 257 | it("Rando can't remove signers", async () => { 258 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 259 | .connect(owner) 260 | .deploy( 261 | owner.address, 262 | 'http://localhost:8080/graphql', 263 | 'Signature Ccip Resolver', 264 | 60, 265 | resolver.address, 266 | [signer1.address], 267 | ); 268 | 269 | const signerIsEnabled = await signatureCcipVerifier.signers(signer1.address); 270 | expect(signerIsEnabled).to.equal(true); 271 | 272 | try { 273 | await signatureCcipVerifier.connect(rando).removeSigners([signer1.address]); 274 | expect.fail('should have reverted'); 275 | } catch (e) { 276 | expect(e.toString()).to.include('only owner'); 277 | } 278 | 279 | const signerIsStillEnabled = await signatureCcipVerifier.signers(signer1.address); 280 | expect(signerIsStillEnabled).to.equal(true); 281 | }); 282 | }); 283 | 284 | describe('resolveWithProof', () => { 285 | it('returns result if signed correctly ', async () => { 286 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 287 | .connect(owner) 288 | .deploy( 289 | owner.address, 290 | 'http://localhost:8080/graphql', 291 | 'Signature Ccip Resolver', 292 | 60, 293 | resolver.address, 294 | [signer1.address], 295 | ); 296 | 297 | const iface = new ethers.utils.Interface([ 298 | 'function addr(bytes32)', 299 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 300 | ]); 301 | 302 | const result = ethers.utils.defaultAbiCoder.encode(['bytes'], [alice.address]); 303 | 304 | const name = ethers.utils.dnsEncode('alice.eth'); 305 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('alice.eth')]); 306 | const extraData = iface.encodeFunctionData('resolveWithContext', [name, data, alice.address]); 307 | const response = await signAndEncodeResponse(signer1, resolver.address, result, extraData); 308 | 309 | const decodedResponse = await signatureCcipVerifier.resolveWithProof(response, extraData); 310 | expect(ethers.utils.getAddress(decodedResponse)).to.equal(alice.address); 311 | }); 312 | 313 | it('reverts if response was signed from rando ', async () => { 314 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 315 | .connect(owner) 316 | .deploy( 317 | owner.address, 318 | 'http://localhost:8080/graphql', 319 | 'Signature Ccip Resolver', 320 | 60, 321 | resolver.address, 322 | [signer1.address], 323 | ); 324 | 325 | const iface = new ethers.utils.Interface([ 326 | 'function addr(bytes32)', 327 | 'function resolveWithContext(bytes calldata name,bytes calldata data,bytes calldata context) external view returns (bytes memory result)', 328 | ]); 329 | 330 | const result = ethers.utils.defaultAbiCoder.encode(['bytes'], [alice.address]); 331 | 332 | const name = ethers.utils.dnsEncode('alice.eth'); 333 | const data = iface.encodeFunctionData('addr', [ethers.utils.namehash('alice.eth')]); 334 | const extraData = iface.encodeFunctionData('resolveWithContext', [name, data, alice.address]); 335 | const response = await signAndEncodeResponse(rando, resolver.address, result, extraData); 336 | 337 | try { 338 | await signatureCcipVerifier.resolveWithProof(response, extraData); 339 | expect.fail('should have reverted'); 340 | } catch (e) { 341 | console.log(e); 342 | expect(e.reason).to.equal('SignatureVerifier: Invalid signature'); 343 | } 344 | }); 345 | }); 346 | describe('Metadata', () => { 347 | it('returns metadata', async () => { 348 | const convertCoinTypeToEVMChainId = (_coinType: number) => { 349 | return (0x7fffffff & _coinType) >> 0; 350 | }; 351 | const signatureCcipVerifier = await new SignatureCcipVerifier__factory() 352 | .connect(owner) 353 | .deploy( 354 | owner.address, 355 | 'http://localhost:8080/graphql', 356 | 'Signature Ccip Resolver', 357 | 60, 358 | resolver.address, 359 | [signer1.address], 360 | ); 361 | 362 | const [name, coinType, graphqlUrl, storageType, storageLocation, context] = 363 | await signatureCcipVerifier.metadata(dnsEncode('alice.eth')); 364 | expect(name).to.equal('Signature Ccip Resolver'); 365 | expect(convertCoinTypeToEVMChainId(BigNumber.from(coinType).toNumber())).to.equal(60); 366 | expect(graphqlUrl).to.equal('http://localhost:8080/graphql'); 367 | expect(storageType).to.equal(storageType); 368 | expect(ethers.utils.toUtf8String(storageLocation)).to.equal('Postgres'); 369 | expect(context).to.equal(ethers.constants.AddressZero); 370 | }); 371 | }); 372 | }); 373 | -------------------------------------------------------------------------------- /test/e2e/ProofService.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { BigNumber } from 'ethers'; 3 | import { ethers } from 'hardhat'; 4 | import { BedrockProofVerifier, ProofServiceTestContract__factory } from 'typechain'; 5 | 6 | import { ProofService, StorageLayout } from '../../gateway/service/proof/ProofService'; 7 | 8 | const PROOF_SERVICE_TEST_CONTRACT = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; 9 | 10 | describe('ProofServiceTest', () => { 11 | let bedrockProofVerifier: BedrockProofVerifier; 12 | 13 | const l1Provider = new ethers.providers.StaticJsonRpcProvider('http://localhost:8545'); 14 | const l2Provider = new ethers.providers.StaticJsonRpcProvider('http://localhost:9545'); 15 | 16 | beforeEach(async () => { 17 | // See github.com/ethereum-optimism/optimism/op-bindings/predeploys/dev_addresses.go 18 | const BedrockProofVerifierFactory = await ethers.getContractFactory('BedrockProofVerifier'); 19 | bedrockProofVerifier = (await BedrockProofVerifierFactory.deploy( 20 | '0x6900000000000000000000000000000000000000', 21 | )) as BedrockProofVerifier; 22 | }); 23 | 24 | it('bool slot', async () => { 25 | const proofService = new ProofService(l1Provider, l2Provider); 26 | const { proof } = await proofService.createProof( 27 | PROOF_SERVICE_TEST_CONTRACT, 28 | ethers.constants.HashZero, 29 | StorageLayout.FIXED, 30 | ); 31 | 32 | const responseBytes = await bedrockProofVerifier.getProofValue(proof); 33 | expect(responseBytes).to.equal('0x01'); 34 | }); 35 | it('bytes32 slot', async () => { 36 | const proofService = new ProofService(l1Provider, l2Provider); 37 | const { proof } = await proofService.createProof( 38 | PROOF_SERVICE_TEST_CONTRACT, 39 | '0x0000000000000000000000000000000000000000000000000000000000000001', 40 | StorageLayout.FIXED, 41 | ); 42 | 43 | const responseBytes = await bedrockProofVerifier.getProofValue(proof); 44 | expect(responseBytes).to.equal(ethers.utils.namehash('alice.eth')); 45 | }); 46 | 47 | it('address slot', async () => { 48 | const proofService = new ProofService(l1Provider, l2Provider); 49 | const { proof } = await proofService.createProof( 50 | PROOF_SERVICE_TEST_CONTRACT, 51 | '0x0000000000000000000000000000000000000000000000000000000000000002', 52 | StorageLayout.FIXED, 53 | ); 54 | 55 | const responseBytes = await bedrockProofVerifier.getProofValue(proof); 56 | expect(ethers.utils.getAddress(responseBytes)).to.equal('0x8111DfD23B99233a7ae871b7c09cCF0722847d89'); 57 | }); 58 | it('uint slot', async () => { 59 | const proofService = new ProofService(l1Provider, l2Provider); 60 | const { proof } = await proofService.createProof( 61 | PROOF_SERVICE_TEST_CONTRACT, 62 | '0x0000000000000000000000000000000000000000000000000000000000000003', 63 | StorageLayout.FIXED, 64 | ); 65 | 66 | const responseBytes = await bedrockProofVerifier.getProofValue(proof); 67 | expect(BigNumber.from(responseBytes).toNumber()).to.equal(123); 68 | }); 69 | it('bytes slot', async () => { 70 | const proofService = new ProofService(l1Provider, l2Provider); 71 | const { proof } = await proofService.createProof( 72 | PROOF_SERVICE_TEST_CONTRACT, 73 | '0x0000000000000000000000000000000000000000000000000000000000000004', 74 | StorageLayout.DYNAMIC, 75 | ); 76 | 77 | const responseBytes = await bedrockProofVerifier.getProofValue(proof); 78 | expect(responseBytes).to.equal(ethers.utils.namehash('alice.eth')); 79 | }); 80 | it('string slot', async () => { 81 | const proofService = new ProofService(l1Provider, l2Provider); 82 | const { proof } = await proofService.createProof( 83 | PROOF_SERVICE_TEST_CONTRACT, 84 | '0x0000000000000000000000000000000000000000000000000000000000000005', 85 | StorageLayout.DYNAMIC, 86 | ); 87 | 88 | const responseBytes = await bedrockProofVerifier.getProofValue(proof); 89 | expect(Buffer.from(responseBytes.slice(2), 'hex').toString()).to.equal('Hello from Alice'); 90 | }); 91 | it('long string slot', async () => { 92 | const longString = 93 | 'Praesent elementum ligula et dolor varius lobortis. Morbi eget eleifend augue.Nunc quis lectus in augue feugiat malesuada vitae sed lacus. Aenean suscipit tristique mauris a blandit. Maecenas ut lectus quis metus commodo tincidunt. Aliquam erat volutpat. Fusce non erat malesuada, consequat mauris id, tincidunt nisi. Aenean a pulvinar ex. Mauris ullamcorper eget odio nec eleifend. Donec pellentesque et tellus id consectetur. Nullam dictum, felis sit amet consectetur convallis, leo lorem rhoncus nulla, eget tincidunt leo erat sit amet urna. Quisque urna turpis, lobortis laoreet nunc at, fermentum ultrices neque.Proin dignissim enim non arcu elementum tempor.Donec convallis turpis at erat luctus, eget pellentesque augue blandit.In faucibus rhoncus mollis.Phasellus malesuada, mauris ut finibus venenatis, ex risus consectetur dolor, quis suscipit augue magna iaculis leo.Proin non nibh at justo porttitor sollicitudin.Pellentesque id malesuada tellus.Aliquam condimentum accumsan ex eu vulputate.Aenean faucibus a quam vitae tincidunt.Fusce vitae mollis nunc, at volutpat ex.Vestibulum sed tellus urna.Donec ac urna lectus.Phasellus a dolor elit.Aenean bibendum hendrerit elit, in cursus sem maximus id.Sed porttitor nulla non consectetur vehicula.Fusce elementum, urna in gravida accumsan, lectus arcu congue augue, at rhoncus purus nunc ac libero.'; 94 | 95 | const proofService = new ProofService(l1Provider, l2Provider); 96 | const { proof } = await proofService.createProof( 97 | PROOF_SERVICE_TEST_CONTRACT, 98 | '0x0000000000000000000000000000000000000000000000000000000000000006', 99 | StorageLayout.DYNAMIC, 100 | ); 101 | 102 | const responseBytes = await bedrockProofVerifier.getProofValue(proof); 103 | expect(Buffer.from(responseBytes.slice(2), 'hex').toString()).to.equal(longString); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/e2e/optimismBedrockHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { FakeContract, smock } from '@defi-wonderland/smock'; 2 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 3 | import axios from 'axios'; 4 | import MockAdapter from 'axios-mock-adapter'; 5 | import bodyParser from 'body-parser'; 6 | import { expect } from 'chai'; 7 | import { ethers, Wallet } from 'ethers'; 8 | import express from 'express'; 9 | import { config, ethers as hreEthers } from 'hardhat'; 10 | import request from 'supertest'; 11 | import winston from 'winston'; 12 | 13 | import { getConfigReader } from '../../gateway/config/ConfigReader'; 14 | import { ccipGateway } from '../../gateway/http/ccipGateway'; 15 | import { StorageLayout } from '../../gateway/service/proof/ProofService'; 16 | import { 17 | BedrockCcipVerifier, 18 | BedrockCcipVerifier__factory, 19 | BedrockProofVerifier, 20 | BedrockProofVerifier__factory, 21 | ENS, 22 | ERC3668Resolver, 23 | INameWrapper, 24 | } from '../../typechain'; 25 | import { getGateWayUrl } from '../helper/getGatewayUrl'; 26 | 27 | describe('Optimism Bedrock Handler', () => { 28 | let ccipApp: express.Express; 29 | let erc3668Resolver: ERC3668Resolver; 30 | let owner: SignerWithAddress; 31 | 32 | // 0x8111DfD23B99233a7ae871b7c09cCF0722847d89 33 | const alice = new ethers.Wallet('0xfd9f3842a10eb01ccf3109d4bd1c4b165721bf8c26db5db7570c146f9fad6014').connect( 34 | hreEthers.provider, 35 | ); 36 | 37 | let signer: Wallet; 38 | 39 | let ensRegistry: FakeContract; 40 | // NameWrapper 41 | let nameWrapper: FakeContract; 42 | 43 | // Bedrock Proof Verifier 44 | let bedrockProofVerifier: BedrockProofVerifier; 45 | let bedrockCcipVerifier: BedrockCcipVerifier; 46 | 47 | beforeEach(async () => { 48 | [owner] = await hreEthers.getSigners(); 49 | signer = ethers.Wallet.createRandom(); 50 | /** 51 | * MOCK ENS Registry 52 | */ 53 | ensRegistry = (await smock.fake( 54 | '@ensdomains/ens-contracts/contracts/registry/ENS.sol:ENS', 55 | )) as FakeContract; 56 | ensRegistry.owner.whenCalledWith(ethers.utils.namehash('alice.eth')).returns(alice.address); 57 | /** 58 | * MOCK NameWrapper 59 | */ 60 | nameWrapper = (await smock.fake( 61 | '@ensdomains/ens-contracts/contracts/wrapper/INameWrapper.sol:INameWrapper', 62 | )) as FakeContract; 63 | ensRegistry.owner.whenCalledWith(ethers.utils.namehash('namewrapper.alice.eth')).returns(nameWrapper.address); 64 | // nameWrapper.ownerOf.whenCalledWith(ethers.utils.namehash("namewrapper.alice.eth")).returns(alice.address); 65 | 66 | const BedrockProofVerifierFactory = (await hreEthers.getContractFactory( 67 | 'BedrockProofVerifier', 68 | )) as BedrockProofVerifier__factory; 69 | bedrockProofVerifier = await BedrockProofVerifierFactory.deploy('0x6900000000000000000000000000000000000000'); 70 | 71 | const BedrockCcipVerifierFactory = (await hreEthers.getContractFactory( 72 | 'BedrockCcipVerifier', 73 | )) as BedrockCcipVerifier__factory; 74 | 75 | bedrockCcipVerifier = await BedrockCcipVerifierFactory.deploy( 76 | owner.address, 77 | 'http://localhost:8081/graphql', 78 | 'Optimism Bedrock', 79 | 420, 80 | bedrockProofVerifier.address, 81 | '0x5FbDB2315678afecb367f032d93F642f64180aa3', 82 | ); 83 | const ERC3668ResolverFactory = await hreEthers.getContractFactory('ERC3668Resolver'); 84 | erc3668Resolver = (await ERC3668ResolverFactory.deploy( 85 | ensRegistry.address, 86 | nameWrapper.address, 87 | ethers.constants.AddressZero, 88 | [], 89 | )) as ERC3668Resolver; 90 | 91 | await owner.sendTransaction({ 92 | to: alice.address, 93 | value: ethers.utils.parseEther('1'), 94 | }); 95 | 96 | ccipApp = express(); 97 | ccipApp.use(bodyParser.json()); 98 | 99 | global.logger = winston.createLogger({ 100 | level: process.env.LOG_LEVEL ?? 'info', 101 | transports: [new winston.transports.Console()], 102 | }); 103 | }); 104 | 105 | it('Returns valid string data from resolver', async () => { 106 | process.env.SIGNER_PRIVATE_KEY = signer.privateKey; 107 | 108 | const mock = new MockAdapter(axios); 109 | 110 | await erc3668Resolver 111 | .connect(alice) 112 | .setVerifierForDomain(ethers.utils.namehash('alice.eth'), bedrockCcipVerifier.address, [ 113 | 'http://test/{sender}/{data}', 114 | ]); 115 | 116 | const { callData } = await getGateWayUrl('alice.eth', 'foo', erc3668Resolver); 117 | 118 | const result = [ 119 | { 120 | result: ethers.utils.defaultAbiCoder.encode(['string'], ['Hello from Alice']), 121 | target: '0x5FbDB2315678afecb367f032d93F642f64180aa3', 122 | slot: '0x0000000000000000000000000000000000000000000000000000000000000005', 123 | layout: StorageLayout.DYNAMIC, 124 | }, 125 | ]; 126 | mock.onGet(`http://test/${bedrockCcipVerifier.address}/${callData}`).reply(200, result); 127 | 128 | const ccipConfig = {}; 129 | ccipConfig[bedrockCcipVerifier.address] = { 130 | type: 'optimism-bedrock', 131 | handlerUrl: 'http://test', 132 | l1ProviderUrl: 'http://localhost:8545', 133 | l2ProviderUrl: 'http://localhost:9545', 134 | l1chainId: 900, 135 | l2chainId: 901, 136 | }; 137 | 138 | const configReader = getConfigReader(JSON.stringify(ccipConfig)); 139 | config[bedrockCcipVerifier.address] = ccipApp.use(ccipGateway(configReader)); 140 | 141 | const sender = bedrockCcipVerifier.address; 142 | 143 | // You the url returned by he contract to fetch the profile from the ccip gateway 144 | const response = await request(ccipApp).get(`/${sender}/${callData}`).send(); 145 | 146 | expect(response.status).to.equal(200); 147 | 148 | const responseEncoded = await erc3668Resolver.resolveWithProof(response.body.data, callData); 149 | 150 | const [decodedBytes] = ethers.utils.defaultAbiCoder.decode(['bytes'], responseEncoded); 151 | const [decodedArray] = ethers.utils.defaultAbiCoder.decode(['bytes[]'], decodedBytes); 152 | const [responseDecoded] = ethers.utils.defaultAbiCoder.decode(['string'], decodedArray[0]); 153 | 154 | expect(responseDecoded).to.equal('Hello from Alice'); 155 | }); 156 | it('Returns valid bytes32 data from resolver', async () => { 157 | process.env.SIGNER_PRIVATE_KEY = signer.privateKey; 158 | 159 | const mock = new MockAdapter(axios); 160 | 161 | await erc3668Resolver 162 | .connect(alice) 163 | .setVerifierForDomain(ethers.utils.namehash('alice.eth'), bedrockCcipVerifier.address, [ 164 | 'http://test/{sender}/{data}', 165 | ]); 166 | 167 | const { callData } = await getGateWayUrl('alice.eth', 'foo', erc3668Resolver); 168 | 169 | const result = [ 170 | { 171 | result: ethers.utils.namehash('alice.eth'), 172 | target: '0x5FbDB2315678afecb367f032d93F642f64180aa3', 173 | slot: '0x0000000000000000000000000000000000000000000000000000000000000001', 174 | layout: StorageLayout.FIXED, 175 | }, 176 | ]; 177 | mock.onGet(`http://test/${bedrockCcipVerifier.address}/${callData}`).reply(200, result); 178 | 179 | const ccipConfig = {}; 180 | ccipConfig[bedrockCcipVerifier.address] = { 181 | type: 'optimism-bedrock', 182 | handlerUrl: 'http://test', 183 | l1ProviderUrl: 'http://localhost:8545', 184 | l2ProviderUrl: 'http://localhost:9545', 185 | l1chainId: 900, 186 | l2chainId: 901, 187 | }; 188 | 189 | const configReader = getConfigReader(JSON.stringify(ccipConfig)); 190 | config[bedrockCcipVerifier.address] = ccipApp.use(ccipGateway(configReader)); 191 | 192 | const sender = bedrockCcipVerifier.address; 193 | 194 | // You the url returned by he contract to fetch the profile from the ccip gateway 195 | const response = await request(ccipApp).get(`/${sender}/${callData}`).send(); 196 | 197 | expect(response.status).to.equal(200); 198 | 199 | const responseEncoded = await erc3668Resolver.resolveWithProof(response.body.data, callData); 200 | 201 | const [decodedBytes] = ethers.utils.defaultAbiCoder.decode(['bytes'], responseEncoded); 202 | const [[decodedResponse]] = ethers.utils.defaultAbiCoder.decode(['bytes[]'], decodedBytes); 203 | expect(decodedResponse).to.equal(ethers.utils.namehash('alice.eth')); 204 | }); 205 | }); 206 | -------------------------------------------------------------------------------- /test/gateway/readConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import winston from 'winston'; 3 | 4 | import { getConfigReader } from '../../gateway/config/ConfigReader'; 5 | 6 | global.logger = winston.createLogger({ 7 | level: process.env.LOG_LEVEL ?? 'info', 8 | transports: [new winston.transports.Console()], 9 | }); 10 | // 0x49e0aec78ec0df50852e99116e524a43be91b789 11 | // 0x49e0AeC78ec0dF50852E99116E524a43bE91B789 12 | describe('ReadConfig Test', () => { 13 | it('Reads config for Signiture Resolver', () => { 14 | const configString = JSON.stringify({ 15 | '0xafb5b5032d920c8158e541c6326ce63baf60aabf': { 16 | type: 'signing', 17 | handlerUrl: 'http://test', 18 | }, 19 | '0x49e0AeC78ec0dF50852E99116E524a43bE91B789': { 20 | type: 'optimism-bedrock', 21 | handlerUrl: 'http://test', 22 | }, 23 | }); 24 | expect(getConfigReader(configString).getConfigForResolver('0xafb5b5032d920c8158e541c6326ce63baf60aabf')).to.eql( 25 | { 26 | type: 'signing', 27 | handlerUrl: 'http://test', 28 | }, 29 | ); 30 | 31 | expect(getConfigReader(configString).getConfigForResolver('0xAFb5B5032d920C8158E541c6326CE63BAF60aAbf')).to.eql( 32 | { 33 | type: 'signing', 34 | handlerUrl: 'http://test', 35 | }, 36 | ); 37 | expect(getConfigReader(configString).getConfigForResolver('0x49e0AeC78ec0dF50852E99116E524a43bE91B789')).to.eql( 38 | { 39 | type: 'optimism-bedrock', 40 | handlerUrl: 'http://test', 41 | }, 42 | ); 43 | expect(getConfigReader(configString).getConfigForResolver('0x49e0aec78ec0df50852e99116e524a43be91b789')).to.eql( 44 | { 45 | type: 'optimism-bedrock', 46 | handlerUrl: 'http://test', 47 | }, 48 | ); 49 | }); 50 | it('Throws when config is undefined', () => { 51 | expect(() => getConfigReader(undefined)).to.throw('CONFIG IS MISSING'); 52 | }); 53 | it('Throws when config is not valid JSON', () => { 54 | expect(() => getConfigReader('FOOO')).to.throw('Invalid JSON'); 55 | }); 56 | it('Throw if config entry key is not an address', () => { 57 | expect(() => 58 | getConfigReader( 59 | JSON.stringify({ 60 | '0xafb5b5032d920c8158e541c6326ce63baf60aabf': { 61 | type: 'signing', 62 | handlerUrl: 'http://test', 63 | }, 64 | FOOO: { 65 | type: 'signing', 66 | handlerUrl: 'http://test', 67 | }, 68 | }), 69 | ), 70 | ).to.throw('Invalid address FOOO'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/gateway/signatureHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { FakeContract, smock } from '@defi-wonderland/smock'; 2 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 3 | import axios from 'axios'; 4 | import MockAdapter from 'axios-mock-adapter'; 5 | import bodyParser from 'body-parser'; 6 | import { expect } from 'chai'; 7 | import { ethers, Wallet } from 'ethers'; 8 | import express from 'express'; 9 | import { config, ethers as hreEthers } from 'hardhat'; 10 | import request from 'supertest'; 11 | import winston from 'winston'; 12 | 13 | import { getConfigReader } from '../../gateway/config/ConfigReader'; 14 | import { ccipGateway } from '../../gateway/http/ccipGateway'; 15 | import { 16 | ENS, 17 | ERC3668Resolver, 18 | INameWrapper, 19 | SignatureCcipVerifier, 20 | SignatureCcipVerifier__factory, 21 | } from '../../typechain'; 22 | import { getGateWayUrl, getGateWayUrlForAddress } from '../helper/getGatewayUrl'; 23 | 24 | global.logger = winston.createLogger({ 25 | level: process.env.LOG_LEVEL ?? 'info', 26 | transports: [new winston.transports.Console()], 27 | }); 28 | 29 | describe('Signature Handler', () => { 30 | let ccipApp: express.Express; 31 | let erc3668Resolver: ERC3668Resolver; 32 | let owner: SignerWithAddress; 33 | let vitalik: SignerWithAddress; 34 | 35 | let signer: Wallet; 36 | 37 | let ensRegistry: FakeContract; 38 | // NameWrapper 39 | let nameWrapper: FakeContract; 40 | 41 | let signatureCcipVerifier: SignatureCcipVerifier; 42 | 43 | beforeEach(async () => { 44 | [owner, vitalik] = await hreEthers.getSigners(); 45 | signer = ethers.Wallet.createRandom(); 46 | /** 47 | * MOCK ENS Registry 48 | */ 49 | ensRegistry = (await smock.fake( 50 | '@ensdomains/ens-contracts/contracts/registry/ENS.sol:ENS', 51 | )) as FakeContract; 52 | ensRegistry.owner.whenCalledWith(ethers.utils.namehash('vitalik.eth')).returns(vitalik.address); 53 | /** 54 | * MOCK NameWrapper 55 | */ 56 | nameWrapper = (await smock.fake( 57 | '@ensdomains/ens-contracts/contracts/wrapper/INameWrapper.sol:INameWrapper', 58 | )) as FakeContract; 59 | ensRegistry.owner.whenCalledWith(ethers.utils.namehash('namewrapper.alice.eth')).returns(nameWrapper.address); 60 | // nameWrapper.ownerOf.whenCalledWith(ethers.utils.namehash("namewrapper.alice.eth")).returns(alice.address); 61 | 62 | const SignerCcipVerifierFactory = (await hreEthers.getContractFactory( 63 | 'SignatureCcipVerifier', 64 | )) as SignatureCcipVerifier__factory; 65 | 66 | const ERC3668ResolverFactory = await hreEthers.getContractFactory('ERC3668Resolver'); 67 | erc3668Resolver = (await ERC3668ResolverFactory.deploy( 68 | ensRegistry.address, 69 | nameWrapper.address, 70 | ethers.constants.AddressZero, 71 | [''], 72 | )) as ERC3668Resolver; 73 | 74 | signatureCcipVerifier = await SignerCcipVerifierFactory.deploy( 75 | owner.address, 76 | 'http://localhost:8081/graphql', 77 | 'Signature Ccip Resolver', 78 | 420, 79 | erc3668Resolver.address, 80 | [signer.address], 81 | ); 82 | // Get signers 83 | [owner] = await hreEthers.getSigners(); 84 | 85 | ccipApp = express(); 86 | ccipApp.use(bodyParser.json()); 87 | }); 88 | 89 | it('Returns valid data from resolver', async () => { 90 | process.env.SIGNER_PRIVATE_KEY = signer.privateKey; 91 | 92 | const mock = new MockAdapter(axios); 93 | 94 | await erc3668Resolver 95 | .connect(vitalik) 96 | .setVerifierForDomain(ethers.utils.namehash('vitalik.eth'), signatureCcipVerifier.address, [ 97 | 'http://test/{sender}/{data}', 98 | ]); 99 | 100 | const { callData } = await getGateWayUrl('vitalik.eth', 'my-record', erc3668Resolver); 101 | 102 | const result = ethers.utils.defaultAbiCoder.encode(['string'], ['Hello World']); 103 | mock.onGet(`http://test/${erc3668Resolver.address}/${callData}`).reply(200, result); 104 | 105 | const ccipConfig = {}; 106 | ccipConfig[erc3668Resolver.address] = { 107 | type: 'signing', 108 | handlerUrl: 'http://test', 109 | }; 110 | 111 | const configReader = getConfigReader(JSON.stringify(ccipConfig)); 112 | 113 | config[erc3668Resolver.address] = ccipApp.use(ccipGateway(configReader)); 114 | 115 | const sender = erc3668Resolver.address; 116 | 117 | // You the url returned by he contract to fetch the profile from the ccip gateway 118 | const response = await request(ccipApp).get(`/${sender}/${callData}`).send(); 119 | 120 | expect(response.status).to.equal(200); 121 | console.log('check'); 122 | const resultString = await erc3668Resolver.resolveWithProof(response.body.data, callData); 123 | 124 | expect(resultString).to.equal(result); 125 | }); 126 | it('Returns valid address from resolver', async () => { 127 | process.env.SIGNER_PRIVATE_KEY = signer.privateKey; 128 | 129 | const mock = new MockAdapter(axios); 130 | 131 | await erc3668Resolver 132 | .connect(vitalik) 133 | .setVerifierForDomain(ethers.utils.namehash('vitalik.eth'), signatureCcipVerifier.address, [ 134 | 'http://test/{sender}/{data}', 135 | ]); 136 | 137 | const { callData } = await getGateWayUrlForAddress('vitalik.eth', erc3668Resolver); 138 | 139 | // Pass the addr to the contract 140 | const expected = ethers.utils.hexlify(vitalik.address); 141 | console.log(expected); 142 | 143 | mock.onGet(`http://test/${erc3668Resolver.address}/${callData}`).reply(200, expected); 144 | 145 | const ccipConfig = {}; 146 | ccipConfig[erc3668Resolver.address] = { 147 | type: 'signing', 148 | handlerUrl: 'http://test', 149 | }; 150 | 151 | const configReader = getConfigReader(JSON.stringify(ccipConfig)); 152 | 153 | config[erc3668Resolver.address] = ccipApp.use(ccipGateway(configReader)); 154 | 155 | const sender = erc3668Resolver.address; 156 | 157 | // You the url returned by he contract to fetch the profile from the ccip gateway 158 | const response = await request(ccipApp).get(`/${sender}/${callData}`).send(); 159 | 160 | expect(response.status).to.equal(200); 161 | console.log('check'); 162 | const resultString = await erc3668Resolver.resolveWithProof(response.body.data, callData); 163 | 164 | const ethersFormated = new ethers.providers.Formatter().callAddress(resultString); 165 | 166 | expect(ethersFormated).to.equal(ethers.utils.getAddress(vitalik.address)); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /test/helper/encodeEnsName.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | /* Encodes an ensName to an dns compliant format 3 | * https://github.com/ensdomains/offchain-resolver/blob/ed330e4322b1fafe2ffbd1496829c75185dd9e2e/packages/gateway/test/e2e.test.ts#L193 4 | * @param ensName the ensname in the format foo.bar.eth 5 | * @returns the dns encoded name 6 | */ 7 | export function encodeEnsName(ensName: string) { 8 | // strip leading and trailing . 9 | const n = ensName.replace(/^\.|\.$/gm, ''); 10 | 11 | const bufLen = n === '' ? 1 : n.length + 2; 12 | const buf = Buffer.allocUnsafe(bufLen); 13 | 14 | let offset = 0; 15 | if (n.length) { 16 | const list = n.split('.'); 17 | for (const e of list) { 18 | const len = buf.write(e, offset + 1); 19 | buf[offset] = len; 20 | offset += len + 1; 21 | } 22 | } 23 | buf[offset++] = 0; 24 | return '0x' + buf.reduce((output, elem) => output + ('0' + elem.toString(16)).slice(-2), ''); 25 | } 26 | -------------------------------------------------------------------------------- /test/helper/getGatewayUrl.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ethers } from 'ethers'; 2 | 3 | import { encodeEnsName } from './encodeEnsName'; 4 | 5 | export const getGateWayUrl = async (ensName: string, recordName: string, offchainResolver: Contract) => { 6 | try { 7 | const textData = new ethers.utils.Interface([ 8 | 'function text(bytes32 node, string calldata key) external view returns (string memory)', 9 | ]).encodeFunctionData('text', [ethers.utils.namehash(ensName), recordName]); 10 | 11 | // This always revers and throws the OffchainLookup Exceptions hence we need to catch it 12 | await offchainResolver.resolve(encodeEnsName(ensName), textData); 13 | return { gatewayUrl: '', callbackFunction: '', extraData: '' }; 14 | } catch (err: any) { 15 | const { sender, urls, callData } = err.errorArgs; 16 | // Decode call 17 | 18 | // Replace template vars 19 | const gatewayUrl = urls[0].replace('{sender}', sender).replace('{data}', callData); 20 | 21 | return { gatewayUrl, sender, callData }; 22 | } 23 | }; 24 | export const getGateWayUrlForAddress = async (ensName: string, offchainResolver: Contract) => { 25 | try { 26 | const addrData = new ethers.utils.Interface([ 27 | 'function addr(bytes32 node) external view returns (address)', 28 | ]).encodeFunctionData('addr', [ethers.utils.namehash(ensName)]); 29 | 30 | // This always revers and throws the OffchainLookup Exceptions hence we need to catch it 31 | await offchainResolver.resolve(encodeEnsName(ensName), addrData); 32 | return { gatewayUrl: '', callbackFunction: '', extraData: '' }; 33 | } catch (err: any) { 34 | const { sender, urls, callData } = err.errorArgs; 35 | // Decode call 36 | 37 | // Replace template vars 38 | const gatewayUrl = urls[0].replace('{sender}', sender).replace('{data}', callData); 39 | 40 | return { gatewayUrl, sender, callData }; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "CommonJS", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "outDir": "dist", 17 | "sourceMap": true 18 | }, 19 | "include": ["gateway","abi"], 20 | "exclude": ["gateway/**/*.test.ts"], 21 | 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "outDir": "dist", 7 | "resolveJsonModule": true, 8 | "baseUrl": "./" 9 | }, 10 | "include": ["./scripts", "./test", "./deploy"], 11 | "files": ["./hardhat.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-plugin-prettier", "tslint-config-prettier"], 3 | "rules": { 4 | "prettier": true, 5 | "object-literal-sort-keys": false, 6 | "no-submodule-imports": false, 7 | "interface-name": false, 8 | "max-classes-per-file": false, 9 | "no-empty": false, 10 | "no-console": false, 11 | "no-bitwise": false, 12 | "only-arrow-functions": false, 13 | "variable-name": [true, "check-format", "allow-leading-underscore", "allow-pascal-case"], 14 | "ordered-imports": [ 15 | true, 16 | { 17 | "grouped-imports": true, 18 | "import-sources-order": "case-insensitive" 19 | } 20 | ], 21 | "no-floating-promises": true, 22 | "prefer-conditional-expression": false, 23 | "no-implicit-dependencies": [true, "dev"], 24 | "max-line-length": [ 25 | false, 26 | { 27 | "limit": 160, 28 | "ignore-pattern": "^import |^export {(.*?)}", 29 | "check-strings": true, 30 | "check-regex": true 31 | } 32 | ] 33 | } 34 | } 35 | --------------------------------------------------------------------------------