diff --git a/deploy/deploy_contracts.ts b/deploy/deploy_contracts.ts index 8212562c..feb9561e 100644 --- a/deploy/deploy_contracts.ts +++ b/deploy/deploy_contracts.ts @@ -11,7 +11,9 @@ import { RoundsManager, TicketBroker, LivepeerToken, - Governor + Governor, + LivepeerGovernor, + Treasury } from "../typechain" import ContractDeployer from "../utils/deployer" @@ -63,7 +65,7 @@ const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) { const config = getNetworkConfig(hre.network.name) - const contractDeployer = new ContractDeployer(deploy, deployer, deployments) + const contractDeployer = new ContractDeployer(deployer, deployments) const Controller: Controller = await contractDeployer.deployController() @@ -159,6 +161,8 @@ const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) { proxy: true, args: [Controller.address] }) + // tests expect it to be saved as AdjustableRoundsManager as well + await deployments.save("AdjustableRoundsManager", roundsManager) } else { roundsManager = await contractDeployer.deployAndRegister({ contract: "RoundsManager", @@ -212,6 +216,21 @@ const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) { await (await Governor.stage(transferOwnershipUpdate, 0)).wait() await (await Governor.execute(transferOwnershipUpdate)).wait() + // Treasury (LivepeerGovernor) + + const treasury = await contractDeployer.deployAndRegister({ + contract: "Treasury", + name: "Treasury", + args: [] + }) + + const livepeerGovernor = await contractDeployer.deployAndRegister({ + contract: "LivepeerGovernor", + name: "LivepeerGovernor", + args: [Controller.address], + proxy: true + }) + // Set BondingManager parameters const BondingManager: BondingManager = (await ethers.getContractAt( "BondingManager", @@ -229,6 +248,21 @@ const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) { ) ).wait() + if (config.bondingManager.treasuryRewardCutRate) { + await ( + await BondingManager.setTreasuryRewardCutRate( + config.bondingManager.treasuryRewardCutRate + ) + ).wait() + } + if (config.bondingManager.treasuryBalanceCeiling) { + await ( + await BondingManager.setTreasuryBalanceCeiling( + config.bondingManager.treasuryBalanceCeiling + ) + ).wait() + } + // Set RoundsManager parameters const RoundsManager: RoundsManager = (await ethers.getContractAt( "RoundsManager", @@ -259,6 +293,48 @@ const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) { } } + // Initialize Treasury and LivepeerGovernor + + const Treasury: Treasury = await ethers.getContractAt( + "Treasury", + treasury.address + ) + + await Treasury.initialize( + config.treasury.minDelay, + [], // governor will be added as a proposer later + [], // governor will be added as an executor later + deployer // temporary admin role for deployer + ).then(tx => tx.wait()) + + const LivepeerGovernor: LivepeerGovernor = await ethers.getContractAt( + "LivepeerGovernor", + livepeerGovernor.address + ) + + await LivepeerGovernor.initialize( + config.livepeerGovernor.initialVotingDelay, + config.livepeerGovernor.initialVotingPeriod, + config.livepeerGovernor.initialProposalThreshold, + config.livepeerGovernor.initialQuorum, + config.livepeerGovernor.quota + ).then(tx => tx.wait()) + + // Now grant proposer and executor roles to governor and renounce deployer admin role + const roles = { + proposer: await Treasury.PROPOSER_ROLE(), + canceller: await Treasury.CANCELLER_ROLE(), + executor: await Treasury.EXECUTOR_ROLE(), + admin: await Treasury.TIMELOCK_ADMIN_ROLE() + } + for (const role of [roles.proposer, roles.canceller, roles.executor]) { + await Treasury.grantRole(role, LivepeerGovernor.address).then(tx => + tx.wait() + ) + } + + await Treasury.renounceRole(roles.admin, deployer).then(tx => tx.wait()) + // Set TicketBroker parameters const Broker: TicketBroker = (await ethers.getContractAt( "TicketBroker", diff --git a/deploy/deploy_delta_upgrade.ts b/deploy/deploy_delta_upgrade.ts new file mode 100644 index 00000000..aafc007d --- /dev/null +++ b/deploy/deploy_delta_upgrade.ts @@ -0,0 +1,207 @@ +import {HardhatRuntimeEnvironment} from "hardhat/types" +import {DeployFunction} from "hardhat-deploy/types" + +import ContractDeployer from "../utils/deployer" +import {ethers} from "hardhat" +import {BondingManager, LivepeerGovernor, Treasury} from "../typechain" +import getNetworkConfig from "./migrations.config" +import {contractId} from "../utils/helpers" + +const PROD_NETWORKS = ["mainnet", "arbitrumMainnet"] + +const isProdNetwork = (name: string): boolean => { + return PROD_NETWORKS.indexOf(name) > -1 +} + +// Returns a reference to the given contract in the format used by the governor-scripts repo (e.g. +// "ADDRESSES.arbitrumMainnet.controller"). Notice that on the serialized JSON, this will come out as a string while in +// the governance script it should be de-stringified (remove quotes) to reference an imported ADDRESS object. +const contractAddressRef = (network: string, name: string) => { + const lowerName = name[0].toLowerCase() + name.slice(1) + return `ADDRESSES.${network}.${lowerName}` +} + +// Returns a governance action spec in the format expected by the governor-scripts to call the setContractInfo function +// on the controller to register a contract in the protocol. +const setContractInfoAction = ( + network: string, + gitCommitHash: string, + name: string +) => ({ + target: contractAddressRef(network, "Controller"), + value: "0", + contract: "Controller", + name: "setContractInfo", + params: [contractId(name), contractAddressRef(network, name), gitCommitHash] +}) + +// Deploys the Livepeer Delta protocol upgrade from the previous version (962107f). This deploys only the targets +// for already existing contracts, and skips registering them in the controller in case it's a production network. +const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) { + const {deployments, getNamedAccounts} = hre // Get the deployments and getNamedAccounts which are provided by hardhat-deploy + + const network = hre.network.name + const config = getNetworkConfig(network) + + const {deployer} = await getNamedAccounts() // Fetch named accounts from hardhat.config.ts + + const contractDeployer = new ContractDeployer(deployer, deployments) + const controller = await contractDeployer.fetchDeployedController() + + // on prod networks, deploy contracts here but registration is done later through governance + // on test networks, the deployer must also be the controller owner so we can register here + const skipRegister = isProdNetwork(network) + const deploy = skipRegister ? + contractDeployer.deploy.bind(contractDeployer) : + contractDeployer.deployAndRegister.bind(contractDeployer) + + const gitCommitHash = await contractDeployer.getGitHeadCommitHash() + const governanceActions = [] + + // Deploy contracts + await deploy({ + contract: "BondingVotes", + name: "BondingVotes", + args: [controller.address], + proxy: true + }) + governanceActions.push( + setContractInfoAction(network, gitCommitHash, "BondingVotesTarget"), + setContractInfoAction(network, gitCommitHash, "BondingVotes") + ) + + const treasury = await deploy({ + contract: "Treasury", + name: "Treasury", + args: [], + proxy: false + }) + governanceActions.push( + setContractInfoAction(network, gitCommitHash, "Treasury") + ) + + const Treasury: Treasury = await ethers.getContractAt( + "Treasury", + treasury.address + ) + + // We should already initialize the treasury since it's not a proxy + await Treasury.initialize( + config.treasury.minDelay, + [], // governor will be added as a proposer later + [], // governor will be added as an executor later + deployer // temporary admin role for deployer + ).then(tx => tx.wait()) + + const livepeerGovernor = await deploy({ + contract: "LivepeerGovernor", + name: "LivepeerGovernor", + args: [controller.address], + proxy: true + }) + governanceActions.push( + setContractInfoAction(network, gitCommitHash, "LivepeerGovernorTarget"), + setContractInfoAction(network, gitCommitHash, "LivepeerGovernor") + ) + + const llDeployment = await deployments.get("SortedDoublyLL") + + await deploy({ + contract: "BondingManager", + name: "BondingManagerTarget", + libraries: { + SortedDoublyLL: llDeployment.address + }, + args: [controller.address], + proxy: false // we're deploying the Target directly, so proxy is false + }) + governanceActions.push( + setContractInfoAction(network, gitCommitHash, "BondingManagerTarget") + ) + + // Setup/initialize contracts (or print the required governance actions) + + // Grant proposer and executor roles to governor and renounce deployer admin role + const roles = { + proposer: await Treasury.PROPOSER_ROLE(), + canceller: await Treasury.CANCELLER_ROLE(), + executor: await Treasury.EXECUTOR_ROLE(), + admin: await Treasury.TIMELOCK_ADMIN_ROLE() + } + for (const role of [roles.proposer, roles.canceller, roles.executor]) { + await Treasury.grantRole(role, livepeerGovernor.address).then(tx => + tx.wait() + ) + } + + const LivepeerGovernor: LivepeerGovernor = await ethers.getContractAt( + "LivepeerGovernor", + livepeerGovernor.address + ) + const governorInitParams = [ + config.livepeerGovernor.initialVotingDelay, + config.livepeerGovernor.initialVotingPeriod, + config.livepeerGovernor.initialProposalThreshold, + config.livepeerGovernor.initialQuorum, + config.livepeerGovernor.quota + ] as Parameters + + if (!skipRegister) { + await LivepeerGovernor.initialize(...governorInitParams).then(tx => + tx.wait() + ) + } else { + governanceActions.push({ + target: contractAddressRef(network, "LivepeerGovernor"), + value: "0", + contract: "LivepeerGovernor", + name: "initialize", + params: governorInitParams + }) + } + + if (!skipRegister) { + // We need to refetch the BondingManager contract as we only deploy the Target above + const bondingManager = await deployments.get("BondingManager") + const BondingManager: BondingManager = await ethers.getContractAt( + "BondingManager", + bondingManager.address + ) + + await BondingManager.setTreasuryRewardCutRate( + config.bondingManager.treasuryRewardCutRate + ).then(tx => tx.wait()) + await BondingManager.setTreasuryBalanceCeiling( + config.bondingManager.treasuryBalanceCeiling + ).then(tx => tx.wait()) + } else { + governanceActions.push( + { + target: contractAddressRef(network, "BondingManager"), + value: "0", + contract: "BondingManager", + name: "setTreasuryRewardCutRate", + params: [config.bondingManager.treasuryRewardCutRate] + }, + { + target: contractAddressRef(network, "BondingManager"), + value: "0", + contract: "BondingManager", + name: "setTreasuryBalanceCeiling", + params: [config.bondingManager.treasuryBalanceCeiling] + } + ) + } + + // Helper print out to validate the pending governance actions that will be required by the protocol owner + if (skipRegister) { + console.log("Pending governance actions:") + console.log(JSON.stringify(governanceActions, null, 2)) + } + + console.log("Deployer must renounce ADMIN role from the Treasury with:") + console.log(`npx hardhat treasury-renounce-admin-role --network ${network}`) +} + +func.tags = ["DELTA_UPGRADE"] +export default func diff --git a/deploy/deploy_livepeer_governor.ts b/deploy/deploy_livepeer_governor.ts deleted file mode 100644 index 9e849ed0..00000000 --- a/deploy/deploy_livepeer_governor.ts +++ /dev/null @@ -1,87 +0,0 @@ -import {constants} from "ethers" -import {ethers} from "hardhat" -import {HardhatRuntimeEnvironment} from "hardhat/types" -import {DeployFunction} from "hardhat-deploy/types" - -import ContractDeployer from "../utils/deployer" -import {LivepeerGovernor, Treasury} from "../typechain" -import getNetworkConfig from "./migrations.config" - -const PROD_NETWORKS = ["mainnet", "arbitrumMainnet"] - -const isProdNetwork = (name: string): boolean => { - return PROD_NETWORKS.indexOf(name) > -1 -} - -const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) { - const {deployments, getNamedAccounts} = hre // Get the deployments and getNamedAccounts which are provided by hardhat-deploy - const {deploy} = deployments // the deployments object itself contains the deploy function - - const {deployer} = await getNamedAccounts() // Fetch named accounts from hardhat.config.ts - - const config = getNetworkConfig(hre.network.name) - - const contractDeployer = new ContractDeployer(deploy, deployer, deployments) - const controller = await contractDeployer.fetchDeployedController() - - const treasury = await contractDeployer.deployAndRegister({ - contract: "Treasury", - name: "Treasury", - args: [] - }) - const Treasury: Treasury = await ethers.getContractAt( - "Treasury", - treasury.address - ) - - await Treasury.initialize( - config.treasury.minDelay, - [], // governor will be added as a proposer later - [constants.AddressZero], // let anyone execute proposals - deployer // temporary admin role for deployer - ).then(tx => tx.wait()) - - const livepeerGovernor = await contractDeployer.deployAndRegister({ - contract: "LivepeerGovernor", - name: "LivepeerGovernor", - args: [controller.address], - proxy: true - }) - const LivepeerGovernor: LivepeerGovernor = await ethers.getContractAt( - "LivepeerGovernor", - livepeerGovernor.address - ) - - await LivepeerGovernor.initialize( - config.livepeerGovernor.initialVotingDelay, - config.livepeerGovernor.initialVotingPeriod, - config.livepeerGovernor.initialProposalThreshold, - config.livepeerGovernor.initialQuorum, - config.livepeerGovernor.quota - ).then(tx => tx.wait()) - - // Now grant proposer and executor roles to governor and renounce deployer admin role - const roles = { - proposer: await Treasury.PROPOSER_ROLE(), - canceller: await Treasury.CANCELLER_ROLE(), - executor: await Treasury.EXECUTOR_ROLE(), - admin: await Treasury.TIMELOCK_ADMIN_ROLE() - } - for (const role of [roles.proposer, roles.canceller, roles.executor]) { - await Treasury.grantRole(role, LivepeerGovernor.address).then(tx => - tx.wait() - ) - } - - if (isProdNetwork(hre.network.name)) { - // TODO: Make sure we really want this. Multi-sig would have root to everything - await Treasury.grantRole(roles.admin, config.governor.owner).then(tx => - tx.wait() - ) - } - await Treasury.renounceRole(roles.admin, deployer).then(tx => tx.wait()) -} - -func.dependencies = ["Contracts", "Poll"] -func.tags = ["LivepeerGovernor"] -export default func diff --git a/deploy/deploy_poll.ts b/deploy/deploy_poll.ts index f6253c5d..45305aa0 100644 --- a/deploy/deploy_poll.ts +++ b/deploy/deploy_poll.ts @@ -5,15 +5,15 @@ import ContractDeployer from "../utils/deployer" const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) { const {deployments, getNamedAccounts} = hre // Get the deployments and getNamedAccounts which are provided by hardhat-deploy - const {deploy, get} = deployments // the deployments object itself contains the deploy function + const {get} = deployments // the deployments object itself contains the deploy function const {deployer} = await getNamedAccounts() // Fetch named accounts from hardhat.config.ts - const contractDeployer = new ContractDeployer(deploy, deployer, deployments) + const contractDeployer = new ContractDeployer(deployer, deployments) const bondingManager = await get("BondingManager") - await contractDeployer.deployAndRegister({ + await contractDeployer.deploy({ contract: "PollCreator", name: "PollCreator", args: [bondingManager.address] diff --git a/deploy/migrations.config.ts b/deploy/migrations.config.ts index e9ecf23b..f0ef4bc9 100644 --- a/deploy/migrations.config.ts +++ b/deploy/migrations.config.ts @@ -139,8 +139,9 @@ const arbitrumMainnet = { }, bondingManager: { numActiveTranscoders: 100, - // Rounds - unbondingPeriod: 7 + unbondingPeriod: 7, // 7 rounds + treasuryRewardCutRate: ethers.BigNumber.from(10).pow(26), // 10% in 27-digit precision + treasuryBalanceCeiling: ethers.utils.parseEther("750000") // 750k LPT }, broker: { // Rounds @@ -161,6 +162,16 @@ const arbitrumMainnet = { inflation: 218500, inflationChange: 500, targetBondingRate: 500000000 + }, + treasury: { + minDelay: 0 // 0s initial proposal execution delay + }, + livepeerGovernor: { + initialVotingDelay: 1, // 1 round + initialVotingPeriod: 10, // 10 rounds + initialProposalThreshold: ethers.utils.parseEther("100"), // 100 LPT + initialQuorum: 333300, // 33% + quota: 500000 // 50% } } diff --git a/doc/deploy_delta.md b/doc/deploy_delta.md new file mode 100644 index 00000000..560b8246 --- /dev/null +++ b/doc/deploy_delta.md @@ -0,0 +1,102 @@ +# Livepeer Delta upgrade deployment steps + +## Step 1: Setup deployer account + +Configure the private key for an account that has enough ETH to run the deploy transactions. + +``` +export PRIVATE_KEY=... (no 0x prefix) +``` + +Alternatively you can set it in the `.env` file in your repo. All commands listed below expect the same account to be configured. + +## Step 2: Deploy contracts + +Run the deployment script to deploy the new contracts: + +``` +npx hardhat deploy --tags DELTA_UPGRADE --network arbitrumMainnet +``` + +## Step 3: Verify contracts source code + +Verify the contracts source code on Arbiscan: + +``` +yarn etherscan-verify --network arbitrumMainnet BondingVotesTarget BondingVotes Treasury LivepeerGovernorTarget LivepeerGovernor BondingManagerTarget +``` + +Then check the contracts on Arbiscan to make sure the source code is verified and matches the expected code (compare +with `npx hardhat flatten`). + +Also check the `Treasury` configuration, the only non-proxied contract, to make sure it wasn't manipulated by anyone +before the `initialize` function was called. + +## Step 4: Prepare governance update + +1. Grab the full git hash from the current repository where you ran the deployment script, or get it from the output of + the pending governance actions. +2. Also obtain the addresses of the contracts deployed above. You can find them in the corresponding `deployments` directory or + in the output of the deployment script. +3. Now on the `governance-scripts` repository, update: + a) the `0xPENDING_ADDRESS` address entries on the `updates/addresses.js` file with the deployed contracts. + b) the `0xPENDING_GIT_HASH` references in `updates/l2-lip-delta-91-92.js` with the git hash from the protocol + repository. +4. Commit and push the changes to the [`governance-scripts` PR](https://github.com/livepeer/governor-scripts/pull/7) and later merge it before deploy. + +## Step 5.1: Simulate governance update on a fork (optional) + +Make a fork of mainnet after the contracts deploy and make sure the Governance script can run cleanly. Also run the +validation script from Step 8 below to make sure everything is configured correctly. + +## Step 6: Renounce deployer admin role + +Once the deployed contracts have been verified, the deployer admin role over the Treasury should be renounced. + +To do so, run the following command with the same account with which you ran the deploy: + +``` +npx hardhat treasury-renounce-admin-role --network arbitrumMainnet +``` + +## Step 7: Run governance update + +In the `governance-scripts` repository, run the governance update script: + +``` +node index.js create ./updates/l2-lip-delta-91-92.js 0 +``` + +This will print out the transaction that should be run by the **protocol** governor owner to stage the update. Note that +this refers to the existing `Governor` contract that manages the protocol governance, not the new `LivepeerGovernor` +that will only manage the treasury for now. + +This output should be provided to the governor owner wallet interface to stage and then execute the governance update. + +## Step 8: Validate the update + +You can run the [verify-delta-deployment](../tasks/verify-delta-deployment.ts) task to verify the deployment and +governance update. + +To do so, run this with the same `deployer` account with which you ran the deploy: + +``` +npx hardhat verify-delta-deployment --network arbitrumMainnet +``` + +Keep in mind that it makes a voting power checkpoint of the top-stake orchestrator, in case they don't have one yet. + +## Step 9: Monitor the behavior + +Now wait until the next round so that the treasury reward cut rate gets updated. Check reward calls from orchestrators +and make sure they are being properly discounted by the expected 10% cut. + +Also worth running the `verify-delta-deployment` script again and checking its output. + +## Step 10: Deploy subgraph and explorer + +Update the subgraph and explorer changes with any new contract addresses, merge them and deploy to production. This is +less risky as we can iterate quickly. Open PRs: + +- https://github.com/livepeer/subgraph/pull/157 +- https://github.com/livepeer/explorer/pull/224 diff --git a/doc/devnet.md b/doc/devnet.md new file mode 100644 index 00000000..bad97691 --- /dev/null +++ b/doc/devnet.md @@ -0,0 +1,29 @@ +# Deploying contracts in a single network devnet + +## Prerequisites + +- Copy [`.env.sample`](../.env.sample) to a `.env` file in the repo root +- Insert values for all the environment variables + - The ETHERSCAN_API_KEY is optional, but if you want to verify the contracts on "Etherscan" you need to provide it. It + should also be from the Etherscan-like service from the network you are deploying to (e.g. Arbiscan for Arbitrum). +- Pick a testnet to use and make sure its configured in: + - [`hardhat.config.ts`](../hardhat.config.ts#L56) + - [`deploy/migrations.config.ts`](../deploy/migrations.config.ts#L185) + - `LIVE_NETWORKS` config in [`deploy/deploy_contracts.ts`](../deploy/deploy_contracts.ts#L26) +- The name of this testnet will be referred as `` in the commands below + +## Deployment + +- `yarn deploy --network ` to deploy all the core protocol contracts +- `npx hardhat deploy --tags ARBITRUM_LPT_DUMMIES --network ` to deploy the L2 bridge no-ops + +## Verification + +To verify all contracts that have been deployed in the network on the corresponding etherscan-like service: + +- `yarn etherscan-verify --network ` + +## Housekeeping + +Make sure you save or commit the `deployments` folder from your run if you want to guarantee a reference to them. If you +run the deployment commands again they will override most of the previous deployment metadata. diff --git a/doc/upgrade.md b/doc/upgrade.md index 709edf50..42727cd3 100644 --- a/doc/upgrade.md +++ b/doc/upgrade.md @@ -28,13 +28,23 @@ After deployment, a file in the `deployments` directory containing the latest ad ## Verify Contract Code -Verify the contract code on arbiscan.io. +You can run the `etherscan-verify-deployments` task from this repository available as a `yarn` script: ``` -npx hardhat etherscan-verify --network arbitrumMainnet --license MIT --sleep +yarn etherscan-verify --network arbitrumMainnet Contract1 Contract2 ... ``` -The `etherscan-verify` task might return an error for certain contracts. If this happens, an alternative approach is to generate a single "flattened" (contains code from all files that the contract depends on) `.sol` file that can be manually submitted on arbiscan.io. +Additionally, you can omit the contract names to verify all the contracts that have been deployed on the specified network. + +## Manual Verification + +If the above script does not work, you can manually verify the contract code on arbiscan.io with the `hardhat-verify` plugin: + +``` +npx hardhat verify --network arbitrumMainnet DEPLOYED_CONTRACT_ADDRESS "Constructor argument 1" ... +``` + +The `verify` task might return an error for certain contracts. If this happens, an alternative approach is to generate a single "flattened" (contains code from all files that the contract depends on) `.sol` file that can be manually submitted on arbiscan.io. ``` npx hardhat flatten contracts/bonding/BondingManager.sol > flattened.sol @@ -42,7 +52,7 @@ npx hardhat flatten contracts/bonding/BondingManager.sol > flattened.sol You can use https://arbiscan.io/verifyContract to manually submit contract code for public verification. -- The compiler config (i.e. version, optimizer runs, etc.) can be found in `hardhat.config.ts` under `solidity.compilers`. +- The compiler config (i.e. version, optimizer runs, etc.) can be found in `hardhat.config.ts` under `solidity.compilers`. - For Compiler Type, select "Solidity (Single File)". - For Open Source License Type, select "MIT" diff --git a/hardhat.config.ts b/hardhat.config.ts index 0e2b5cf7..e602adf4 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,9 +1,12 @@ +import * as dotenv from "dotenv" +dotenv.config() + import "@nomiclabs/hardhat-ethers" import "@nomiclabs/hardhat-web3" import "@typechain/hardhat" import "@nomiclabs/hardhat-waffle" import "hardhat-gas-reporter" -import "@nomiclabs/hardhat-etherscan" +import "@nomicfoundation/hardhat-verify" import "hardhat-abi-exporter" // deployment plugins @@ -96,7 +99,8 @@ const config: HardhatUserConfig = { enabled: process.env.REPORT_GAS ? true : false }, etherscan: { - apiKey: process.env.ETHERSCAN_API_KEY + apiKey: process.env.ETHERSCAN_API_KEY, + customChains: [] }, abiExporter: { path: "./abi", diff --git a/package.json b/package.json index 4d0ecf51..e0f26e8f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "deploy": "npx hardhat deploy --tags Contracts,Poll", "deploy:poll": "npx hardhat deploy --tags Poll", "deploy:contracts": "npx hardhat deploy --tags Contracts", + "etherscan-verify": "npx hardhat etherscan-verify-deployments", "lint": "yarn eslint && yarn solhint", "eslint:fix": "eslint . --ext .js,.ts --fix", "eslint": "eslint . --ext .js,.ts", @@ -45,8 +46,8 @@ "homepage": "https://github.com/livepeer/protocol#readme", "devDependencies": { "@defi-wonderland/smock": "^2.0.7", + "@nomicfoundation/hardhat-verify": "^1.1.1", "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers", - "@nomiclabs/hardhat-etherscan": "^2.1.4", "@nomiclabs/hardhat-waffle": "^2.0.1", "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/contracts": "^4.9.2", @@ -63,6 +64,7 @@ "chai": "^4.3.4", "child_process": "^1.0.2", "coveralls": "^3.0.2", + "dotenv": "^16.3.1", "eslint": "^7.32.0", "eslint-config-google": "^0.14.0", "ethereum-waffle": "^3.4.0", diff --git a/tasks/etherscan-verify-deployment.ts b/tasks/etherscan-verify-deployment.ts new file mode 100644 index 00000000..eb990f5d --- /dev/null +++ b/tasks/etherscan-verify-deployment.ts @@ -0,0 +1,95 @@ +import {Address} from "hardhat-deploy/types" +import {Etherscan} from "@nomicfoundation/hardhat-verify/etherscan" +import {task} from "hardhat/config" +import https from "https" +import {HardhatRuntimeEnvironment} from "hardhat/types" + +task( + "etherscan-verify-deployments", + "Verifies all contracts in the deployments folder" +) + .addVariadicPositionalParam("contracts", "List of contracts to verify") + .setAction(async (taskArgs, hre) => { + const etherscan = await etherscanClient(hre) + let deployments = Object.entries(await hre.deployments.all()) + + console.log(`Read ${deployments.length} deployments from environment`) + + if (taskArgs.contracts?.length) { + deployments = deployments.filter(([name]) => + taskArgs.contracts.includes(name) + ) + const names = deployments.map(t => t[0]).join(", ") + console.log( + `Filtered to ${deployments.length} contracts (${names})` + ) + } + + for (const [name, deployment] of deployments) { + console.log() // newline + + const address = deployment.address + const constructorArguments = deployment.args + + console.log( + `Verifying ${name} at ${address} constructed with (${constructorArguments})` + ) + await hre.run("verify:verify", {address, constructorArguments}) + + if (name.endsWith("Proxy") && name !== "ManagerProxy") { + const targetName = name.replace("Proxy", "Target") + const target = await hre.deployments.get(targetName) + + console.log( + `Verifying as proxy to ${targetName} at ${target.address}` + ) + await verifyProxyContract(etherscan, address, target.address) + } + } + }) + +async function etherscanClient({config, network}: HardhatRuntimeEnvironment) { + const apiKey = config.etherscan.apiKey + const chainConfig = await Etherscan.getCurrentChainConfig( + network.name, + network.provider, + [] + ) + return Etherscan.fromChainConfig(apiKey, chainConfig) +} + +function verifyProxyContract( + etherscan: Etherscan, + proxyAddress: Address, + targetAddress: Address +) { + const url = new URL(etherscan.apiUrl) + if (url.protocol !== "https:") { + throw new Error("Etherscan API URL must use HTTPS") + } + + const options = { + hostname: url.hostname, + path: + url.pathname + + `?module=contract&action=verifyproxycontract&address=${proxyAddress}` + + `&expectedimplementation=${targetAddress}&apikey=${etherscan.apiKey}`, + method: "GET" + } + + return new Promise((resolve, reject) => { + const req = https.request(options, res => { + if (res.statusCode === 200) { + return resolve() + } + + reject( + new Error( + `Failed to verify proxy contract: ${res.statusCode} ${res.statusMessage}` + ) + ) + }) + req.on("error", reject) + req.end() + }) +} diff --git a/tasks/treasury-renounce-admin-role.ts b/tasks/treasury-renounce-admin-role.ts new file mode 100644 index 00000000..850318f5 --- /dev/null +++ b/tasks/treasury-renounce-admin-role.ts @@ -0,0 +1,38 @@ +import {task} from "hardhat/config" +import {Controller, Treasury} from "../typechain" +import {contractId} from "../utils/helpers" + +task( + "treasury-renounce-admin-role", + "Renounces the admin role from the deployer once everything is good to go" +).setAction(async (taskArgs, hre) => { + const {ethers, deployments} = hre + + const {deployer} = await hre.getNamedAccounts() + + const controller = await deployments.get("Controller") + const Controller: Controller = await hre.ethers.getContractAt( + "Controller", + controller.address + ) + + const address = await Controller.getContract(contractId("Treasury")) + const Treasury: Treasury = await ethers.getContractAt("Treasury", address) + + const adminRole = await Treasury.TIMELOCK_ADMIN_ROLE() + let hasAdminRole = await Treasury.hasRole(adminRole, deployer) + if (!hasAdminRole) { + console.log("Deployer does not have admin role") + return + } + + console.log("Renouncing admin role") + await Treasury.renounceRole(adminRole, deployer).then(tx => tx.wait()) + + hasAdminRole = await Treasury.hasRole(adminRole, deployer) + if (hasAdminRole) { + throw new Error("Deployer still has admin role") + } + + console.log("Success!") +}) diff --git a/tasks/verify-delta-deployment.ts b/tasks/verify-delta-deployment.ts new file mode 100644 index 00000000..a34022fd --- /dev/null +++ b/tasks/verify-delta-deployment.ts @@ -0,0 +1,221 @@ +import {task} from "hardhat/config" +import { + BondingManager, + BondingVotes, + Controller, + LivepeerGovernor, + RoundsManager, + Treasury +} from "../typechain" +import {contractId} from "../utils/helpers" +import {constants} from "../utils/constants" +import {ethers} from "ethers" + +const expected = { + bondingManager: { + nextRoundTreasuryRewardCutRate: constants.PERC_DIVISOR_PRECISE.div(10), + treasuryBalanceCeiling: ethers.utils.parseEther("750000") + }, + livepeerGovernor: { + name: "LivepeerGovernor", + votingDelay: 1, + votingPeriod: 10, + proposalThreshold: ethers.utils.parseEther("100"), + quorumNumerator: 333300, // 33.33% + quorumDenominator: 1000000, + quota: 500000 // 50% + }, + treasury: { + minDelay: 0 + } +} + +task( + "verify-delta-deployment", + "Verifies deployment of Delta upgrade contracts (LIP-91 and LIP-92)" +) + .addOptionalPositionalParam("substep", "Substep to verify (unimplemented)") + .setAction(async (taskArgs, hre) => { + const {ethers, deployments} = hre + + const controller = await deployments.get("Controller") + const Controller: Controller = await hre.ethers.getContractAt( + "Controller", + controller.address + ) + + const getContract = async ( + name: string + ): Promise => { + const address = await Controller.getContract(contractId(name)) + return await ethers.getContractAt(name, address) + } + + const checkParam = async ( + name: string, + actual: { toString: () => string }, + expected: { toString: () => string } + ) => { + console.log(`${name} is ${actual}`) + + if (actual.toString() !== expected.toString()) { + throw new Error(`${name} is ${actual} but expected ${expected}`) + } + } + + const BondingManager: BondingManager = await getContract( + "BondingManager" + ) + + const params = { + treasuryRewardCutRate: await BondingManager.treasuryRewardCutRate(), + nextRoundTreasuryRewardCutRate: + await BondingManager.nextRoundTreasuryRewardCutRate(), + treasuryBalanceCeiling: + await BondingManager.treasuryBalanceCeiling() + } + + await checkParam( + "BondingManager.nextRoundTreasuryRewardCutRate", + params.nextRoundTreasuryRewardCutRate, + expected.bondingManager.nextRoundTreasuryRewardCutRate + ) + + await checkParam( + "BondingManager.treasuryBalanceCeiling", + params.treasuryBalanceCeiling, + expected.bondingManager.treasuryBalanceCeiling + ) + + if ( + params.treasuryRewardCutRate.eq( + params.nextRoundTreasuryRewardCutRate + ) + ) { + console.log( + "Treasury reward cut rate of 10% already propagated to current round" + ) + } else { + console.log("Treasury reward cut rate hasn't propagated yet") + + const RoundsManager: RoundsManager = await getContract( + "RoundsManager" + ) + const initialized = await RoundsManager.currentRoundInitialized() + if (!initialized) { + console.log( + "Missing only current round initialization. Call RoundsManager.initializeRound()" + ) + } else { + const currentRound = await RoundsManager.currentRound() + const nextRound = currentRound.add(1) + const currRoundStartBlock = + await RoundsManager.currentRoundStartBlock() + const nextRoundStartBlock = currRoundStartBlock.add( + await RoundsManager.roundLength() + ) + const currBlock = await RoundsManager.blockNum() + + console.log( + `Cut rate will be initialized on round ${nextRound} starting at block ${nextRoundStartBlock} (${nextRoundStartBlock.sub( + currBlock + )} blocks left)` + ) + } + } + + const LivepeerGovernor: LivepeerGovernor = await getContract( + "LivepeerGovernor" + ) + const actual = { + name: await LivepeerGovernor.name(), + votingDelay: await LivepeerGovernor.votingDelay(), + votingPeriod: await LivepeerGovernor.votingPeriod(), + proposalThreshold: await LivepeerGovernor.proposalThreshold(), + quorumNumerator: await LivepeerGovernor["quorumNumerator()"](), + quorumDenominator: await LivepeerGovernor.quorumDenominator(), + quota: await LivepeerGovernor.quota() + } + + const allParams = Object.keys( + expected.livepeerGovernor + ) as (keyof typeof expected.livepeerGovernor)[] // ts sorcery + for (const param of allParams) { + await checkParam( + `LivepeerGovernor.${param}`, + actual[param], + expected.livepeerGovernor[param] + ) + } + + const Treasury: Treasury = await getContract("Treasury") + await checkParam( + "LivepeerGovernor.timelock", + await LivepeerGovernor.timelock(), + Treasury.address + ) + await checkParam( + "Treasury.minDelay", + await Treasury.getMinDelay(), + expected.treasury.minDelay + ) + + const roles = { + proposer: await Treasury.PROPOSER_ROLE(), + canceller: await Treasury.CANCELLER_ROLE(), + executor: await Treasury.EXECUTOR_ROLE(), + admin: await Treasury.TIMELOCK_ADMIN_ROLE() + } + const checkRole = async (role: keyof typeof roles) => { + const hasRole = await Treasury.hasRole( + roles[role], + LivepeerGovernor.address + ) + if (!hasRole) { + throw new Error( + `Treasury does not provide ${role} role for governor` + ) + } + console.log(`Treasury provides ${role} role for governor`) + } + + await checkRole("proposer") + await checkRole("canceller") + await checkRole("executor") + + const {deployer} = await hre.getNamedAccounts() // Fetch named accounts from hardhat.config.ts + const deployerHasAdmin = await Treasury.hasRole(roles.admin, deployer) + if (deployerHasAdmin) { + console.error( + `WARNING: Treasury still provides ADMIN role to deployer ${deployer}` + ) + } else { + console.log( + `Treasury does not provide admin role for deployer ${deployer}` + ) + } + + const BondingVotes: BondingVotes = await getContract("BondingVotes") + + const topTranscoder = await BondingManager.getFirstTranscoderInPool() + if (!(await BondingVotes.hasCheckpoint(topTranscoder))) { + console.log(`Checkpointing top transcoder ${topTranscoder}`) + await BondingManager.checkpointBondingState(topTranscoder).then( + tx => tx.wait() + ) + } + + await checkParam( + "BondingVotes.hasCheckpoint(topTranscoder)", + await BondingVotes.hasCheckpoint(topTranscoder), + true + ) + + await checkParam( + "BondingVotes.getVotes(topTranscoder)", + await BondingVotes.getVotes(topTranscoder), + await BondingManager.transcoderTotalStake(topTranscoder) + ) + + console.log("All good!") + }) diff --git a/test/integration/LivepeerGovernor.ts b/test/integration/LivepeerGovernor.ts index 7a6c7d80..2a53ec2a 100644 --- a/test/integration/LivepeerGovernor.ts +++ b/test/integration/LivepeerGovernor.ts @@ -48,7 +48,7 @@ describe("LivepeerGovernor", () => { proposer = signers[0] const fixture = await setupIntegrationTest({ - tags: ["LivepeerGovernor"] + tags: ["Contracts", "Poll"] }) controller = await ethers.getContractAt( "Controller", diff --git a/tsconfig.json b/tsconfig.json index 719c7e8e..22445483 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,6 @@ "outDir": "dist", "resolveJsonModule": true }, - "include": ["./scripts", "./test", "./deploy", "./utils"], + "include": ["./scripts", "./test", "./deploy", "./utils", "./tasks"], "files": ["./hardhat.config.ts"] } diff --git a/utils/deployer.ts b/utils/deployer.ts index f3866f57..63072076 100644 --- a/utils/deployer.ts +++ b/utils/deployer.ts @@ -3,33 +3,31 @@ import childProcess from "child_process" const exec = util.promisify(childProcess.exec) import {ethers} from "hardhat" -import { - DeployOptions, - DeployResult, - DeploymentsExtension -} from "hardhat-deploy/types" +import {DeployResult, DeploymentsExtension} from "hardhat-deploy/types" import {deployments} from "hardhat" import {Controller} from "../typechain" import {Libraries} from "hardhat/types" +export type DeployConfig = { + contract: string + name: string + proxy?: boolean + args: Array + libraries?: Libraries | undefined +} + export default class ContractDeployer { - deploy: (name: string, options: DeployOptions) => Promise deployer: string deployments: DeploymentsExtension controller: Controller | undefined - constructor( - deploy: (name: string, options: DeployOptions) => Promise, - deployer: string, - deployments: DeploymentsExtension - ) { - this.deploy = deploy + constructor(deployer: string, deployments: DeploymentsExtension) { this.deployer = deployer this.deployments = deployments this.controller = undefined } - private async getGitHeadCommitHash(): Promise { + async getGitHeadCommitHash(): Promise { const {stdout, stderr} = await exec("git rev-parse HEAD") if (stderr) { throw new Error(stderr) @@ -41,22 +39,13 @@ export default class ContractDeployer { return ethers.utils.solidityKeccak256(["string"], [name]) } - async register(name: string, address: string) { - const gitHash = await this.getGitHeadCommitHash() - await ( - await this.controller?.setContractInfo( - this.contractId(name), - address, - gitHash - ) - )?.wait() - } - async deployController(): Promise { if (this.controller && (await deployments.get("Controller"))) { console.log("Controller already deployed") } else { - const controller = await this.deploy("Controller", { + const {deploy} = this.deployments // the deployments object itself contains the deploy function + + const controller = await deploy("Controller", { from: this.deployer, // msg.sender overwrite, use named account args: [], // constructor arguments log: true // display the address and gas used in the console (not when run in test though) @@ -78,63 +67,76 @@ export default class ContractDeployer { return this.controller } - async deployAndRegister(config: { - contract: string - name: string - proxy?: boolean - args: Array - libraries?: Libraries | undefined - }): Promise { - const {contract, name, proxy, args, libraries} = config - const targetName = `${name}Target` + async deployAndRegister(config: DeployConfig): Promise { + const {name, proxy} = config - const gitHash = await this.getGitHeadCommitHash() + if (!this.controller) { + throw new Error("Controller not initialized for registration") + } + + const deploy = await this.deploy(config) + + if (proxy) { + // deploy function only returns the proxy deployment in this case, so fetch the deployed target info + const targetName = `${name}Target` + const target = await this.deployments.get(targetName) + if (!target) { + throw new Error(`${targetName} not found`) + } - const target = await this.deploy(contract, { + // target has to be registered with a Target suffix + await this.register(targetName, target.address) + } + + // proxy gets registered as the actual contract name + await this.register(name, deploy.address) + + return deploy + } + + async deploy(config: DeployConfig) { + const {contract, name, proxy, args, libraries} = config + const {deploy} = this.deployments // the deployments object itself contains the deploy function + + // if there's no proxy, the target is just the contract itself + const targetName = proxy ? `${name}Target` : name + const target = await deploy(targetName, { + contract, from: this.deployer, log: true, args: [...args], libraries: libraries }) - if (proxy) { - await ( - await this.controller?.setContractInfo( - this.contractId(targetName), - target.address, - gitHash - ) - )?.wait() - } else { - await ( - await this.controller?.setContractInfo( - this.contractId(name), - target.address, - gitHash - ) - )?.wait() - await deployments.save(name, target) + if (!proxy) { return target } + if (!this.controller) { + throw new Error("Controller not initialized for proxy deploy") + } - // proxy == true, proceed with proxy deployment and registration - const managerProxy = await this.deploy("ManagerProxy", { + // proxy == true, proceed with proxy deployment and registration as the actual contract `name` + const managerProxy = await deploy(name, { + contract: "ManagerProxy", from: this.deployer, log: true, - args: [this.controller?.address, this.contractId(targetName)] + args: [this.controller.address, this.contractId(targetName)] }) + // additionally, save the proxy deployment with a "Proxy" suffix + await deployments.save(`${name}Proxy`, managerProxy) + + return managerProxy + } + + async register(name: string, address: string) { + const gitHash = await this.getGitHeadCommitHash() await ( await this.controller?.setContractInfo( this.contractId(name), - managerProxy.address, + address, gitHash ) )?.wait() - await deployments.save(`${contract}Target`, target) - await deployments.save(`${contract}Proxy`, managerProxy) - await deployments.save(contract, managerProxy) - - return managerProxy } } diff --git a/yarn.lock b/yarn.lock index f6501a16..e867e5f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -331,7 +331,7 @@ "@ethersproject/logger" "^5.5.0" "@ethersproject/properties" "^5.5.0" -"@ethersproject/address@5.4.0", "@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.0.4", "@ethersproject/address@^5.4.0": +"@ethersproject/address@5.4.0", "@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@^5.0.4", "@ethersproject/address@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.4.0.tgz#ba2d00a0f8c4c0854933b963b9a3a9f6eb4a37a3" integrity sha512-SD0VgOEkcACEG/C6xavlU1Hy3m5DGSXW3CUHkaaEHbAPPsgi0coP5oNPsxau8eTlZOk/bpa/hKeCNoK5IzVI2Q== @@ -342,6 +342,17 @@ "@ethersproject/logger" "^5.4.0" "@ethersproject/rlp" "^5.4.0" +"@ethersproject/address@^5.0.2": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" + integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/address@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.5.0.tgz#bcc6f576a553f21f3dd7ba17248f81b473c9c78f" @@ -401,6 +412,15 @@ "@ethersproject/logger" "^5.5.0" bn.js "^4.11.9" +"@ethersproject/bignumber@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" + integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + bn.js "^5.2.1" + "@ethersproject/bytes@5.4.0", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4", "@ethersproject/bytes@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.4.0.tgz#56fa32ce3bf67153756dbaefda921d1d4774404e" @@ -415,6 +435,13 @@ dependencies: "@ethersproject/logger" "^5.5.0" +"@ethersproject/bytes@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" + integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== + dependencies: + "@ethersproject/logger" "^5.7.0" + "@ethersproject/constants@5.4.0", "@ethersproject/constants@>=5.0.0-beta.128", "@ethersproject/constants@^5.0.4", "@ethersproject/constants@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.4.0.tgz#ee0bdcb30bf1b532d2353c977bf2ef1ee117958a" @@ -579,6 +606,14 @@ "@ethersproject/bytes" "^5.5.0" js-sha3 "0.8.0" +"@ethersproject/keccak256@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" + integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + js-sha3 "0.8.0" + "@ethersproject/logger@5.4.0", "@ethersproject/logger@>=5.0.0-beta.129", "@ethersproject/logger@^5.0.5", "@ethersproject/logger@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.4.0.tgz#f39adadf62ad610c420bcd156fd41270e91b3ca9" @@ -589,6 +624,11 @@ resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d" integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg== +"@ethersproject/logger@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" + integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== + "@ethersproject/networks@5.4.2", "@ethersproject/networks@^5.4.0": version "5.4.2" resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.4.2.tgz#2247d977626e97e2c3b8ee73cd2457babde0ce35" @@ -715,6 +755,14 @@ "@ethersproject/bytes" "^5.5.0" "@ethersproject/logger" "^5.5.0" +"@ethersproject/rlp@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" + integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/sha2@5.4.0", "@ethersproject/sha2@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.4.0.tgz#c9a8db1037014cbc4e9482bd662f86c090440371" @@ -958,6 +1006,21 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@nomicfoundation/hardhat-verify@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-verify/-/hardhat-verify-1.1.1.tgz#6a433d777ce0172d1f0edf7f2d3e1df14b3ecfc1" + integrity sha512-9QsTYD7pcZaQFEA3tBb/D/oCStYDiEVDN7Dxeo/4SCyHRSm86APypxxdOMEPlGmXsAvd+p1j/dTODcpxb8aztA== + dependencies: + "@ethersproject/abi" "^5.1.2" + "@ethersproject/address" "^5.0.2" + cbor "^8.1.0" + chalk "^2.4.2" + debug "^4.1.1" + lodash.clonedeep "^4.5.0" + semver "^6.3.0" + table "^6.8.0" + undici "^5.14.0" + "@nomiclabs/ethereumjs-vm@^4.2.2": version "4.2.2" resolved "https://registry.yarnpkg.com/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.2.2.tgz#2f8817113ca0fb6c44c1b870d0a809f0e026a6cc" @@ -985,19 +1048,6 @@ resolved "https://registry.yarnpkg.com/hardhat-deploy-ethers/-/hardhat-deploy-ethers-0.3.0-beta.10.tgz#bccfcf635d380bbab3638960f6739fe4d396fc5f" integrity sha512-TeyriUshRZ7XVHOjMsDtTozIrdwLf3Bw+oZRYNhXdG/eut5HeDhjUFPfRlG7TI1lSLvkcB5dt7OxOtPYKDOxTg== -"@nomiclabs/hardhat-etherscan@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-2.1.4.tgz#510b6a669cc2dad095466b2ba9ae0b411c779081" - integrity sha512-KgFNTQv9gpioiTpQ9UlTysCAFfkcBonmEn9rVPTT22A7DRENFM1VTsVeGWF3AzRhd0mrASBF+o0gvbH30pSe0Q== - dependencies: - "@ethersproject/abi" "^5.1.2" - "@ethersproject/address" "^5.0.2" - cbor "^5.0.2" - debug "^4.1.1" - fs-extra "^7.0.1" - node-fetch "^2.6.0" - semver "^6.3.0" - "@nomiclabs/hardhat-waffle@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.1.tgz#5d43654fba780720c5033dea240fe14f70ef4bd2" @@ -1760,6 +1810,11 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -2576,7 +2631,7 @@ bech32@1.1.4: resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== -bignumber.js@*, bignumber.js@^9.0.0, bignumber.js@^9.0.1: +bignumber.js@*, bignumber.js@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== @@ -2646,6 +2701,11 @@ bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2, bn.js@^5.1.3: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== +bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + body-parser@1.19.0, body-parser@^1.16.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -2837,6 +2897,13 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.2.0" +busboy@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -2964,13 +3031,12 @@ caseless@^0.12.0, caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -cbor@^5.0.2: - version "5.2.0" - resolved "https://registry.yarnpkg.com/cbor/-/cbor-5.2.0.tgz#4cca67783ccd6de7b50ab4ed62636712f287a67c" - integrity sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A== +cbor@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/cbor/-/cbor-8.1.0.tgz#cfc56437e770b73417a2ecbfc9caf6b771af60d5" + integrity sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg== dependencies: - bignumber.js "^9.0.1" - nofilter "^1.0.4" + nofilter "^3.1.0" chai@^4.3.4: version "4.3.4" @@ -3800,6 +3866,11 @@ dom-walk@^0.1.0: resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== +dotenv@^16.3.1: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + dotignore@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" @@ -7981,10 +8052,10 @@ node-gyp-build@^4.2.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg== -nofilter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-1.0.4.tgz#78d6f4b6a613e7ced8b015cec534625f7667006e" - integrity sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA== +nofilter@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-3.1.0.tgz#c757ba68801d41ff930ba2ec55bab52ca184aa66" + integrity sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g== nopt@3.x: version "3.0.6" @@ -9943,6 +10014,11 @@ stream-to-pull-stream@^1.7.1: looper "^3.0.0" pull-stream "^3.2.3" +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -9983,6 +10059,15 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string.prototype.trim@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.4.tgz#6014689baf5efaf106ad031a5fa45157666ed1bd" @@ -10055,6 +10140,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -10191,6 +10283,17 @@ table@^6.0.9: string-width "^4.2.0" strip-ansi "^6.0.0" +table@^6.8.0: + version "6.8.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" + integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + tape@^4.6.3: version "4.14.0" resolved "https://registry.yarnpkg.com/tape/-/tape-4.14.0.tgz#e4d46097e129817175b90925f2385f6b1bcfa826" @@ -10604,6 +10707,13 @@ underscore@1.9.1: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== +undici@^5.14.0: + version "5.23.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.23.0.tgz#e7bdb0ed42cebe7b7aca87ced53e6eaafb8f8ca0" + integrity sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg== + dependencies: + busboy "^1.6.0" + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"