diff --git a/.github/workflows/verifydeployed.yml b/.github/workflows/verifydeployed.yml new file mode 100644 index 000000000..8bae264e0 --- /dev/null +++ b/.github/workflows/verifydeployed.yml @@ -0,0 +1,66 @@ +name: Verify deployed contracts + +on: + workflow_dispatch: + inputs: + contracts: + description: 'List of deployed contracts to verify (space delimited)' + required: true + type: string + network: + description: 'Network where the contracts are deployed' + required: true + type: choice + default: mainnet + options: + - mainnet + - arbitrum-one + - goerli + - arbitrum-goerli + +jobs: + build: + name: Compile contracts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'yarn' + - run: yarn install --non-interactive --frozen-lockfile + + - name: Compile contracts + run: yarn build + + - name: Save build artifacts + uses: actions/upload-artifact@v3 + with: + name: contract-artifacts + path: | + build + cache/*.json + + verify: + name: Verify deployments + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'yarn' + - run: yarn install --non-interactive --frozen-lockfile + - name: Get build artifacts + uses: actions/download-artifact@v3 + with: + name: contract-artifacts + + - name: Verify contracts on Defender + run: yarn hardhat --network ${{ inputs.network }} verify-defender ${{ inputs.contracts }} + env: + DEFENDER_API_KEY: "${{ secrets.DEFENDER_API_KEY }}" + DEFENDER_API_SECRET: "${{ secrets.DEFENDER_API_SECRET }}" + INFURA_KEY: "${{ secrets.INFURA_KEY }}" + WORKFLOW_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/hardhat.config.ts b/hardhat.config.ts index 1821701d4..4c9c557a3 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -19,6 +19,7 @@ import 'hardhat-contract-sizer' import 'hardhat-tracer' import '@tenderly/hardhat-tenderly' import '@openzeppelin/hardhat-upgrades' +import '@openzeppelin/hardhat-defender' import '@typechain/hardhat' import 'solidity-coverage' import 'hardhat-storage-layout' @@ -209,6 +210,10 @@ const config: HardhatUserConfig = { runOnCompile: false, disambiguatePaths: false, }, + defender: { + apiKey: process.env.DEFENDER_API_KEY!, + apiSecret: process.env.DEFENDER_API_SECRET!, + }, } setupNetworkProviders(config) diff --git a/package.json b/package.json index b9a34b9a3..8c14f97ac 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@nomiclabs/hardhat-waffle": "^2.0.1", "@openzeppelin/contracts": "^3.4.1", "@openzeppelin/contracts-upgradeable": "3.4.2", + "@openzeppelin/hardhat-defender": "^1.8.1", "@openzeppelin/hardhat-upgrades": "^1.6.0", "@tenderly/hardhat-tenderly": "^1.0.11", "@typechain/ethers-v5": "^7.0.0", diff --git a/tasks/verify/defender.ts b/tasks/verify/defender.ts new file mode 100644 index 000000000..bd2c17b7c --- /dev/null +++ b/tasks/verify/defender.ts @@ -0,0 +1,102 @@ +import { execSync } from 'child_process' +import { task } from 'hardhat/config' +import { HardhatRuntimeEnvironment as HRE } from 'hardhat/types' +import { constants } from 'ethers' +import { appendFileSync } from 'fs' +import type { VerificationResponse } from '@openzeppelin/hardhat-defender/dist/verify-deployment' + +async function main(args: { referenceUrl?: string; contracts: string[] }, hre: HRE) { + const { referenceUrl, contracts } = args + const { defender, network, graph } = hre + const summaryPath = process.env.GITHUB_STEP_SUMMARY + if (summaryPath) appendFileSync(summaryPath, `# Contracts deployment verification\n\n`) + + const workflowUrl = + referenceUrl || + process.env.WORKFLOW_URL || + execSync(`git config --get remote.origin.url`).toString().trim() + const addressBook = graph().addressBook + const errs = [] + + for (const contractName of contracts) { + const entry = addressBook.getEntry(contractName) + if (!entry || entry.address === constants.AddressZero) { + errs.push([contractName, { message: `Entry not found on address book.` }]) + continue + } + + const addressToVerify = entry.implementation?.address ?? entry.address + console.error(`Verifying artifact for ${contractName} at ${addressToVerify}`) + + try { + const response = await defender.verifyDeployment(addressToVerify, contractName, workflowUrl) + console.error(`Bytecode match for ${contractName} is ${response.matchType}`) + if (summaryPath) { + appendFileSync( + summaryPath, + `- ${contractName} at ${etherscanLink(network.name, addressToVerify)} is ${ + response.matchType + } ${emojiForMatch(response.matchType)}\n`, + ) + } + if (response.matchType === 'NO_MATCH') { + errs.push([contractName, { message: `No bytecode match.` }]) + } + } catch (err: any) { + if (summaryPath) { + appendFileSync( + summaryPath, + `- ${contractName} at ${etherscanLink( + network.name, + addressToVerify, + )} failed to verify :x:\n`, + ) + } + console.error(`Error verifying artifact: ${err.message}`) + errs.push([contractName, err]) + } + } + + if (errs.length > 0) { + throw new Error( + `Some verifications failed:\n${errs + .map(([name, err]) => ` ${name}: ${err.message}`) + .join('\n')}`, + ) + } +} + +function etherscanLink(network: string, address: string): string { + switch (network) { + case 'mainnet': + return `[\`${address}\`](https://etherscan.io/address/${address})` + case 'arbitrum-one': + return `[\`${address}\`](https://arbiscan.io/address/${address})` + case 'goerli': + return `[\`${address}\`](https://goerli.etherscan.io/address/${address})` + case 'arbitrum-goerli': + return `[\`${address}\`](https://goerli.arbiscan.io/address/${address})` + default: + return `\`${address}\`` + } +} + +function emojiForMatch(matchType: VerificationResponse['matchType']): string { + switch (matchType) { + case 'EXACT': + return ':heavy_check_mark:' + case 'PARTIAL': + return ':warning:' + case 'NO_MATCH': + return ':x:' + } +} + +task('verify-defender') + .addVariadicPositionalParam('contracts', 'List of contracts to verify') + .addOptionalParam( + 'referenceUrl', + 'URL to link to for artifact verification (defaults to $WORKFLOW_URL or the remote.origin.url of the repository)', + ) + .setDescription('Verifies deployed implementations on Defender') + .setAction(main) diff --git a/yarn.lock b/yarn.lock index 33ead1e4d..3f5473bea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1006,6 +1006,26 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2.tgz#d81f786fda2871d1eb8a8c5a73e455753ba53527" integrity sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA== +"@openzeppelin/hardhat-defender@^1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@openzeppelin/hardhat-defender/-/hardhat-defender-1.8.1.tgz#d2527cc7898f76cf45ba51b0aaa40075ef742b52" + integrity sha512-vXfNSteO4xi8iGe/CRU5en2t6sbH8T4dIhrhZLa3TYke7LUt5du4GPFv74Kie+KIynPzAeVNEdW9suqDZNLYwA== + dependencies: + "@openzeppelin/hardhat-upgrades" "^1.20.0" + defender-admin-client "^1.29.0-rc.1" + defender-base-client "^1.3.1" + ethereumjs-util "^7.1.5" + +"@openzeppelin/hardhat-upgrades@^1.20.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-1.21.0.tgz#e90fb7d858093f35a300b3a5a2fd32bca6179dfc" + integrity sha512-Kwl7IN0Hlhj4HluMTTl0DrtU90OI/Q6rG3sAyd2pv3fababe9EuZqs9DydOlkWM45JwTzC+eBzX3TgHsqI13eA== + dependencies: + "@openzeppelin/upgrades-core" "^1.20.0" + chalk "^4.1.0" + debug "^4.1.1" + proper-lockfile "^4.1.1" + "@openzeppelin/hardhat-upgrades@^1.6.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-1.17.0.tgz#24ea0f366c3b2df985263cf8b1b796afd04d7e13" @@ -1029,6 +1049,19 @@ proper-lockfile "^4.1.1" solidity-ast "^0.4.15" +"@openzeppelin/upgrades-core@^1.20.0": + version "1.20.5" + resolved "https://registry.yarnpkg.com/@openzeppelin/upgrades-core/-/upgrades-core-1.20.5.tgz#1650b7805a847a5ccc097cd676ec12316a5a4b8d" + integrity sha512-Wp4uUov9/8cY0H4xHYsGCkLh0EItrpusSdQPWOTI1Q/YDDfu4uTH3LYyTeVAavzEvkAuKCCuTOPnZBibLZGxSw== + dependencies: + cbor "^8.0.0" + chalk "^4.1.0" + compare-versions "^5.0.0" + debug "^4.1.1" + ethereumjs-util "^7.0.3" + proper-lockfile "^4.1.1" + solidity-ast "^0.4.15" + "@resolver-engine/core@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@resolver-engine/core/-/core-0.3.3.tgz#590f77d85d45bc7ecc4e06c654f41345db6ca967" @@ -1799,6 +1832,17 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" +amazon-cognito-identity-js@^4.3.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/amazon-cognito-identity-js/-/amazon-cognito-identity-js-4.6.3.tgz#889410379a5fc5e883edc95f4ce233cc628e354c" + integrity sha512-MPVJfirbdmSGo7l4h7Kbn3ms1eJXT5Xq8ly+mCPPi8yAxaxdg7ouMUUNTqtDykoZxIdDLF/P6F3Zbg3dlGKOWg== + dependencies: + buffer "4.9.2" + crypto-js "^4.0.0" + fast-base64-decode "^1.0.0" + isomorphic-unfetch "^3.0.0" + js-cookie "^2.2.1" + amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -2042,6 +2086,13 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + async@1.x, async@^1.4.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -2101,7 +2152,7 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axios@^0.21.1: +axios@^0.21.1, axios@^0.21.2: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== @@ -2646,7 +2697,7 @@ base-x@^3.0.2, base-x@^3.0.6, base-x@^3.0.8: dependencies: safe-buffer "^5.0.1" -base64-js@^1.3.1: +base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2971,6 +3022,15 @@ buffer-xor@^2.0.1: dependencies: safe-buffer "^5.1.1" +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + buffer@^5.0.5, buffer@^5.2.1, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -3524,6 +3584,11 @@ compare-versions@^4.0.0: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-4.1.3.tgz#8f7b8966aef7dc4282b45dfa6be98434fc18a1a4" integrity sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg== +compare-versions@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-5.0.1.tgz#14c6008436d994c3787aba38d4087fabe858555e" + integrity sha512-v8Au3l0b+Nwkp4G142JcgJFh1/TUhdxut7wzD1Nq1dyp5oa3tXaqb03EXOAB6jS4gMlalkjAUPZBMiAfKUixHQ== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -3782,6 +3847,11 @@ crypto-browserify@3.12.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto-js@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -3908,6 +3978,27 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +defender-admin-client@^1.29.0-rc.1: + version "1.37.0" + resolved "https://registry.yarnpkg.com/defender-admin-client/-/defender-admin-client-1.37.0.tgz#d4b6cc6c7ebaa9aab138f39b10572018c25dc9a1" + integrity sha512-0I0n+LUo4X75uiy/Sd5V5Qv1HwhmIvC++uswxx8xnQ0VUK+y0ic0uMk/8ACrItH/9EGH2YrURNm8ZGg9AY1K5A== + dependencies: + axios "^0.21.2" + defender-base-client "1.37.0" + lodash "^4.17.19" + node-fetch "^2.6.0" + +defender-base-client@1.37.0, defender-base-client@^1.3.1: + version "1.37.0" + resolved "https://registry.yarnpkg.com/defender-base-client/-/defender-base-client-1.37.0.tgz#22f63357ac99c2c8f64eab6e52c99ef113c62d3a" + integrity sha512-V6tU0q8/n1/m/edT2FlTvUmZn6u5/A64FqYQfrMgg4PEy1TvYCz9tF+3dnGjk+sJrzICAv0GQWwLw/+8uRq2mg== + dependencies: + amazon-cognito-identity-js "^4.3.3" + async-retry "^1.3.3" + axios "^0.21.2" + lodash "^4.17.19" + node-fetch "^2.6.0" + defer-to-connect@^1.0.1: version "1.1.3" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" @@ -5261,6 +5352,11 @@ fake-merkle-patricia-tree@^1.0.1: dependencies: checkpoint-store "^1.1.0" +fast-base64-decode@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" + integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -6472,7 +6568,7 @@ idna-uts46-hx@^2.3.1: dependencies: punycode "2.1.0" -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -7134,7 +7230,7 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== -isarray@1.0.0, isarray@~1.0.0: +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== @@ -7174,6 +7270,14 @@ isomorphic-fetch@^3.0.0: node-fetch "^2.6.1" whatwg-fetch "^3.4.1" +isomorphic-unfetch@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" + integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q== + dependencies: + node-fetch "^2.6.1" + unfetch "^4.2.0" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -7260,6 +7364,11 @@ it-to-stream@^0.1.1: p-fifo "^1.0.0" readable-stream "^3.6.0" +js-cookie@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== + js-sha3@0.5.7, js-sha3@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7" @@ -10258,6 +10367,11 @@ retry-as-promised@^5.0.0: resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-5.0.0.tgz#f4ecc25133603a2d2a7aff4a128691d7bc506d54" integrity sha512-6S+5LvtTl2ggBumk04hBo/4Uf6fRJUwIgunGZ7CYEBCeufGFW1Pu6ucUf/UskHeWOIsUcLOGLFXPig5tR5V1nA== +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -11846,6 +11960,11 @@ undici@^5.4.0: resolved "https://registry.yarnpkg.com/undici/-/undici-5.8.0.tgz#dec9a8ccd90e5a1d81d43c0eab6503146d649a4f" integrity sha512-1F7Vtcez5w/LwH2G2tGnFIihuWUlc58YidwLiCv+jR2Z50x0tNXpRRw7eOIJ+GvqCqIkg9SB7NWAJ/T9TLfv8Q== +unfetch@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" + integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"