├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .openzeppelin └── goerli.json ├── LICENSE ├── README.md ├── addresses.json ├── contracts ├── Box.sol └── Greeter.sol ├── hardhat.config.ts ├── imgs ├── awaiting-review.png ├── sample-artifact-verified.png ├── sample-proposal.png ├── sample-summary.png └── workflow-diagram.png ├── multisigs.json ├── package.json ├── releases ├── v1.3 │ ├── deployed.json │ └── index.yml └── v1.4 │ ├── deploy.ts │ ├── deployed.json │ └── index.yml ├── tasks ├── deploy-proxy.ts ├── noop.ts ├── prepare-upgrade.ts ├── propose-upgrade.ts ├── utils │ ├── chains_mini.json │ └── index.ts └── verify-deployed.ts ├── test └── Box.test.js ├── tsconfig.json └── yarn.lock /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | - "!releases/**" 12 | 13 | jobs: 14 | build: 15 | name: Build 16 | runs-on: ubuntu-20.04 17 | timeout-minutes: 10 18 | env: 19 | INFURA_PROJECT_ID: "${{ secrets.INFURA_PROJECT_ID }}" 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: '14' 26 | cache: 'yarn' 27 | - run: yarn install --frozen-lockfile 28 | 29 | - name: Compile contracts 30 | run: yarn build 31 | 32 | - name: Save build artifacts 33 | uses: actions/upload-artifact@v3 34 | with: 35 | name: contract-artifacts 36 | path: | 37 | artifacts 38 | cache/*.json 39 | 40 | test: 41 | name: Unit Tests 42 | runs-on: ubuntu-20.04 43 | timeout-minutes: 10 44 | needs: build 45 | steps: 46 | - uses: actions/checkout@v3 47 | - uses: actions/setup-node@v3 48 | with: 49 | node-version: '14' 50 | cache: 'yarn' 51 | - run: yarn install --frozen-lockfile 52 | 53 | - name: Get build artifacts 54 | uses: actions/download-artifact@v3 55 | with: 56 | name: contract-artifacts 57 | 58 | - name: Show downloaded build artifacts 59 | run: ls -R artifacts cache 60 | 61 | - name: Run unit tests 62 | run: yarn test --no-compile -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - 'master' 8 | - 'release/**' 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | build: 16 | name: Build release 17 | if: startsWith(github.head_ref, 'release/') 18 | runs-on: ubuntu-22.04 19 | timeout-minutes: 10 20 | env: 21 | INFURA_PROJECT_ID: "${{ secrets.INFURA_PROJECT_ID }}" 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | with: 26 | ref: ${{ github.event.pull_request.head.sha }} 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: '14' 30 | cache: 'yarn' 31 | - run: yarn install --frozen-lockfile 32 | 33 | - name: Compile contracts 34 | run: yarn build 35 | 36 | - name: Save build artifacts 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: contract-artifacts 40 | path: | 41 | artifacts 42 | cache/*.json 43 | 44 | test: 45 | name: Test release 46 | if: startsWith(github.head_ref, 'release/') 47 | runs-on: ubuntu-22.04 48 | timeout-minutes: 20 49 | needs: build 50 | steps: 51 | - uses: actions/checkout@v3 52 | with: 53 | ref: ${{ github.event.pull_request.head.sha }} 54 | - uses: actions/setup-node@v3 55 | with: 56 | node-version: '14' 57 | cache: 'yarn' 58 | - run: yarn install --frozen-lockfile 59 | 60 | - name: Get build artifacts 61 | uses: actions/download-artifact@v3 62 | with: 63 | name: contract-artifacts 64 | 65 | - name: Run unit tests 66 | run: yarn test --no-compile 67 | 68 | prepare: 69 | name: Prepare release 70 | if: startsWith(github.head_ref, 'release/') 71 | runs-on: ubuntu-22.04 72 | needs: test 73 | outputs: 74 | release_version: ${{ steps.parse.outputs.release_version }} 75 | release_path: ${{ steps.parse.outputs.release_path }} 76 | release_network: ${{ steps.parse.outputs.release_network }} 77 | release_title: ${{ steps.parse.outputs.release_title }} 78 | release_description: ${{ steps.parse.outputs.release_description }} 79 | release_multisig: ${{ steps.parse.outputs.release_multisig }} 80 | release_audited: ${{ steps.parse.outputs.release_audited }} 81 | release_deploy_cmd: ${{ steps.parse.outputs.release_deploy_cmd }} 82 | release_verify_cmd: ${{ steps.parse.outputs.release_verify_cmd }} 83 | release_finish_cmd: ${{ steps.parse.outputs.release_finish_cmd }} 84 | steps: 85 | - uses: actions/checkout@v3 86 | with: 87 | ref: ${{ github.event.pull_request.head.sha }} 88 | fetch-depth: 0 89 | 90 | - name: Parse release info 91 | id: parse 92 | run: | 93 | version=${BRANCH_NAME#release\/} 94 | path=releases/v$version 95 | network=$(yq -r .network $path/index.yml) 96 | echo "release_version=$version" >> $GITHUB_OUTPUT 97 | echo "release_path=$path" >> $GITHUB_OUTPUT 98 | echo "release_network=$network" >> $GITHUB_OUTPUT 99 | echo "release_multisig=$(jq -r .$network multisigs.json)" >> $GITHUB_OUTPUT 100 | echo "release_deploy_cmd=$(yq -r '.deploy // ""' $path/index.yml)" >> $GITHUB_OUTPUT 101 | echo "release_verify_cmd=$(yq -r '.verify // ""' $path/index.yml)" >> $GITHUB_OUTPUT 102 | echo "release_finish_cmd=$(yq -r '.finish // ""' $path/index.yml)" >> $GITHUB_OUTPUT 103 | echo "release_audited=$(yq -r '.audited // ""' $path/index.yml)" >> $GITHUB_OUTPUT 104 | echo "release_title=$(yq -r '.title // ""' $path/index.yml)" >> $GITHUB_OUTPUT 105 | echo "release_description<> $GITHUB_OUTPUT 106 | echo "$(yq -r '.description // ""' $path/index.yml)" >> $GITHUB_OUTPUT 107 | echo "DESCRIPTION_DELIMITER" >> $GITHUB_OUTPUT 108 | env: 109 | BRANCH_NAME: ${{ github.head_ref }} 110 | 111 | - name: Output summary 112 | run: | 113 | echo "## $TITLE" >> $GITHUB_STEP_SUMMARY 114 | echo "" >> $GITHUB_STEP_SUMMARY 115 | echo "**Network:** $NETWORK" >> $GITHUB_STEP_SUMMARY 116 | echo "**Commit:** [\`$COMMIT\`]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$COMMIT)" >> $GITHUB_STEP_SUMMARY 117 | echo "" >> $GITHUB_STEP_SUMMARY 118 | echo "$DESCRIPTION" >> $GITHUB_STEP_SUMMARY 119 | echo "" >> $GITHUB_STEP_SUMMARY 120 | env: 121 | TITLE: ${{ steps.parse.outputs.release_title }} 122 | DESCRIPTION: ${{ steps.parse.outputs.release_description }} 123 | NETWORK: ${{ steps.parse.outputs.release_network }} 124 | COMMIT: ${{ github.event.pull_request.head.sha }} 125 | 126 | - name: Output audit info 127 | if: "steps.parse.outputs.release_audited != ''" 128 | run: | 129 | echo "---" >> $GITHUB_STEP_SUMMARY 130 | echo "### Audit" >> $GITHUB_STEP_SUMMARY 131 | echo "Audited contracts at commit [\`$AUDIT_COMMIT\`]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$AUDIT_COMMIT) :detective:" >> $GITHUB_STEP_SUMMARY 132 | audit_diff="$(git diff $AUDIT_COMMIT *.sol)" 133 | if [[ -z $audit_diff ]]; then 134 | echo "Contracts have not been modified since audit :heavy_check_mark:" >> $GITHUB_STEP_SUMMARY 135 | else 136 | echo "Contracts have been modified since audit :warning:" >> $GITHUB_STEP_SUMMARY 137 | echo "" >> $GITHUB_STEP_SUMMARY 138 | echo "\`\`\`diff" >> $GITHUB_STEP_SUMMARY 139 | echo "$audit_diff" >> $GITHUB_STEP_SUMMARY 140 | echo "\`\`\`" >> $GITHUB_STEP_SUMMARY 141 | fi 142 | env: 143 | AUDIT_COMMIT: ${{ steps.parse.outputs.release_audited }} 144 | 145 | deploy: 146 | name: Deploy contracts 147 | if: startsWith(github.head_ref, 'release/') 148 | runs-on: ubuntu-22.04 149 | timeout-minutes: 20 150 | needs: prepare 151 | environment: production 152 | outputs: 153 | deploy_commit: ${{ steps.commit.outputs.commit_hash }} 154 | steps: 155 | - uses: actions/checkout@v3 156 | with: 157 | ref: ${{ github.event.pull_request.head.sha }} 158 | - uses: actions/setup-node@v3 159 | with: 160 | node-version: '14' 161 | cache: 'yarn' 162 | - run: yarn install --frozen-lockfile 163 | 164 | - name: Get build artifacts 165 | uses: actions/download-artifact@v3 166 | with: 167 | name: contract-artifacts 168 | 169 | - name: Deploy contracts 170 | run: eval yarn hardhat --network ${{ needs.prepare.outputs.release_network }} ${{ needs.prepare.outputs.release_deploy_cmd }} 171 | env: 172 | INFURA_PROJECT_ID: "${{ secrets.INFURA_PROJECT_ID }}" 173 | PRIVATE_KEY: "${{ secrets.PRIVATE_KEY }}" 174 | ETHERSCAN_API_KEY: "${{ secrets.ETHERSCAN_API_KEY }}" 175 | DEFENDER_API_KEY: "${{ secrets.DEFENDER_API_KEY }}" 176 | DEFENDER_API_SECRET: "${{ secrets.DEFENDER_API_SECRET }}" 177 | RELEASE_PATH: "${{ needs.prepare.outputs.release_path }}" 178 | MULTISIG_ADDRESS: "${{ needs.prepare.outputs.release_multisig }}" 179 | 180 | - name: Commit changes 181 | uses: stefanzweifel/git-auto-commit-action@v4 182 | id: commit 183 | if: always() 184 | with: 185 | commit_message: Update registries of deployed addresses 186 | file_pattern: '.openzeppelin/ releases/ addresses.json' 187 | skip_checkout: true 188 | 189 | verify: 190 | name: Verify contracts 191 | if: "startsWith(github.head_ref, 'release/') && needs.prepare.outputs.release_verify_cmd != ''" 192 | runs-on: ubuntu-22.04 193 | timeout-minutes: 20 194 | needs: 195 | - prepare 196 | - deploy 197 | steps: 198 | - uses: actions/checkout@v3 199 | with: 200 | ref: ${{ needs.deploy.outputs.deploy_commit || github.event.pull_request.head.sha }} 201 | - uses: actions/setup-node@v3 202 | with: 203 | node-version: '14' 204 | cache: 'yarn' 205 | - run: yarn install --frozen-lockfile 206 | 207 | - name: Get build artifacts 208 | uses: actions/download-artifact@v3 209 | with: 210 | name: contract-artifacts 211 | 212 | - name: Verify contracts 213 | run: eval yarn hardhat --network ${{ needs.prepare.outputs.release_network }} ${{ needs.prepare.outputs.release_verify_cmd }} 214 | env: 215 | INFURA_PROJECT_ID: "${{ secrets.INFURA_PROJECT_ID }}" 216 | PRIVATE_KEY: "${{ secrets.PRIVATE_KEY }}" 217 | ETHERSCAN_API_KEY: "${{ secrets.ETHERSCAN_API_KEY }}" 218 | DEFENDER_API_KEY: "${{ secrets.DEFENDER_API_KEY }}" 219 | DEFENDER_API_SECRET: "${{ secrets.DEFENDER_API_SECRET }}" 220 | RELEASE_PATH: "${{ needs.prepare.outputs.release_path }}" 221 | ARTIFACT_REFERENCE_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" 222 | 223 | finish-release: 224 | name: Finish release 225 | if: "startsWith(github.head_ref, 'release/')" 226 | runs-on: ubuntu-22.04 227 | timeout-minutes: 20 228 | environment: production 229 | needs: 230 | - prepare 231 | - deploy 232 | steps: 233 | - uses: actions/checkout@v3 234 | with: 235 | ref: ${{ needs.deploy.outputs.deploy_commit || github.event.pull_request.head.sha }} 236 | - uses: actions/setup-node@v3 237 | with: 238 | node-version: '14' 239 | cache: 'yarn' 240 | - run: yarn install --frozen-lockfile 241 | 242 | - name: Get build artifacts 243 | uses: actions/download-artifact@v3 244 | with: 245 | name: contract-artifacts 246 | 247 | - name: Complete release 248 | if: "needs.prepare.outputs.release_finish_cmd != ''" 249 | run: eval yarn hardhat --network ${{ needs.prepare.outputs.release_network }} ${{ needs.prepare.outputs.release_finish_cmd }} 250 | env: 251 | INFURA_PROJECT_ID: "${{ secrets.INFURA_PROJECT_ID }}" 252 | PRIVATE_KEY: "${{ secrets.PRIVATE_KEY }}" 253 | ETHERSCAN_API_KEY: "${{ secrets.ETHERSCAN_API_KEY }}" 254 | DEFENDER_API_KEY: "${{ secrets.DEFENDER_API_KEY }}" 255 | DEFENDER_API_SECRET: "${{ secrets.DEFENDER_API_SECRET }}" 256 | RELEASE_PATH: "${{ needs.prepare.outputs.release_path }}" 257 | MULTISIG_ADDRESS: "${{ needs.prepare.outputs.release_multisig }}" 258 | 259 | - name: Link to run in PR 260 | uses: actions/github-script@v6 261 | env: 262 | RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" 263 | with: 264 | script: | 265 | github.rest.issues.createComment({ 266 | issue_number: context.issue.number, 267 | owner: context.repo.owner, 268 | repo: context.repo.repo, 269 | body: `[**Deploy finished**](${process.env.RUN_URL})`, 270 | }); 271 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /artifacts 4 | /cache 5 | /address.txt 6 | .env 7 | .openzeppelin/unknown-31337.json 8 | -------------------------------------------------------------------------------- /.openzeppelin/goerli.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": "3.2", 3 | "proxies": [ 4 | { 5 | "address": "0x22431A9012cC3Fa9Ba7643e16275e8565Ff3a0BA", 6 | "txHash": "0x2cea4a8d03b072f41ea29dfd8af9317c9daafc71d5834e3df019be2fd6c551b2", 7 | "kind": "uups" 8 | }, 9 | { 10 | "address": "0x96F2B948d40006d9BaF546Ae507A7f27702128C1", 11 | "txHash": "0xdb61c5fdac531f2c5c850c3117372f9d95cbf70a27a8b8bf128c251abe834b76", 12 | "kind": "uups" 13 | }, 14 | { 15 | "address": "0x0e46dF975AF95B8bf8F52AbC97a49669C2d663b5", 16 | "txHash": "0x64f5dd4e93978296def84e9ebce267dfc29567244c827e4296b659c07571dbf3", 17 | "kind": "uups" 18 | }, 19 | { 20 | "address": "0x2D575f5832B2A877e36628cdC5c8f4141874D397", 21 | "txHash": "0x91f0b7902faacb22cbebbf44d164f7d32fa45e6118473964e6aee88c91bbf708", 22 | "kind": "uups" 23 | }, 24 | { 25 | "address": "0x2dE4b51615E2d732905131e90201ca362F1789Fb", 26 | "txHash": "0x98edbdbab7b268e9c356804d01c1b1e1da1d94599b862e67c0054c3b16ca6171", 27 | "kind": "uups" 28 | } 29 | ], 30 | "impls": { 31 | "7cdc9adf26211ab1cecb8e7285976791ae457ed41c192b8abff87c1a8f82fbd7": { 32 | "address": "0x4042d5fD79124EcE1a5DcC5985294E5804Dd6f53", 33 | "txHash": "0xe57625d56478e873d8ddfd830c14eaa3dd7fd112aa4e4ec0453d6dc0cf29d62c", 34 | "layout": { 35 | "storage": [ 36 | { 37 | "label": "_initialized", 38 | "offset": 0, 39 | "slot": "0", 40 | "type": "t_uint8", 41 | "contract": "Initializable", 42 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 43 | "retypedFrom": "bool" 44 | }, 45 | { 46 | "label": "_initializing", 47 | "offset": 1, 48 | "slot": "0", 49 | "type": "t_bool", 50 | "contract": "Initializable", 51 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 52 | }, 53 | { 54 | "label": "__gap", 55 | "offset": 0, 56 | "slot": "1", 57 | "type": "t_array(t_uint256)50_storage", 58 | "contract": "ContextUpgradeable", 59 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 60 | }, 61 | { 62 | "label": "_owner", 63 | "offset": 0, 64 | "slot": "51", 65 | "type": "t_address", 66 | "contract": "OwnableUpgradeable", 67 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 68 | }, 69 | { 70 | "label": "__gap", 71 | "offset": 0, 72 | "slot": "52", 73 | "type": "t_array(t_uint256)49_storage", 74 | "contract": "OwnableUpgradeable", 75 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 76 | }, 77 | { 78 | "label": "__gap", 79 | "offset": 0, 80 | "slot": "101", 81 | "type": "t_array(t_uint256)50_storage", 82 | "contract": "ERC1967UpgradeUpgradeable", 83 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 84 | }, 85 | { 86 | "label": "__gap", 87 | "offset": 0, 88 | "slot": "151", 89 | "type": "t_array(t_uint256)50_storage", 90 | "contract": "UUPSUpgradeable", 91 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 92 | }, 93 | { 94 | "label": "value", 95 | "offset": 0, 96 | "slot": "201", 97 | "type": "t_uint256", 98 | "contract": "Box", 99 | "src": "contracts/Box.sol:8" 100 | } 101 | ], 102 | "types": { 103 | "t_address": { 104 | "label": "address", 105 | "numberOfBytes": "20" 106 | }, 107 | "t_array(t_uint256)49_storage": { 108 | "label": "uint256[49]", 109 | "numberOfBytes": "1568" 110 | }, 111 | "t_array(t_uint256)50_storage": { 112 | "label": "uint256[50]", 113 | "numberOfBytes": "1600" 114 | }, 115 | "t_bool": { 116 | "label": "bool", 117 | "numberOfBytes": "1" 118 | }, 119 | "t_uint256": { 120 | "label": "uint256", 121 | "numberOfBytes": "32" 122 | }, 123 | "t_uint8": { 124 | "label": "uint8", 125 | "numberOfBytes": "1" 126 | } 127 | } 128 | } 129 | }, 130 | "9fd4445770a40be8960da4bbecb5229f16adcc10a61e32ff6a34da26a72fe790": { 131 | "address": "0x369a4c5bE3329de4D9b9E24840222659ee7E668E", 132 | "txHash": "0x7724345c3a05e4efafcd6f716cd723c4c0280960be44be27f9af892aabcd6f05", 133 | "layout": { 134 | "storage": [ 135 | { 136 | "label": "_initialized", 137 | "offset": 0, 138 | "slot": "0", 139 | "type": "t_uint8", 140 | "contract": "Initializable", 141 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 142 | "retypedFrom": "bool" 143 | }, 144 | { 145 | "label": "_initializing", 146 | "offset": 1, 147 | "slot": "0", 148 | "type": "t_bool", 149 | "contract": "Initializable", 150 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 151 | }, 152 | { 153 | "label": "__gap", 154 | "offset": 0, 155 | "slot": "1", 156 | "type": "t_array(t_uint256)50_storage", 157 | "contract": "ContextUpgradeable", 158 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 159 | }, 160 | { 161 | "label": "_owner", 162 | "offset": 0, 163 | "slot": "51", 164 | "type": "t_address", 165 | "contract": "OwnableUpgradeable", 166 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 167 | }, 168 | { 169 | "label": "__gap", 170 | "offset": 0, 171 | "slot": "52", 172 | "type": "t_array(t_uint256)49_storage", 173 | "contract": "OwnableUpgradeable", 174 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 175 | }, 176 | { 177 | "label": "__gap", 178 | "offset": 0, 179 | "slot": "101", 180 | "type": "t_array(t_uint256)50_storage", 181 | "contract": "ERC1967UpgradeUpgradeable", 182 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 183 | }, 184 | { 185 | "label": "__gap", 186 | "offset": 0, 187 | "slot": "151", 188 | "type": "t_array(t_uint256)50_storage", 189 | "contract": "UUPSUpgradeable", 190 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 191 | }, 192 | { 193 | "label": "value", 194 | "offset": 0, 195 | "slot": "201", 196 | "type": "t_uint256", 197 | "contract": "Box", 198 | "src": "contracts/Box.sol:9" 199 | } 200 | ], 201 | "types": { 202 | "t_address": { 203 | "label": "address", 204 | "numberOfBytes": "20" 205 | }, 206 | "t_array(t_uint256)49_storage": { 207 | "label": "uint256[49]", 208 | "numberOfBytes": "1568" 209 | }, 210 | "t_array(t_uint256)50_storage": { 211 | "label": "uint256[50]", 212 | "numberOfBytes": "1600" 213 | }, 214 | "t_bool": { 215 | "label": "bool", 216 | "numberOfBytes": "1" 217 | }, 218 | "t_uint256": { 219 | "label": "uint256", 220 | "numberOfBytes": "32" 221 | }, 222 | "t_uint8": { 223 | "label": "uint8", 224 | "numberOfBytes": "1" 225 | } 226 | } 227 | } 228 | }, 229 | "532c44eceba374396eb92d83c8cd689067159c144122c6a4df6fbec998ca2efc": { 230 | "address": "0xb65ba627aF30Ca2baAeA7160e8Fc0D7867352131", 231 | "txHash": "0x876b66ba2971425a5e0178bc6062f5ffb6c564f5533810f48708b0cb4c5ed4fb", 232 | "layout": { 233 | "storage": [ 234 | { 235 | "label": "_initialized", 236 | "offset": 0, 237 | "slot": "0", 238 | "type": "t_uint8", 239 | "contract": "Initializable", 240 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 241 | "retypedFrom": "bool" 242 | }, 243 | { 244 | "label": "_initializing", 245 | "offset": 1, 246 | "slot": "0", 247 | "type": "t_bool", 248 | "contract": "Initializable", 249 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 250 | }, 251 | { 252 | "label": "__gap", 253 | "offset": 0, 254 | "slot": "1", 255 | "type": "t_array(t_uint256)50_storage", 256 | "contract": "ContextUpgradeable", 257 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 258 | }, 259 | { 260 | "label": "_owner", 261 | "offset": 0, 262 | "slot": "51", 263 | "type": "t_address", 264 | "contract": "OwnableUpgradeable", 265 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 266 | }, 267 | { 268 | "label": "__gap", 269 | "offset": 0, 270 | "slot": "52", 271 | "type": "t_array(t_uint256)49_storage", 272 | "contract": "OwnableUpgradeable", 273 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 274 | }, 275 | { 276 | "label": "__gap", 277 | "offset": 0, 278 | "slot": "101", 279 | "type": "t_array(t_uint256)50_storage", 280 | "contract": "ERC1967UpgradeUpgradeable", 281 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 282 | }, 283 | { 284 | "label": "__gap", 285 | "offset": 0, 286 | "slot": "151", 287 | "type": "t_array(t_uint256)50_storage", 288 | "contract": "UUPSUpgradeable", 289 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 290 | }, 291 | { 292 | "label": "value", 293 | "offset": 0, 294 | "slot": "201", 295 | "type": "t_uint256", 296 | "contract": "Box", 297 | "src": "contracts/Box.sol:9" 298 | } 299 | ], 300 | "types": { 301 | "t_address": { 302 | "label": "address", 303 | "numberOfBytes": "20" 304 | }, 305 | "t_array(t_uint256)49_storage": { 306 | "label": "uint256[49]", 307 | "numberOfBytes": "1568" 308 | }, 309 | "t_array(t_uint256)50_storage": { 310 | "label": "uint256[50]", 311 | "numberOfBytes": "1600" 312 | }, 313 | "t_bool": { 314 | "label": "bool", 315 | "numberOfBytes": "1" 316 | }, 317 | "t_uint256": { 318 | "label": "uint256", 319 | "numberOfBytes": "32" 320 | }, 321 | "t_uint8": { 322 | "label": "uint8", 323 | "numberOfBytes": "1" 324 | } 325 | } 326 | } 327 | }, 328 | "0eefee0fa7b9463ee5d67a8a94f2916a4fd2389ef8aab45ab0aa4dec399f76c5": { 329 | "address": "0x8b2EFf9e785d0554f914cfda42514B55b11375DE", 330 | "txHash": "0x77f17ca968a1e066b946fad412b81908e8578343a6c7fa79d3828855906ddf20", 331 | "layout": { 332 | "storage": [ 333 | { 334 | "label": "_initialized", 335 | "offset": 0, 336 | "slot": "0", 337 | "type": "t_uint8", 338 | "contract": "Initializable", 339 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 340 | "retypedFrom": "bool" 341 | }, 342 | { 343 | "label": "_initializing", 344 | "offset": 1, 345 | "slot": "0", 346 | "type": "t_bool", 347 | "contract": "Initializable", 348 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 349 | }, 350 | { 351 | "label": "__gap", 352 | "offset": 0, 353 | "slot": "1", 354 | "type": "t_array(t_uint256)50_storage", 355 | "contract": "ContextUpgradeable", 356 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 357 | }, 358 | { 359 | "label": "_owner", 360 | "offset": 0, 361 | "slot": "51", 362 | "type": "t_address", 363 | "contract": "OwnableUpgradeable", 364 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 365 | }, 366 | { 367 | "label": "__gap", 368 | "offset": 0, 369 | "slot": "52", 370 | "type": "t_array(t_uint256)49_storage", 371 | "contract": "OwnableUpgradeable", 372 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 373 | }, 374 | { 375 | "label": "__gap", 376 | "offset": 0, 377 | "slot": "101", 378 | "type": "t_array(t_uint256)50_storage", 379 | "contract": "ERC1967UpgradeUpgradeable", 380 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 381 | }, 382 | { 383 | "label": "__gap", 384 | "offset": 0, 385 | "slot": "151", 386 | "type": "t_array(t_uint256)50_storage", 387 | "contract": "UUPSUpgradeable", 388 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 389 | }, 390 | { 391 | "label": "value", 392 | "offset": 0, 393 | "slot": "201", 394 | "type": "t_uint256", 395 | "contract": "Box", 396 | "src": "contracts/Box.sol:9" 397 | } 398 | ], 399 | "types": { 400 | "t_address": { 401 | "label": "address", 402 | "numberOfBytes": "20" 403 | }, 404 | "t_array(t_uint256)49_storage": { 405 | "label": "uint256[49]", 406 | "numberOfBytes": "1568" 407 | }, 408 | "t_array(t_uint256)50_storage": { 409 | "label": "uint256[50]", 410 | "numberOfBytes": "1600" 411 | }, 412 | "t_bool": { 413 | "label": "bool", 414 | "numberOfBytes": "1" 415 | }, 416 | "t_uint256": { 417 | "label": "uint256", 418 | "numberOfBytes": "32" 419 | }, 420 | "t_uint8": { 421 | "label": "uint8", 422 | "numberOfBytes": "1" 423 | } 424 | } 425 | } 426 | }, 427 | "4dedde2ffd8063c1f7becc0236d1d1057de26ac397cc3a99ce25ce2588382dbe": { 428 | "address": "0x71e9df6F79545e121bA7C09192E351A31BC64F12", 429 | "txHash": "0x80eb33965d91fcfef29e9d469792fc5c2539e5de0c63ced112b6abdd00aa92e0", 430 | "layout": { 431 | "storage": [ 432 | { 433 | "label": "_initialized", 434 | "offset": 0, 435 | "slot": "0", 436 | "type": "t_uint8", 437 | "contract": "Initializable", 438 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 439 | "retypedFrom": "bool" 440 | }, 441 | { 442 | "label": "_initializing", 443 | "offset": 1, 444 | "slot": "0", 445 | "type": "t_bool", 446 | "contract": "Initializable", 447 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 448 | }, 449 | { 450 | "label": "__gap", 451 | "offset": 0, 452 | "slot": "1", 453 | "type": "t_array(t_uint256)50_storage", 454 | "contract": "ContextUpgradeable", 455 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 456 | }, 457 | { 458 | "label": "_owner", 459 | "offset": 0, 460 | "slot": "51", 461 | "type": "t_address", 462 | "contract": "OwnableUpgradeable", 463 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 464 | }, 465 | { 466 | "label": "__gap", 467 | "offset": 0, 468 | "slot": "52", 469 | "type": "t_array(t_uint256)49_storage", 470 | "contract": "OwnableUpgradeable", 471 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 472 | }, 473 | { 474 | "label": "__gap", 475 | "offset": 0, 476 | "slot": "101", 477 | "type": "t_array(t_uint256)50_storage", 478 | "contract": "ERC1967UpgradeUpgradeable", 479 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 480 | }, 481 | { 482 | "label": "__gap", 483 | "offset": 0, 484 | "slot": "151", 485 | "type": "t_array(t_uint256)50_storage", 486 | "contract": "UUPSUpgradeable", 487 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 488 | }, 489 | { 490 | "label": "value", 491 | "offset": 0, 492 | "slot": "201", 493 | "type": "t_uint256", 494 | "contract": "Box", 495 | "src": "contracts/Box.sol:9" 496 | } 497 | ], 498 | "types": { 499 | "t_address": { 500 | "label": "address", 501 | "numberOfBytes": "20" 502 | }, 503 | "t_array(t_uint256)49_storage": { 504 | "label": "uint256[49]", 505 | "numberOfBytes": "1568" 506 | }, 507 | "t_array(t_uint256)50_storage": { 508 | "label": "uint256[50]", 509 | "numberOfBytes": "1600" 510 | }, 511 | "t_bool": { 512 | "label": "bool", 513 | "numberOfBytes": "1" 514 | }, 515 | "t_uint256": { 516 | "label": "uint256", 517 | "numberOfBytes": "32" 518 | }, 519 | "t_uint8": { 520 | "label": "uint8", 521 | "numberOfBytes": "1" 522 | } 523 | } 524 | } 525 | }, 526 | "851fc3cdfeb1e7e801ea60c3251013bc161f3f9dc6d904451cd0b96aa82c446c": { 527 | "address": "0x2bBf5F012Aa6a802cF813E3298b1bfd732c57679", 528 | "txHash": "0x8d1d0f3ec209bcff8f17850843241160b9635dce7601fe6018eede185d2e24ff", 529 | "layout": { 530 | "storage": [ 531 | { 532 | "label": "_initialized", 533 | "offset": 0, 534 | "slot": "0", 535 | "type": "t_uint8", 536 | "contract": "Initializable", 537 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 538 | "retypedFrom": "bool" 539 | }, 540 | { 541 | "label": "_initializing", 542 | "offset": 1, 543 | "slot": "0", 544 | "type": "t_bool", 545 | "contract": "Initializable", 546 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 547 | }, 548 | { 549 | "label": "__gap", 550 | "offset": 0, 551 | "slot": "1", 552 | "type": "t_array(t_uint256)50_storage", 553 | "contract": "ContextUpgradeable", 554 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 555 | }, 556 | { 557 | "label": "_owner", 558 | "offset": 0, 559 | "slot": "51", 560 | "type": "t_address", 561 | "contract": "OwnableUpgradeable", 562 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 563 | }, 564 | { 565 | "label": "__gap", 566 | "offset": 0, 567 | "slot": "52", 568 | "type": "t_array(t_uint256)49_storage", 569 | "contract": "OwnableUpgradeable", 570 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 571 | }, 572 | { 573 | "label": "__gap", 574 | "offset": 0, 575 | "slot": "101", 576 | "type": "t_array(t_uint256)50_storage", 577 | "contract": "ERC1967UpgradeUpgradeable", 578 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 579 | }, 580 | { 581 | "label": "__gap", 582 | "offset": 0, 583 | "slot": "151", 584 | "type": "t_array(t_uint256)50_storage", 585 | "contract": "UUPSUpgradeable", 586 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 587 | }, 588 | { 589 | "label": "value", 590 | "offset": 0, 591 | "slot": "201", 592 | "type": "t_uint256", 593 | "contract": "Box", 594 | "src": "contracts/Box.sol:9" 595 | } 596 | ], 597 | "types": { 598 | "t_address": { 599 | "label": "address", 600 | "numberOfBytes": "20" 601 | }, 602 | "t_array(t_uint256)49_storage": { 603 | "label": "uint256[49]", 604 | "numberOfBytes": "1568" 605 | }, 606 | "t_array(t_uint256)50_storage": { 607 | "label": "uint256[50]", 608 | "numberOfBytes": "1600" 609 | }, 610 | "t_bool": { 611 | "label": "bool", 612 | "numberOfBytes": "1" 613 | }, 614 | "t_uint256": { 615 | "label": "uint256", 616 | "numberOfBytes": "32" 617 | }, 618 | "t_uint8": { 619 | "label": "uint8", 620 | "numberOfBytes": "1" 621 | } 622 | } 623 | } 624 | }, 625 | "9687b798c4f2aae05632b5a7537c3cd0e96ca7b4dd9deca94e11f95f43a9f392": { 626 | "address": "0xd64948810D01c420c81Bf6F542e525473db25C39", 627 | "txHash": "0xacf3ec32edf191177680b44a84eca449299bcb2e070592ba201c1ed9a0a2b7dc", 628 | "layout": { 629 | "storage": [ 630 | { 631 | "label": "_initialized", 632 | "offset": 0, 633 | "slot": "0", 634 | "type": "t_uint8", 635 | "contract": "Initializable", 636 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 637 | "retypedFrom": "bool" 638 | }, 639 | { 640 | "label": "_initializing", 641 | "offset": 1, 642 | "slot": "0", 643 | "type": "t_bool", 644 | "contract": "Initializable", 645 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 646 | }, 647 | { 648 | "label": "__gap", 649 | "offset": 0, 650 | "slot": "1", 651 | "type": "t_array(t_uint256)50_storage", 652 | "contract": "ContextUpgradeable", 653 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 654 | }, 655 | { 656 | "label": "_owner", 657 | "offset": 0, 658 | "slot": "51", 659 | "type": "t_address", 660 | "contract": "OwnableUpgradeable", 661 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 662 | }, 663 | { 664 | "label": "__gap", 665 | "offset": 0, 666 | "slot": "52", 667 | "type": "t_array(t_uint256)49_storage", 668 | "contract": "OwnableUpgradeable", 669 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 670 | }, 671 | { 672 | "label": "__gap", 673 | "offset": 0, 674 | "slot": "101", 675 | "type": "t_array(t_uint256)50_storage", 676 | "contract": "ERC1967UpgradeUpgradeable", 677 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 678 | }, 679 | { 680 | "label": "__gap", 681 | "offset": 0, 682 | "slot": "151", 683 | "type": "t_array(t_uint256)50_storage", 684 | "contract": "UUPSUpgradeable", 685 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 686 | }, 687 | { 688 | "label": "value", 689 | "offset": 0, 690 | "slot": "201", 691 | "type": "t_uint256", 692 | "contract": "Box", 693 | "src": "contracts/Box.sol:9" 694 | } 695 | ], 696 | "types": { 697 | "t_address": { 698 | "label": "address", 699 | "numberOfBytes": "20" 700 | }, 701 | "t_array(t_uint256)49_storage": { 702 | "label": "uint256[49]", 703 | "numberOfBytes": "1568" 704 | }, 705 | "t_array(t_uint256)50_storage": { 706 | "label": "uint256[50]", 707 | "numberOfBytes": "1600" 708 | }, 709 | "t_bool": { 710 | "label": "bool", 711 | "numberOfBytes": "1" 712 | }, 713 | "t_uint256": { 714 | "label": "uint256", 715 | "numberOfBytes": "32" 716 | }, 717 | "t_uint8": { 718 | "label": "uint8", 719 | "numberOfBytes": "1" 720 | } 721 | } 722 | } 723 | }, 724 | "ca6257262f744c24ee8feb08d9685661702391de15dacc764ed9c9b3068a36b6": { 725 | "address": "0x279Aca754AF3372326BCD8488EaBEBA6fC19435d", 726 | "txHash": "0x8d6c13e1143666ab2651a6bd428e9cd2eba0689e0a93c984953a6526de270ea2", 727 | "layout": { 728 | "storage": [ 729 | { 730 | "label": "_initialized", 731 | "offset": 0, 732 | "slot": "0", 733 | "type": "t_uint8", 734 | "contract": "Initializable", 735 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 736 | "retypedFrom": "bool" 737 | }, 738 | { 739 | "label": "_initializing", 740 | "offset": 1, 741 | "slot": "0", 742 | "type": "t_bool", 743 | "contract": "Initializable", 744 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 745 | }, 746 | { 747 | "label": "__gap", 748 | "offset": 0, 749 | "slot": "1", 750 | "type": "t_array(t_uint256)50_storage", 751 | "contract": "ContextUpgradeable", 752 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 753 | }, 754 | { 755 | "label": "_owner", 756 | "offset": 0, 757 | "slot": "51", 758 | "type": "t_address", 759 | "contract": "OwnableUpgradeable", 760 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 761 | }, 762 | { 763 | "label": "__gap", 764 | "offset": 0, 765 | "slot": "52", 766 | "type": "t_array(t_uint256)49_storage", 767 | "contract": "OwnableUpgradeable", 768 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 769 | }, 770 | { 771 | "label": "__gap", 772 | "offset": 0, 773 | "slot": "101", 774 | "type": "t_array(t_uint256)50_storage", 775 | "contract": "ERC1967UpgradeUpgradeable", 776 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 777 | }, 778 | { 779 | "label": "__gap", 780 | "offset": 0, 781 | "slot": "151", 782 | "type": "t_array(t_uint256)50_storage", 783 | "contract": "UUPSUpgradeable", 784 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 785 | }, 786 | { 787 | "label": "value", 788 | "offset": 0, 789 | "slot": "201", 790 | "type": "t_uint256", 791 | "contract": "Box", 792 | "src": "contracts/Box.sol:9" 793 | } 794 | ], 795 | "types": { 796 | "t_address": { 797 | "label": "address", 798 | "numberOfBytes": "20" 799 | }, 800 | "t_array(t_uint256)49_storage": { 801 | "label": "uint256[49]", 802 | "numberOfBytes": "1568" 803 | }, 804 | "t_array(t_uint256)50_storage": { 805 | "label": "uint256[50]", 806 | "numberOfBytes": "1600" 807 | }, 808 | "t_bool": { 809 | "label": "bool", 810 | "numberOfBytes": "1" 811 | }, 812 | "t_uint256": { 813 | "label": "uint256", 814 | "numberOfBytes": "32" 815 | }, 816 | "t_uint8": { 817 | "label": "uint8", 818 | "numberOfBytes": "1" 819 | } 820 | } 821 | } 822 | }, 823 | "790d6e4d11d2a238de3e1d7ca8472e48b138d1bfda779de322b006fc72eb2684": { 824 | "address": "0x447b67C43347ae336cABe9d1C60A56dF82781e1E", 825 | "txHash": "0x15b2d53cf85c0edb085c61065b820f32213079f34fb4f0633f60ca37f52245c8", 826 | "layout": { 827 | "storage": [ 828 | { 829 | "label": "_initialized", 830 | "offset": 0, 831 | "slot": "0", 832 | "type": "t_uint8", 833 | "contract": "Initializable", 834 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 835 | "retypedFrom": "bool" 836 | }, 837 | { 838 | "label": "_initializing", 839 | "offset": 1, 840 | "slot": "0", 841 | "type": "t_bool", 842 | "contract": "Initializable", 843 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 844 | }, 845 | { 846 | "label": "__gap", 847 | "offset": 0, 848 | "slot": "1", 849 | "type": "t_array(t_uint256)50_storage", 850 | "contract": "ContextUpgradeable", 851 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 852 | }, 853 | { 854 | "label": "_owner", 855 | "offset": 0, 856 | "slot": "51", 857 | "type": "t_address", 858 | "contract": "OwnableUpgradeable", 859 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 860 | }, 861 | { 862 | "label": "__gap", 863 | "offset": 0, 864 | "slot": "52", 865 | "type": "t_array(t_uint256)49_storage", 866 | "contract": "OwnableUpgradeable", 867 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 868 | }, 869 | { 870 | "label": "__gap", 871 | "offset": 0, 872 | "slot": "101", 873 | "type": "t_array(t_uint256)50_storage", 874 | "contract": "ERC1967UpgradeUpgradeable", 875 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 876 | }, 877 | { 878 | "label": "__gap", 879 | "offset": 0, 880 | "slot": "151", 881 | "type": "t_array(t_uint256)50_storage", 882 | "contract": "UUPSUpgradeable", 883 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 884 | }, 885 | { 886 | "label": "value", 887 | "offset": 0, 888 | "slot": "201", 889 | "type": "t_uint256", 890 | "contract": "Box", 891 | "src": "contracts/Box.sol:9" 892 | } 893 | ], 894 | "types": { 895 | "t_address": { 896 | "label": "address", 897 | "numberOfBytes": "20" 898 | }, 899 | "t_array(t_uint256)49_storage": { 900 | "label": "uint256[49]", 901 | "numberOfBytes": "1568" 902 | }, 903 | "t_array(t_uint256)50_storage": { 904 | "label": "uint256[50]", 905 | "numberOfBytes": "1600" 906 | }, 907 | "t_bool": { 908 | "label": "bool", 909 | "numberOfBytes": "1" 910 | }, 911 | "t_uint256": { 912 | "label": "uint256", 913 | "numberOfBytes": "32" 914 | }, 915 | "t_uint8": { 916 | "label": "uint8", 917 | "numberOfBytes": "1" 918 | } 919 | } 920 | } 921 | }, 922 | "a37262475b13ef3ef7f6644ce72b7975032ace34bebe20a432116a89db0cbda8": { 923 | "address": "0x02C0AE8e78843B8c5389b57077EBD26632206Fe0", 924 | "txHash": "0x6e8c44fc89a95be6ee32799220dfc8ddb56c8a4946781bf963104e9c88bb212e", 925 | "layout": { 926 | "storage": [ 927 | { 928 | "label": "_initialized", 929 | "offset": 0, 930 | "slot": "0", 931 | "type": "t_uint8", 932 | "contract": "Initializable", 933 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 934 | "retypedFrom": "bool" 935 | }, 936 | { 937 | "label": "_initializing", 938 | "offset": 1, 939 | "slot": "0", 940 | "type": "t_bool", 941 | "contract": "Initializable", 942 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 943 | }, 944 | { 945 | "label": "__gap", 946 | "offset": 0, 947 | "slot": "1", 948 | "type": "t_array(t_uint256)50_storage", 949 | "contract": "ContextUpgradeable", 950 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 951 | }, 952 | { 953 | "label": "_owner", 954 | "offset": 0, 955 | "slot": "51", 956 | "type": "t_address", 957 | "contract": "OwnableUpgradeable", 958 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 959 | }, 960 | { 961 | "label": "__gap", 962 | "offset": 0, 963 | "slot": "52", 964 | "type": "t_array(t_uint256)49_storage", 965 | "contract": "OwnableUpgradeable", 966 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 967 | }, 968 | { 969 | "label": "__gap", 970 | "offset": 0, 971 | "slot": "101", 972 | "type": "t_array(t_uint256)50_storage", 973 | "contract": "ERC1967UpgradeUpgradeable", 974 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 975 | }, 976 | { 977 | "label": "__gap", 978 | "offset": 0, 979 | "slot": "151", 980 | "type": "t_array(t_uint256)50_storage", 981 | "contract": "UUPSUpgradeable", 982 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 983 | }, 984 | { 985 | "label": "value", 986 | "offset": 0, 987 | "slot": "201", 988 | "type": "t_uint256", 989 | "contract": "Box", 990 | "src": "contracts/Box.sol:9" 991 | } 992 | ], 993 | "types": { 994 | "t_address": { 995 | "label": "address", 996 | "numberOfBytes": "20" 997 | }, 998 | "t_array(t_uint256)49_storage": { 999 | "label": "uint256[49]", 1000 | "numberOfBytes": "1568" 1001 | }, 1002 | "t_array(t_uint256)50_storage": { 1003 | "label": "uint256[50]", 1004 | "numberOfBytes": "1600" 1005 | }, 1006 | "t_bool": { 1007 | "label": "bool", 1008 | "numberOfBytes": "1" 1009 | }, 1010 | "t_uint256": { 1011 | "label": "uint256", 1012 | "numberOfBytes": "32" 1013 | }, 1014 | "t_uint8": { 1015 | "label": "uint8", 1016 | "numberOfBytes": "1" 1017 | } 1018 | } 1019 | } 1020 | }, 1021 | "762567e0a6f6fa67ec73ed992f7b4502f6c75993f73dd43a3e906384ec7921e1": { 1022 | "address": "0x1300522C7103Eb5e041f85F8F7Dc3354501b1E75", 1023 | "txHash": "0x73d7bea053c682f72d3e6e901fd4b6827eef26ab5965d3d4c2a333802d0a5cd5", 1024 | "layout": { 1025 | "storage": [ 1026 | { 1027 | "label": "_initialized", 1028 | "offset": 0, 1029 | "slot": "0", 1030 | "type": "t_uint8", 1031 | "contract": "Initializable", 1032 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 1033 | "retypedFrom": "bool" 1034 | }, 1035 | { 1036 | "label": "_initializing", 1037 | "offset": 1, 1038 | "slot": "0", 1039 | "type": "t_bool", 1040 | "contract": "Initializable", 1041 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 1042 | }, 1043 | { 1044 | "label": "__gap", 1045 | "offset": 0, 1046 | "slot": "1", 1047 | "type": "t_array(t_uint256)50_storage", 1048 | "contract": "ContextUpgradeable", 1049 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 1050 | }, 1051 | { 1052 | "label": "_owner", 1053 | "offset": 0, 1054 | "slot": "51", 1055 | "type": "t_address", 1056 | "contract": "OwnableUpgradeable", 1057 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 1058 | }, 1059 | { 1060 | "label": "__gap", 1061 | "offset": 0, 1062 | "slot": "52", 1063 | "type": "t_array(t_uint256)49_storage", 1064 | "contract": "OwnableUpgradeable", 1065 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 1066 | }, 1067 | { 1068 | "label": "__gap", 1069 | "offset": 0, 1070 | "slot": "101", 1071 | "type": "t_array(t_uint256)50_storage", 1072 | "contract": "ERC1967UpgradeUpgradeable", 1073 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 1074 | }, 1075 | { 1076 | "label": "__gap", 1077 | "offset": 0, 1078 | "slot": "151", 1079 | "type": "t_array(t_uint256)50_storage", 1080 | "contract": "UUPSUpgradeable", 1081 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 1082 | }, 1083 | { 1084 | "label": "greeting", 1085 | "offset": 0, 1086 | "slot": "201", 1087 | "type": "t_string_storage", 1088 | "contract": "Greeter", 1089 | "src": "contracts/Greeter.sol:9" 1090 | } 1091 | ], 1092 | "types": { 1093 | "t_address": { 1094 | "label": "address", 1095 | "numberOfBytes": "20" 1096 | }, 1097 | "t_array(t_uint256)49_storage": { 1098 | "label": "uint256[49]", 1099 | "numberOfBytes": "1568" 1100 | }, 1101 | "t_array(t_uint256)50_storage": { 1102 | "label": "uint256[50]", 1103 | "numberOfBytes": "1600" 1104 | }, 1105 | "t_bool": { 1106 | "label": "bool", 1107 | "numberOfBytes": "1" 1108 | }, 1109 | "t_string_storage": { 1110 | "label": "string", 1111 | "numberOfBytes": "32" 1112 | }, 1113 | "t_uint256": { 1114 | "label": "uint256", 1115 | "numberOfBytes": "32" 1116 | }, 1117 | "t_uint8": { 1118 | "label": "uint8", 1119 | "numberOfBytes": "1" 1120 | } 1121 | } 1122 | } 1123 | }, 1124 | "349e256c5bb8d3aa040a1e667c32324e7530e2f07cff1ce164cb9b0c75e68de6": { 1125 | "address": "0xD4f953b3161C40C20420AdE1b0739644FA36A131", 1126 | "txHash": "0xd727251b1a137cb963b14d815d3364b3ed7d40f46894288793b4b60ef7c30292", 1127 | "layout": { 1128 | "storage": [ 1129 | { 1130 | "label": "_initialized", 1131 | "offset": 0, 1132 | "slot": "0", 1133 | "type": "t_uint8", 1134 | "contract": "Initializable", 1135 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", 1136 | "retypedFrom": "bool" 1137 | }, 1138 | { 1139 | "label": "_initializing", 1140 | "offset": 1, 1141 | "slot": "0", 1142 | "type": "t_bool", 1143 | "contract": "Initializable", 1144 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" 1145 | }, 1146 | { 1147 | "label": "__gap", 1148 | "offset": 0, 1149 | "slot": "1", 1150 | "type": "t_array(t_uint256)50_storage", 1151 | "contract": "ContextUpgradeable", 1152 | "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" 1153 | }, 1154 | { 1155 | "label": "_owner", 1156 | "offset": 0, 1157 | "slot": "51", 1158 | "type": "t_address", 1159 | "contract": "OwnableUpgradeable", 1160 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" 1161 | }, 1162 | { 1163 | "label": "__gap", 1164 | "offset": 0, 1165 | "slot": "52", 1166 | "type": "t_array(t_uint256)49_storage", 1167 | "contract": "OwnableUpgradeable", 1168 | "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" 1169 | }, 1170 | { 1171 | "label": "__gap", 1172 | "offset": 0, 1173 | "slot": "101", 1174 | "type": "t_array(t_uint256)50_storage", 1175 | "contract": "ERC1967UpgradeUpgradeable", 1176 | "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" 1177 | }, 1178 | { 1179 | "label": "__gap", 1180 | "offset": 0, 1181 | "slot": "151", 1182 | "type": "t_array(t_uint256)50_storage", 1183 | "contract": "UUPSUpgradeable", 1184 | "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" 1185 | }, 1186 | { 1187 | "label": "greeting", 1188 | "offset": 0, 1189 | "slot": "201", 1190 | "type": "t_string_storage", 1191 | "contract": "Greeter", 1192 | "src": "contracts/Greeter.sol:9" 1193 | } 1194 | ], 1195 | "types": { 1196 | "t_address": { 1197 | "label": "address", 1198 | "numberOfBytes": "20" 1199 | }, 1200 | "t_array(t_uint256)49_storage": { 1201 | "label": "uint256[49]", 1202 | "numberOfBytes": "1568" 1203 | }, 1204 | "t_array(t_uint256)50_storage": { 1205 | "label": "uint256[50]", 1206 | "numberOfBytes": "1600" 1207 | }, 1208 | "t_bool": { 1209 | "label": "bool", 1210 | "numberOfBytes": "1" 1211 | }, 1212 | "t_string_storage": { 1213 | "label": "string", 1214 | "numberOfBytes": "32" 1215 | }, 1216 | "t_uint256": { 1217 | "label": "uint256", 1218 | "numberOfBytes": "32" 1219 | }, 1220 | "t_uint8": { 1221 | "label": "uint8", 1222 | "numberOfBytes": "1" 1223 | } 1224 | } 1225 | } 1226 | } 1227 | } 1228 | } 1229 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Santiago Palladino 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample contract deployment pipeline 2 | 3 | This is a proof of concept for a smart contract deployment pipeline using Github Actions. The pipeline is composed a set of build, test, deployment, verification, and post-deploy configurable Hardhat scripts. 4 | 5 | ![Workflow diagram](imgs/workflow-diagram.png) 6 | 7 | ## Why? 8 | 9 | Unlike traditional software deployments, contract deployments are usually run by a developer from their local workstation. This is not just a hassle for the developer, but it also hinders transparency and reproducibility of deployments. By moving them to a publicly auditable deployment pipeline, anyone can follow the trace from the source code to a deployed address. 10 | 11 | This is especially valuable for protocol stakeholders (from signers to community members) who need to review the deployment or upgrade to a specific version of the code. A stakeholder needs to know if the code deployed matches a specific version, often described by a git tag or commit, that has already been reviewed or even audited. Having a public compilation and deployment pipeline makes it easy to check that the bytecode deployed at a specific address was generated out of a given version of the source code. 12 | 13 | ## Example 14 | 15 | The following release spec in a pull request from branch `release/1.3` will compile all contracts, run tests, compare the source deployed versus the audited one, deploy a new implementation for the `Box` contract, verify its source code and binary, and create a new proposal for upgrading it. The workflow optionally pauses before sending the deployment transactions and creating the proposal and requires a manual review before proceeding. 16 | 17 | ```yaml 18 | title: Upgrade to v1.3 19 | network: goerli 20 | deploy: prepare-upgrade Box 21 | verify: verify-deployed 22 | finish: propose-upgrade 23 | audited: f37fb0c75b4ec2c13e063d257499e7dcc643e468 24 | description: | 25 | Upgrades Box contract to v1.3. 26 | Bumps version identifier in the contract. 27 | ``` 28 | 29 | ### Autogenerated summary 30 | 31 | The deployment workflow autogenerates a [summary](https://github.com/spalladino/sample-contract-deploy-pipeline/actions/runs/3559747788) with deployment info and relevant links. It will also alert if there is a diff in the contracts between the audited commit and the deployed one. 32 | 33 | > #### Upgrade to v1.3 34 | > 35 | > **Network:** goerli 36 | > 37 | > **Commit:** [`8aa72d2b03db9015dcb74f65515840677e23357a`](https://github.com/spalladino/test-hardhat-project/tree/8aa72d2b03db9015dcb74f65515840677e23357a) 38 | > 39 | > Upgrades Box contract to v1.3. Bumps version identifier in the contract. 40 | > 41 | > #### Audit 42 | > 43 | > Audited contracts at commit [`f37fb0c7`](https://github.com/spalladino/test-hardhat-project/tree/f37fb0c75b4ec2c13e063d257499e7dcc643e468). 44 | > Contracts have been modified since audit :warning: 45 | > 46 | > ```diff 47 | > diff --git a/contracts/Box.sol b/contracts/Box.sol 48 | > index 1fa3762..bfedd5d 100644 49 | > --- a/contracts/Box.sol 50 | > +++ b/contracts/Box.sol 51 | > @@ -25,7 +25,7 @@ contract Box is Initializable, OwnableUpgradeable, UUPSUpgradeable { 52 | > } 53 | > 54 | > function version() external pure returns (string memory) { 55 | > - return "v3"; 56 | > + return "v1.3.7"; 57 | > } 58 | > 59 | > function _authorizeUpgrade(address newImplementation) 60 | > ``` 61 | > 62 | > #### Implementation contracts deployed 63 | > 64 | > - Box at [`0x02C0AE8e78843B8c5389b57077EBD26632206Fe0`](https://goerli.etherscan.io/address/0x02C0AE8e78843B8c5389b57077EBD26632206Fe0) 65 | 66 | ### Manual review 67 | 68 | To prevent accidental expensive deployments, the workflow pauses and requires a manual review from a set of designated team members before proceeding. The same happens before creating the upgrade proposal, so the development team can review the deployment before passing responsibility to signers. 69 | 70 | 71 | 72 | ### Upgrade proposal 73 | 74 | The upgrade proposal is created in OpenZeppelin Defender: 75 | 76 | 77 | 78 | And includes bytecode verification to close the loop with the deployed artifact: 79 | 80 | 81 | 82 | ## How to use 83 | 84 | To trigger a new release, create a new folder with the version identifier `vX.Y(.Z)` in the `releases` folder, with an `index.yml` file with the spec of the release. This includes: 85 | 86 | - `title`: Title of the release 87 | - `description`: Description of what this release is about 88 | - `network`: The network where to deploy, needs to be defined in the hardhat config file 89 | - `audited`: Optional commit in which the code was audited, used to show the diff with the deployed version 90 | - `deploy`: Deployment command to execute 91 | - `verify`: Verification command to execute 92 | - `finish`: Wrap-up command to execute 93 | 94 | All commands are passed to `yarn hardhat`, so any hardhat task can be used. For simplicity, some tasks are already defined: 95 | 96 | - `deploy-proxy CONTRACT ARG1 ARG2...`: Deploys a contract as upgradeable and initializes it with ARGs 97 | - `prepare-upgrade CONTRACT1 CONTRACT2`: Deploys new implementations for all CONTRACTs 98 | - `verify-deployed`: Reads deployed contracts and verifies [source code in Etherscan](https://etherscan.io/verifyContract) and [artifacts in Defender](https://docs.openzeppelin.com/defender/admin#bytecode-verification) 99 | - `propose-upgrade`: Creates a [batch proposal](https://docs.openzeppelin.com/defender/admin#batches) to simultaneously upgrade all contracts via a multisig in Defender 100 | 101 | Alternatively, custom scripts can be used by passing them through hardhat's `run` command. This allows for more complex deployment scripts to be run. See [v1.4/deploy.ts](releases/v1.4/deploy.ts) for an example. 102 | 103 | Once the release is defined, create a pull request from the `release/X.Y(.Z)` branch into master. This will trigger the [release](.github/workflows/release.yml) workflow, that includes the build, test, deployment, verification, and post-deploy jobs. 104 | 105 | ## How it works 106 | 107 | The release workflow is triggered by pull requests from `release/` branches, and runs on the release branch, not on the merge commit. Each stage in the workflow is set up as a sequential job, which can be mapped to common hardhat tasks or custom scripts. 108 | 109 | ### Build 110 | 111 | An initial build job compiles contracts and saves them as artifacts for the following jobs. No other job recompiles contracts, to ensure the same bytecode is preserved throughout the workflow. 112 | 113 | ### Test 114 | 115 | Unit tests are run on the contracts to be released, using `hardhat test`. Other contract test runners could be used as well. 116 | 117 | ### Prepare 118 | 119 | A preparation step parses release info from the user-defined yaml, and outputs an initial release summary. Any diffs with the audited commit are shown here, so the deployment can be halted if needed. 120 | 121 | ### Deploy 122 | 123 | The deployment job requires manual approval to run (see _environments_ below), and uses the defined `PRIVATE_KEY` to deploy contracts. Rationale for using a low-security private key is that the deployer account should not have any privileges in the system, and the only risk of leaking it is losing the funds for paying deployment fees. Still, a managed solution such as a [Defender Relay](https://docs.openzeppelin.com/defender/relay) could be used instead. 124 | 125 | In this example, the [Hardhat Upgrades](https://docs.openzeppelin.com/upgrades-plugins/1.x/) plugin is used for deploying contracts, but other deployment solutions could be used as well. 126 | 127 | The deployment script should output the deployed contracts to a `deployed.json` file in the release folder, and update a global `addresses.json` registry. The provided `deploy-proxy` and `prepare-upgrade` tasks both do this. Once finished, it commits those changes into the release branch, so once merged, the latest deployment info is kept on the main branch. 128 | 129 | ### Verify 130 | 131 | The verification step is meant to verify source code and artifacts. The provided `verify-deployed` task will read newly deployed contracts out of the `deployed.json` file, and verify source on Etherscan and bytecode on Defender. Note that this job can fail, yet the post-deploy step runs anyway. Verification is separate from deployment so it can be re-run if needed. 132 | 133 | ### Post-deploy 134 | 135 | The post-deploy job is intended to cover any actions that are meant to be run after the deployment. It requires manual approval as well, so a reviewer can check if the deployment went according to plan before triggering it. 136 | 137 | The main use case for this step is the `propose-upgrade` task, which creates a new batch multisig proposal to upgrade all contracts for which a new implementation was deployed. 138 | 139 | ## Setup 140 | 141 | To set up a similar flow in another repository, besides the workflow file and hardhat tasks, secrets and environments need to be configured. 142 | 143 | ### Secrets 144 | 145 | The workflow requires a few secrets to be set in order to run, such as: 146 | 147 | - `INFURA_PROJECT_ID` 148 | - `ETHERSCAN_API_KEY` 149 | - `DEFENDER_API_KEY` 150 | - `DEFENDER_API_SECRET` 151 | - `PRIVATE_KEY` 152 | 153 | ### Environments 154 | 155 | Critical steps, namely deployment and post-deploy, **require manual approval** before running. This gives the reviewer a chance to check everything is in place before actually deploying contracts, and to go through the deployment before requesting the multisig signers to approve. This is implemented by defining a Github environment that requires manual approval. 156 | 157 | ## License 158 | 159 | All code in this repository is released under the [MIT License](LICENSE). 160 | 161 | -------------------------------------------------------------------------------- /addresses.json: -------------------------------------------------------------------------------- 1 | { 2 | "5": { 3 | "Box": { 4 | "address": "0x22431A9012cC3Fa9Ba7643e16275e8565Ff3a0BA", 5 | "implementation": "0x02C0AE8e78843B8c5389b57077EBD26632206Fe0" 6 | }, 7 | "Greeter": { 8 | "address": "0x2dE4b51615E2d732905131e90201ca362F1789Fb", 9 | "implementation": "0xD4f953b3161C40C20420AdE1b0739644FA36A131" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /contracts/Box.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: WTFPL 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 7 | 8 | contract Box is Initializable, OwnableUpgradeable, UUPSUpgradeable { 9 | uint256 public value; 10 | 11 | /// @custom:oz-upgrades-unsafe-allow constructor 12 | constructor() { 13 | _disableInitializers(); 14 | } 15 | 16 | function initialize(uint256 initialValue, address initialOwner) initializer public { 17 | __Ownable_init(); 18 | __UUPSUpgradeable_init(); 19 | _transferOwnership(initialOwner); 20 | value = initialValue; 21 | } 22 | 23 | function setValue(uint256 newValue) external { 24 | value = newValue; 25 | } 26 | 27 | function version() external pure returns (string memory) { 28 | return "v1.3.7"; 29 | } 30 | 31 | function _authorizeUpgrade(address newImplementation) 32 | internal 33 | onlyOwner 34 | override 35 | {} 36 | 37 | } -------------------------------------------------------------------------------- /contracts/Greeter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: WTFPL 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 7 | 8 | contract Greeter is Initializable, OwnableUpgradeable, UUPSUpgradeable { 9 | string internal greeting; 10 | 11 | /// @custom:oz-upgrades-unsafe-allow constructor 12 | constructor() { 13 | _disableInitializers(); 14 | } 15 | 16 | function initialize(string memory initialGreeting, address initialOwner) initializer public { 17 | __Ownable_init(); 18 | __UUPSUpgradeable_init(); 19 | _transferOwnership(initialOwner); 20 | greeting = initialGreeting; 21 | } 22 | 23 | function setGreeting(string memory newValue) external onlyOwner { 24 | greeting = newValue; 25 | } 26 | 27 | function version() external pure returns (string memory) { 28 | return "v1.4.1"; 29 | } 30 | 31 | function greet() external view returns (string memory) { 32 | return greeting; 33 | } 34 | 35 | function _authorizeUpgrade(address newImplementation) 36 | internal 37 | onlyOwner 38 | override 39 | {} 40 | 41 | } -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | import '@nomiclabs/hardhat-ethers'; 4 | import '@nomiclabs/hardhat-etherscan'; 5 | import '@openzeppelin/hardhat-upgrades'; 6 | import '@openzeppelin/hardhat-defender'; 7 | 8 | import './tasks/prepare-upgrade'; 9 | import './tasks/verify-deployed'; 10 | import './tasks/propose-upgrade'; 11 | import './tasks/deploy-proxy'; 12 | import './tasks/noop'; 13 | 14 | import { HardhatUserConfig } from "hardhat/config"; 15 | 16 | console.log(process.env.ETHERSCAN_API_KEY); 17 | 18 | const config: HardhatUserConfig = { 19 | solidity: "0.8.9", 20 | defender: { 21 | apiKey: process.env.DEFENDER_API_KEY!, 22 | apiSecret: process.env.DEFENDER_API_SECRET!, 23 | }, 24 | etherscan: { 25 | apiKey: process.env.ETHERSCAN_API_KEY, 26 | }, 27 | networks: { 28 | goerli: { 29 | url: `https://goerli.infura.io/v3/${process.env.INFURA_PROJECT_ID}`, 30 | accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], 31 | }, 32 | } 33 | } as HardhatUserConfig; 34 | 35 | export default config; -------------------------------------------------------------------------------- /imgs/awaiting-review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spalladino/sample-contract-deploy-pipeline/25e549684d32de3aaa90ff54d4030b72810cbbe5/imgs/awaiting-review.png -------------------------------------------------------------------------------- /imgs/sample-artifact-verified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spalladino/sample-contract-deploy-pipeline/25e549684d32de3aaa90ff54d4030b72810cbbe5/imgs/sample-artifact-verified.png -------------------------------------------------------------------------------- /imgs/sample-proposal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spalladino/sample-contract-deploy-pipeline/25e549684d32de3aaa90ff54d4030b72810cbbe5/imgs/sample-proposal.png -------------------------------------------------------------------------------- /imgs/sample-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spalladino/sample-contract-deploy-pipeline/25e549684d32de3aaa90ff54d4030b72810cbbe5/imgs/sample-summary.png -------------------------------------------------------------------------------- /imgs/workflow-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spalladino/sample-contract-deploy-pipeline/25e549684d32de3aaa90ff54d4030b72810cbbe5/imgs/workflow-diagram.png -------------------------------------------------------------------------------- /multisigs.json: -------------------------------------------------------------------------------- 1 | { 2 | "goerli": "0x07a93B04A0E019Cf70025ff3a84943F695B5B606" 3 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-hardhat-project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "yarn hardhat compile", 8 | "test": "yarn hardhat test" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@nomiclabs/hardhat-ethers": "^2.1.1", 15 | "@nomiclabs/hardhat-etherscan": "^3.1.2", 16 | "@openzeppelin/contracts": "^4.7.3", 17 | "@openzeppelin/hardhat-defender": "^1.8.1", 18 | "@openzeppelin/hardhat-upgrades": "^1.20.0", 19 | "@tsconfig/node14": "^1.0.3", 20 | "@types/debug": "^4.1.7", 21 | "@types/lodash": "^4.14.190", 22 | "@types/proper-lockfile": "^4.1.2", 23 | "chai": "^4.3.7", 24 | "defender-admin-client": "^1.37.0", 25 | "defender-base-client": "^1.37.0", 26 | "dotenv": "^16.0.1", 27 | "ethers": "^5.7.0", 28 | "hardhat": "^2.10.2", 29 | "lodash": "^4.17.21", 30 | "mocha": "^10.1.0", 31 | "promise-retry": "^2.0.1", 32 | "ts-node": "^10.9.1", 33 | "typescript": "^4.9.3", 34 | "yaml": "^2.1.3" 35 | }, 36 | "dependencies": { 37 | "@openzeppelin/contracts-upgradeable": "^4.7.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /releases/v1.3/deployed.json: -------------------------------------------------------------------------------- 1 | { 2 | "Box": { 3 | "implementation": "0x02C0AE8e78843B8c5389b57077EBD26632206Fe0" 4 | } 5 | } -------------------------------------------------------------------------------- /releases/v1.3/index.yml: -------------------------------------------------------------------------------- 1 | title: Upgrade to v1.3 2 | network: goerli 3 | deploy: prepare-upgrade Box 4 | verify: verify-deployed 5 | finish: propose-upgrade 6 | audited: f37fb0c75b4ec2c13e063d257499e7dcc643e468 7 | description: | 8 | Upgrades Box contract to v1.3. 9 | Bumps version identifier in the contract. -------------------------------------------------------------------------------- /releases/v1.4/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers, upgrades } from 'hardhat'; 2 | import { writeDeploy } from '../../tasks/utils'; 3 | import { appendFileSync } from 'fs'; 4 | 5 | async function main() { 6 | const owner = process.env.MULTISIG_ADDRESS; 7 | const summaryPath = process.env.GITHUB_STEP_SUMMARY; 8 | const chainId = await ethers.provider.getNetwork().then(n => n.chainId); 9 | const factory = await ethers.getContractFactory('Greeter'); 10 | const instance = await upgrades.deployProxy(factory, ['Hello world', owner], { kind: 'uups' }).then(d => d.deployed()); 11 | const implementation = await upgrades.erc1967.getImplementationAddress(instance.address); 12 | 13 | console.error(`Deployed Greeter at ${instance.address} with implementation ${implementation}`); 14 | writeDeploy(chainId, 'Greeter', { address: instance.address, implementation }); 15 | 16 | if (summaryPath) { 17 | appendFileSync(summaryPath, `## Contract deployed\n\n- Greeter at \`${instance.address}\` with implementation \`${implementation}\``); 18 | } 19 | } 20 | 21 | main().catch((err: any) => { console.error(err); process.exit(1); }); -------------------------------------------------------------------------------- /releases/v1.4/deployed.json: -------------------------------------------------------------------------------- 1 | { 2 | "Greeter": { 3 | "address": "0x2dE4b51615E2d732905131e90201ca362F1789Fb", 4 | "implementation": "0xD4f953b3161C40C20420AdE1b0739644FA36A131" 5 | } 6 | } -------------------------------------------------------------------------------- /releases/v1.4/index.yml: -------------------------------------------------------------------------------- 1 | title: Deploy Greeter 2 | network: goerli 3 | deploy: deploy-proxy Greeter HelloWorld $MULTISIG_ADDRESS 4 | verify: verify-deployed 5 | description: | 6 | Create upgradeable Greeter contract. 7 | A new contract that can greet. -------------------------------------------------------------------------------- /tasks/deploy-proxy.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { appendFileSync } from 'fs'; 3 | import { task, types } from 'hardhat/config'; 4 | import { HardhatRuntimeEnvironment as HRE } from 'hardhat/types'; 5 | import { getReleaseDeploys, writeDeploy } from './utils'; 6 | 7 | const summaryPath = process.env.GITHUB_STEP_SUMMARY; 8 | 9 | function getEtherscanDomain(hre: HRE) { 10 | const network = hre.network.name; 11 | if (network === `mainnet`) return 'etherscan.io'; 12 | return `${network}.etherscan.io`; 13 | } 14 | 15 | async function deploy(hre: HRE, chainId: number, contract: string, args?: string[]) { 16 | console.error(`Deploying contract ${contract}`); 17 | const { upgrades, ethers } = hre; 18 | 19 | const factory = await ethers.getContractFactory(contract); 20 | const instance = await upgrades.deployProxy(factory, args ?? [], { kind: 'uups' }).then(d => d.deployed()); 21 | const implementation = await upgrades.erc1967.getImplementationAddress(instance.address); 22 | 23 | console.error(` Deployed ${contract} at ${instance.address} with implementation ${implementation}`); 24 | writeDeploy(chainId, contract, { address: instance.address, implementation }); 25 | 26 | return implementation; 27 | } 28 | 29 | async function main(args: { contract: string, initArgs: string[] }, hre: HRE) { 30 | const { contract, initArgs } = args; 31 | const { ethers } = hre; 32 | 33 | const commit = execSync(`/usr/bin/git log -1 --format='%H'`).toString().trim(); 34 | const chainId = await ethers.provider.getNetwork().then(n => n.chainId); 35 | console.error(`Deploying contract ${contract} from commit ${commit} on chain ${chainId}`); 36 | 37 | try { 38 | await deploy(hre, chainId, contract, initArgs); 39 | } finally { 40 | const deployed = getReleaseDeploys(); 41 | if (summaryPath && deployed && Object.entries(deployed).length > 0) { 42 | const list = Object.entries(deployed).map(([name, info]) => `- ${name} at [\`${info.address}\`](https://${getEtherscanDomain(hre)}/address/${info.address}) with implementation at [\`${info.implementation}\`](https://${getEtherscanDomain(hre)}/address/${info.implementation})`); 43 | appendFileSync(summaryPath, `## Contract deployed\n\n${list.join('\n')}\n`); 44 | } 45 | } 46 | } 47 | 48 | task('deploy-proxy') 49 | .addPositionalParam('contract', 'Name of the contract to deploy as upgradeable', [], types.string) 50 | .addOptionalVariadicPositionalParam('initArgs', 'Initializer arguments (if any)', []) 51 | .setDescription('Deploys new contract as upgradeable') 52 | .setAction(main); -------------------------------------------------------------------------------- /tasks/noop.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | 3 | task('noop') 4 | .setDescription('No action') 5 | .setAction(() => Promise.resolve()); -------------------------------------------------------------------------------- /tasks/prepare-upgrade.ts: -------------------------------------------------------------------------------- 1 | import { appendFileSync } from 'fs'; 2 | import { execSync } from 'child_process'; 3 | import { task, types } from 'hardhat/config'; 4 | import { getAddressBookEntry, writeDeploy, getReleaseDeploys } from './utils'; 5 | import { HardhatRuntimeEnvironment as HRE } from 'hardhat/types'; 6 | import { DeployImplementationResponse } from '@openzeppelin/hardhat-upgrades/src/deploy-implementation'; 7 | import { getContractAddress } from '@ethersproject/address'; 8 | 9 | const summaryPath = process.env.GITHUB_STEP_SUMMARY; 10 | 11 | function getEtherscanDomain(hre: HRE) { 12 | const network = hre.network.name; 13 | if (network === `mainnet`) return 'etherscan.io'; 14 | return `${network}.etherscan.io`; 15 | } 16 | 17 | function getNewImplementation(prepareUpgradeResult: DeployImplementationResponse): string { 18 | return typeof prepareUpgradeResult === 'string' 19 | ? prepareUpgradeResult 20 | : getContractAddress(prepareUpgradeResult); 21 | } 22 | 23 | async function prepareUpgrade(hre: HRE, chainId: number, contract: string) { 24 | console.error(`Deploying new implementation for contract ${contract}`); 25 | const { upgrades, ethers } = hre; 26 | 27 | const info = getAddressBookEntry(chainId, contract); 28 | console.error(` Proxy for ${contract} at ${info.address}`) 29 | 30 | const factory = await ethers.getContractFactory(contract); 31 | const result = await upgrades.prepareUpgrade(info.address, factory, { kind: 'uups' }); 32 | const implementation = getNewImplementation(result); 33 | 34 | console.error(` Deployed new implementation for ${contract} at ${implementation}`); 35 | writeDeploy(chainId, contract, { implementation }); 36 | 37 | return implementation; 38 | } 39 | 40 | async function main(args: { contracts: string[] }, hre: HRE) { 41 | const { contracts } = args; 42 | const { ethers } = hre; 43 | if (contracts.length === 0) return; 44 | 45 | const commit = execSync(`/usr/bin/git log -1 --format='%H'`).toString().trim(); 46 | const chainId = await ethers.provider.getNetwork().then(n => n.chainId); 47 | console.error(`Deploying implementation contracts ${contracts.join(', ')} from commit ${commit} on chain ${chainId}`); 48 | 49 | try { 50 | for (const contract of contracts) { 51 | await prepareUpgrade(hre, chainId, contract); 52 | } 53 | } finally { 54 | const deployed = getReleaseDeploys(); 55 | if (summaryPath && deployed && Object.entries(deployed).length > 0) { 56 | const list = Object.entries(deployed).map(([name, info]) => `- ${name} at [\`${info.implementation}\`](https://${getEtherscanDomain(hre)}/address/${info.implementation})`); 57 | appendFileSync(summaryPath, `## Implementation contracts deployed\n\n${list.join('\n')}\n`); 58 | } 59 | } 60 | } 61 | 62 | task('prepare-upgrade') 63 | .addVariadicPositionalParam('contracts', 'Names of the contracts to deploy as new implementations', [], types.string) 64 | .setDescription('Deploys new implementation contracts') 65 | .setAction(main); -------------------------------------------------------------------------------- /tasks/propose-upgrade.ts: -------------------------------------------------------------------------------- 1 | import { AdminClient } from 'defender-admin-client'; 2 | import { fromChainId } from 'defender-base-client'; 3 | import { writeFileSync } from 'fs'; 4 | import { task } from 'hardhat/config'; 5 | import { HardhatRuntimeEnvironment as HRE } from 'hardhat/types'; 6 | import { pickBy } from 'lodash'; 7 | import { getAddressBookEntry, getReleaseDeploys, getReleaseInfo, toEIP3770 } from './utils'; 8 | 9 | const proxyAbi = [{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"}],"name":"upgradeTo","outputs":[],"stateMutability":"nonpayable","type":"function"}]; 10 | const summaryPath = process.env.GITHUB_STEP_SUMMARY; 11 | 12 | async function main(args: { multisig?: string, referenceUrl?: string }, hre: HRE) { 13 | const { ethers, config } = hre; 14 | const referenceUrl = args.referenceUrl || process.env.ARTIFACT_REFERENCE_URL 15 | const multisig = args.multisig || process.env.MULTISIG_ADDRESS!; 16 | const chainId = await ethers.provider.getNetwork().then(n => n.chainId); 17 | const network = fromChainId(chainId)!; 18 | const deployed = pickBy(getReleaseDeploys(), c => !!c.implementation); 19 | 20 | console.error(`Creating proposal for upgrade in Defender for contracts ${Object.keys(deployed).join(', ')}`); 21 | const defenderAdmin = new AdminClient(config.defender!); 22 | 23 | const contracts = await Promise.all(Object.entries(deployed).map(async ([name, { implementation }]) => ({ 24 | name, 25 | network, 26 | address: getAddressBookEntry(chainId, name).address, 27 | abi: JSON.stringify([...await hre.artifacts.readArtifact(name).then(a => a.abi), ...proxyAbi]), 28 | newImplementation: implementation!, 29 | }))); 30 | 31 | console.error(`Contracts:\n${contracts.map(c => `- ${c.name} at ${c.address} to ${c.newImplementation}`).join('\n')}`); 32 | 33 | const steps = contracts.map(({ address, network, newImplementation }) => ({ 34 | contractId: `${network}-${address}`, 35 | targetFunction: proxyAbi[0], 36 | functionInputs: [newImplementation], 37 | type: 'custom' as const, 38 | })); 39 | 40 | console.error(`Steps:\n`, JSON.stringify(steps, null, 2)); 41 | 42 | const releaseInfo = getReleaseInfo() || {}; 43 | const title = releaseInfo['title'] || 'Upgrade'; 44 | 45 | let description = releaseInfo['description'] || contracts.map(c => `${c.name} at ${c.address} to ${c.newImplementation}.`).join('\n'); 46 | if (referenceUrl) description += `\n${referenceUrl}`; 47 | 48 | const proposal = await defenderAdmin.createProposal({ 49 | contract: contracts, 50 | title, 51 | description, 52 | type: 'batch', 53 | via: multisig, 54 | viaType: 'Gnosis Safe', 55 | metadata: {}, 56 | steps, 57 | }) 58 | 59 | console.error(`Created upgrade proposal for multisig ${multisig} at ${proposal.url}`); 60 | 61 | if (summaryPath) { 62 | const multisigLink = `https://app.safe.global/${toEIP3770(chainId, multisig)}/home`; 63 | writeFileSync(summaryPath, `## Approval\n\n[Approval required](${proposal.url}) by multisig [\`${multisig}\`](${multisigLink}) signers.`); 64 | } 65 | } 66 | 67 | task('propose-upgrade') 68 | .addOptionalParam('referenceUrl', 'Link to the deployment pipeline run (defaults to $ARTIFACT_REFERENCE_URL)') 69 | .addOptionalParam('multisig', 'Address of the multisig that needs to approve the upgrade (defaults to $MULTISIG_ADDRESS if env var is set)') 70 | .setDescription('Proposes a system upgrade as a batch in Defender Admin') 71 | .setAction(main); -------------------------------------------------------------------------------- /tasks/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync, existsSync } from 'fs'; 2 | import { parse as parseYaml } from 'yaml'; 3 | import chainsMini from './chains_mini.json'; 4 | 5 | const RELEASE_PATH = process.env.RELEASE_PATH; 6 | const DEPLOYED_PATH = RELEASE_PATH ? `${RELEASE_PATH}/deployed.json` : null; 7 | const RELEASE_INFO_PATH = RELEASE_PATH ? `${RELEASE_PATH}/index.yml` : null; 8 | const ADDRESS_BOOK_PATH = `addresses.json`; 9 | 10 | export type ContractDeploymentInfo = { 11 | address: string; 12 | implementation?: string; 13 | } 14 | 15 | export function getAddressBookEntry(chainId: number, contract: string): ContractDeploymentInfo { 16 | const addressBook = JSON.parse(readFileSync(ADDRESS_BOOK_PATH).toString()); 17 | const info = (addressBook[chainId] || {})[contract]; 18 | if (!info) throw new Error(`Could not find info for ${contract} on chain ${chainId}`); 19 | return info; 20 | } 21 | 22 | export function getReleaseDeploys(): Record | undefined { 23 | if (DEPLOYED_PATH) { 24 | return JSON.parse(readFileSync(DEPLOYED_PATH).toString()); 25 | } 26 | } 27 | 28 | export function getReleaseInfo() { 29 | if (RELEASE_INFO_PATH) { 30 | return parseYaml(readFileSync(RELEASE_INFO_PATH).toString()); 31 | } 32 | } 33 | 34 | export function writeDeploy(chainId: number, name: string, data: Partial) { 35 | if (DEPLOYED_PATH) { 36 | const deployed = existsSync(DEPLOYED_PATH) ? JSON.parse(readFileSync(DEPLOYED_PATH).toString()) : {}; 37 | deployed[name] = { ...deployed[name], ... data }; 38 | writeFileSync(DEPLOYED_PATH, JSON.stringify(deployed, null, 2)); 39 | } 40 | 41 | if (ADDRESS_BOOK_PATH) { 42 | const addressBook = JSON.parse(readFileSync(ADDRESS_BOOK_PATH).toString()); 43 | if (!addressBook[chainId]) addressBook[chainId] = {}; 44 | addressBook[chainId][name] = { ...addressBook[chainId][name], ...data }; 45 | writeFileSync(ADDRESS_BOOK_PATH, JSON.stringify(addressBook, null, 2)); 46 | } 47 | } 48 | 49 | export function toEIP3770(chainId: number, address: string): string { 50 | const network = chainsMini.find(c => c.chainId === chainId); 51 | if (!network) throw new Error(`Network ${chainId} not found`); 52 | return `${network.shortName}:${address}`; 53 | } -------------------------------------------------------------------------------- /tasks/verify-deployed.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { task } from 'hardhat/config'; 3 | import { HardhatRuntimeEnvironment as HRE } from 'hardhat/types'; 4 | import { getReleaseDeploys } from './utils'; 5 | 6 | async function main(args: { referenceUrl?: string }, hre: HRE) { 7 | const workflowUrl = args.referenceUrl || process.env.ARTIFACT_REFERENCE_URL || execSync(`git config --get remote.origin.url`).toString().trim(); 8 | const deployed = getReleaseDeploys() || {}; 9 | 10 | const { defender } = hre; 11 | const errs = []; 12 | 13 | // On Etherscan, we verify the proxy address, since the Defender upgrades plugin 14 | // will automatically verify both proxy and implementation. However, if we only 15 | // deployed an implementation, we want to verify it as well. 16 | for (const [name, { address, implementation }] of Object.entries(deployed)) { 17 | const addressToVerify = address || implementation; 18 | console.error(`\nVerifying source for ${name} at ${addressToVerify} on Etherscan`); 19 | try { 20 | await hre.run("verify:verify", { address: addressToVerify, noCompile: true }); 21 | } catch(err: any) { 22 | if (err.message === 'Contract source code already verified') { 23 | console.error(`Source code already verified`); 24 | } else { 25 | console.error(`Error verifying source code: ${err.message}`); 26 | errs.push([name, err]); 27 | } 28 | } 29 | } 30 | 31 | // On Defender, we only care about implementation contracts for verifying bytecode. 32 | for (const [name, { address, implementation }] of Object.entries(deployed)) { 33 | const addressToVerify = implementation || address; 34 | console.error(`\nVerifying artifact for ${name} at ${addressToVerify} on Defender`); 35 | try { 36 | const response = await defender.verifyDeployment(addressToVerify, name, workflowUrl); 37 | console.error(`Bytecode match for ${name} is ${response.matchType}`); 38 | } catch (err: any) { 39 | console.error(`Error verifying artifact: ${err.message}`); 40 | errs.push([name, err]); 41 | } 42 | } 43 | 44 | if (errs.length > 0) { 45 | throw new Error(`Some verifications failed:\n${errs.map(([name, err]) => `${name}: ${err.message}`)}`); 46 | } 47 | } 48 | 49 | task('verify-deployed') 50 | .addOptionalParam('referenceUrl', 'URL to link to for artifact verification (defaults to $ARTIFACT_REFERENCE_URL the remote.origin.url of the repository)') 51 | .setDescription('Verifies deployed implementations in Etherscan and Defender') 52 | .setAction(main); -------------------------------------------------------------------------------- /test/Box.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | 3 | describe("Box", function () { 4 | it("sets value at initialization", async function () { 5 | const [owner] = await ethers.getSigners(); 6 | const factory = await ethers.getContractFactory('Box'); 7 | const box = await upgrades.deployProxy(factory, [42, owner.address], { kind: 'uups' }).then(d => d.deployed()); 8 | 9 | expect(await box.value().then(v => v.toNumber())).to.equal(42); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node14/tsconfig.json", 3 | "include": ["tasks/**/*.ts", "hardhat.config.ts", "releases/**/*.ts"], 4 | "compilerOptions": { 5 | "skipLibCheck": true, 6 | "resolveJsonModule": true 7 | }, 8 | "files": [ 9 | ] 10 | } --------------------------------------------------------------------------------