diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7f7979a86..c76b7260b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -3,6 +3,14 @@ name: Run E2E tests on: push: branches: [dev] + paths: + - contracts/** + - config/** + - e2e/** + - cli/** + - tasks/** + - scripts/** + - hardhat.config.ts pull_request: {} jobs: @@ -25,9 +33,7 @@ jobs: run: | git clone https://github.com/edgeandnode/nitro pushd nitro - git checkout ci git submodule update --init --recursive ./test-node.bash --init --no-blockscout --detach popd - NETWORK=localnitrol1 ADDRESS_BOOK=addresses.json GRAPH_CONFIG=config/graph.localhost.yml yarn test:e2e - NETWORK=localnitrol2 ADDRESS_BOOK=addresses.json GRAPH_CONFIG=config/graph.arbitrum-localhost.yml yarn test:e2e + L1_NETWORK=localnitrol1 L2_NETWORK=localnitrol2 yarn test:e2e \ No newline at end of file diff --git a/.github/workflows/gre.yml b/.github/workflows/gre.yml index 2e5ade371..3a2de02fc 100644 --- a/.github/workflows/gre.yml +++ b/.github/workflows/gre.yml @@ -3,6 +3,8 @@ name: Run GRE tests on: push: branches: [dev] + paths: + - gre/** pull_request: {} jobs: diff --git a/.gitignore b/.gitignore index 1fcf808ed..96fb37310 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,12 @@ bin/ # Coverage and other reports /reports coverage.json + +# Local test files +addresses-local.json +localNetwork.json +arbitrum-addresses-local.json +tx-*.log + +# Keys +.keystore diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index d3222acaa..50c8fee59 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -40,7 +40,7 @@ To deploy to a different network execute: yarn deploy -- --network {networkName} # Example -yarn deploy -- --network kovan +yarn deploy -- --network goerli ``` The network must be configured in the `hardhat.config.ts` as explained in https://hardhat.org/config. @@ -100,7 +100,7 @@ Some contracts require the address from previously deployed contracts. For that ### Deploying a new testnet 1. Make sure contracts are up to date as you please. -2. `yarn deploy-rinkeby` to deploy to Rinkeby. This will create new contracts with new addresses in `addresses.json`. +2. `yarn deploy-goerli` to deploy to Goerli. This will create new contracts with new addresses in `addresses.json`. 3. Update the `package.json` and `package-lock.json` files with the new package version and publish a new npm package with `npm publish`. You can dry-run the files to be uploaded by running `npm publish --dry-run`. 4. Merge this update into master, branch off and save for whatever version of the testnet is going on, and then tag this on the github repo, pointing to your branch (ex. at `testnet-phase-1` branch). This way we can always get the contract code for testnet, while continuing to do work on mainnet. 5. Pull the updated package into the subgraph, and other apps that depend on the package.json. diff --git a/README.md b/README.md index 66d20f9fc..a7cc801d7 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ yarn add @graphprotocol/contracts # Contract Addresses -The testnet runs on Rinkeby, while mainnet is on Ethereum Mainnet. The addresses for both of these can be found in `./addresses.json`. +The testnet runs on Goerli, while mainnet is on Ethereum Mainnet. The addresses for both of these can be found in `./addresses.json`. # Local Setup @@ -92,7 +92,7 @@ The most straightforward way to interact with the contracts is through the hardh ``` # A console to interact with testnet contracts -npx hardhat console --network rinkeby +npx hardhat console --network goerli ``` ### Hardhat Tasks diff --git a/TESTING.md b/TESTING.md index 88b72a31c..08b89c1a5 100644 --- a/TESTING.md +++ b/TESTING.md @@ -33,23 +33,38 @@ There are several types of e2e tests which can be run separately: - Read and write interactions with the blockchain. _Requires an account with sufficient balance!_ - Example: a test validating that a user can add signal to a subgraph. -### Hardhat local node +### Hardhat local node (L1) -To run all e2e tests against a hardhat local node run: +It can be useful to run E2E tests against a fresh protocol deployment on L1, this can be done with the following: ```bash -yarn test:e2e +L1_NETWORK=localhost yarn test:e2e ``` -The command will perform the following actions: +The command will: +- start a hardhat local node +- deploy the L1 protocol +- configure the new L1 deployment +- Run all L1 e2e tests -- Start a hardhat node (localhost) -- Run `migrate:accounts` hardhat task to create keys for all protocol roles (deployer, governor, arbiter, etc). This currently doesn't support multisig accounts. -- Run `migrate` hardhat task to deploy the protocol -- Run `migrate:ownership` hardhat task to transfer ownership of governed contracts to the governor -- Run `migrate:unpause` to unpause the protocol -- Run `e2e` hardhat task to run all deployment tests (config and init) -- Run `e2e:scenario` hardhat task to run a scenario +### Arbitrum Nitro testnodes (L1/L2) + +If you want to test the protocol on an L1/L2 setup, you can run: + +```bash +L1_NETWORK=localnitrol1 L2_NETWORK=localnitrol2 yarn test:e2e +``` + +In this case the command will: +- deploy the L1 protocol +- configure the new L1 deployment +- deploy the L2 protocol +- configure the new L2 deployment +- configure the L1/L2 bridge +- Run all L1 e2e tests +- Run all L2 e2e tests + +Note that you'll need to setup the testnodes before running the tests. See [Quick Setup](https://github.com/edgeandnode/nitro#quick-setup) for details on how to do this. ### Other networks @@ -57,13 +72,13 @@ To run tests against a live testnet or even mainnet run: ```bash # All e2e tests -npx hardhat e2e --network --graph-config config/graph..yml +ARBITRUM_ADDRESS_BOOK= npx hardhat e2e --network --l1-graph-config config/graph..yml --l2-graph-config config/graph..yml # Only deployment config tests -npx hardhat e2e:config --network --graph-config config/graph..yml +ARBITRUM_ADDRESS_BOOK= npx hardhat e2e:config --network --l1-graph-config config/graph..yml --l2-graph-config config/graph..yml # Only deployment init tests -npx hardhat e2e:init --network --graph-config config/graph..yml +ARBITRUM_ADDRESS_BOOK= npx hardhat e2e:init --network --l1-graph-config config/graph..yml --l2-graph-config config/graph..yml # Only a specific scenario npx hardhat e2e:scenario --network --graph-config config/graph..yml @@ -82,26 +97,4 @@ Scenarios are defined by an optional script and a test file: - They run before the test file. - Test file - Should be named e2e/scenarios/{scenario-name}.test.ts. - - Standard chai/mocha/hardhat/ethers test file. - -## Setting up Arbitrum's testnodes - -Arbitrum provides a quick way of setting up L1 and L2 testnodes for local development and testing. The following steps will guide you through the process of setting them up. Note that a local installation of Docker and Docker Compose is required. - -```bash -git clone https://github.com/offchainlabs/nitro -cd nitro -git submodule update --init --recursive - -# Apply any changes you might want, see below for more info, and then start the testnodes -./test-node.bash --init -``` - -**Useful information** -- L1 RPC: [http://localhost:8545](http://localhost:8545/) -- L2 RPC: [http://localhost:8547](http://localhost:8547/) -- Blockscout explorer (L2 only): [http://localhost:4000/](http://localhost:4000/) -- Prefunded genesis key (L1 and L2): `e887f7d17d07cc7b8004053fb8826f6657084e88904bb61590e498ca04704cf2` - -**Enable automine on L1** -In `docker-compose.yml` file edit the `geth` service command by removing the `--dev.period 1` flag. \ No newline at end of file + - Standard chai/mocha/hardhat/ethers test file. \ No newline at end of file diff --git a/addresses.json b/addresses.json index 3855f2d1f..24307ea8d 100644 --- a/addresses.json +++ b/addresses.json @@ -303,324 +303,6 @@ "txHash": "0x106c31f2c24a5285c47a766422823766f1c939034513e85613d70d99ef697173" } }, - "4": { - "IENS": { - "address": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" - }, - "IEthereumDIDRegistry": { - "address": "0xdCa7EF03e98e0DC2B855bE647C39ABe984fcF21B" - }, - "EpochManager": { - "address": "0x23090b246Ad47dB85352b666cAff36760D087a69", - "initArgs": [ - { - "name": "controller", - "value": "0x23B1aD50996ff1b21552cDD6430868D60bF342c5" - }, - { - "name": "lengthInBlocks", - "value": 1108 - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0xcf271ece428e4e0539337bc2eb79c97a9d5337fae86d76f824ce6f92abb367ae", - "proxy": true, - "implementation": { - "address": "0x5fDB51B4126D28b176AC1FE41dd47f72F047eF63", - "creationCodeHash": "0xf03074bb7f026a2574b6ffb5d0f63f0c4fee81e004e1c46ef262dd5802d3384f", - "runtimeCodeHash": "0x0d078a0bf778c6c713c46979ac668161a0a0466356252e47082f80912e4495b2", - "txHash": "0xcc086cfaa1441412731ca073dfa68ca76d634b129b416dc75af7cea1b5292e52" - } - }, - "GraphToken": { - "address": "0x54Fe55d5d255b8460fB3Bc52D5D676F9AE5697CD", - "constructorArgs": [ - { - "name": "initialSupply", - "value": "10000000000000000000000000000" - } - ], - "creationCodeHash": "0x30da7a30d71fbd41d3327e4d0183401f257af3e905a0c68ebfd18b590b27b530", - "runtimeCodeHash": "0xb964f76194a04272e7582382a4d6bd6271bbb90deb5c1fd3ae3913504ea3a830", - "txHash": "0xfd3da9962b88397134b71259287bb056b60cdd092f91c114dcd7c794e6237c78" - }, - "ServiceRegistry": { - "address": "0xB2cD4D1205A55303ef0DeC2e4EfB5338f9c182bc", - "initArgs": [ - { - "name": "controller", - "value": "0x23B1aD50996ff1b21552cDD6430868D60bF342c5" - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x230cde8f618e46f42299e19dced8dcbe1f99b4ee8423441fc7572dea0f457935", - "proxy": true, - "implementation": { - "address": "0x4BaED79823A50c2b3387211f178235FEe8Cc7Ab7", - "creationCodeHash": "0xf5fa541b43d15fade518feb63a95a73b9c67626108ead259e444af3a7ae1804f", - "runtimeCodeHash": "0x9856d2c2985f410f2f77e456fe6163827ea5251eb5e3f3768d3d4f8868187882", - "txHash": "0xd17bdc8e05933c7146105e0e22ab5390677544cad224eb0dcd56c8088585d29a" - } - }, - "GraphCurationToken": { - "address": "0x37014c7b321da7927b6662859dc9a929543386c6", - "creationCodeHash": "0x7e9a56b6fc05d428d1c1116eaa88a658f05487b493d847bfe5c69e35ec34f092", - "runtimeCodeHash": "0x587f9d4e9ecf9e7048d9f42f027957ca34ee6a95ca37d9758d8cd0ee16e89818", - "txHash": "0xd108447808e285fcde0b9aaee9e8446af91e9983041aa47ff9410c2c2862938b" - }, - "Curation": { - "address": "0x5cCaB32d30Ca0969a8f3D495e1F67b3A90d39b14", - "initArgs": [ - { - "name": "controller", - "value": "0x23B1aD50996ff1b21552cDD6430868D60bF342c5" - }, - { - "name": "bondingCurve", - "value": "0xB4B6857dFcE1a31851c0fAde87E94Fc05053916D" - }, - { - "name": "reserveRatio", - "value": 500000 - }, - { - "name": "curationTaxPercentage", - "value": 25000 - }, - { - "name": "minimumCurationDeposit", - "value": "1000000000000000000" - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x1d8707dc7b85d4fc7fe83819acc40b8e4b3ad0b3e8257a44edcc8fb587768dfb", - "proxy": true, - "implementation": { - "address": "0x6cafc4eb2be922505ff8dedcb73e2c1599d7e352", - "creationCodeHash": "0x4aea53d73a1b7b00db3ba36023a70f4e53df68f9b42cb8932afb9cf1837a8cf7", - "runtimeCodeHash": "0x6e5cb73148de597888b628c2e0d97fa0f66ee4867ee0905314034f9031d52872", - "txHash": "0x9188f422354f01613cac4e270fb053235f9071c3c987c7eaf120601d16fa6d6e" - } - }, - "GNS": { - "address": "0x4beb7299221807Cd47C2fa118c597C51Cc2fEC99", - "initArgs": [ - { - "name": "controller", - "value": "0x23B1aD50996ff1b21552cDD6430868D60bF342c5" - }, - { - "name": "bondingCurve", - "value": "0xB4B6857dFcE1a31851c0fAde87E94Fc05053916D" - }, - { - "name": "didRegistry", - "value": "0xdca7ef03e98e0dc2b855be647c39abe984fcf21b" - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x4b3882c3ed81f8b937f207e303d7c16074369da9c72e47212c8004ed799332d7", - "proxy": true, - "implementation": { - "address": "0xA05B12aa4913082562b79ceCcDDe27E19fA00972", - "creationCodeHash": "0x86499a1c90a73b062c0d25777379cdf52085e36c7f4ce44016adc7775ea24355", - "runtimeCodeHash": "0x85cc02c86b4ee2c1b080c6f70500f775bb0fab7960ce62444a8018f3af07af75", - "txHash": "0xfc5f3fe67f9c72e88dc5ab99debe9e30e79fddf5ff4f8d06a66180759eb72177" - } - }, - "RewardsManager": { - "address": "0x460cA3721131BC978e3CF3A49EfC545A2901A828", - "initArgs": [ - { - "name": "controller", - "value": "0x23B1aD50996ff1b21552cDD6430868D60bF342c5" - }, - { - "name": "issuanceRate", - "value": "1000000012184945188" - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0xfff3ccaaeb64a173f9461416d6e3f37ae1e6d8beaeb17ca1e23c264c1d3bbc02", - "proxy": true, - "implementation": { - "address": "0x7Cd54459a1B92c14554b857325AfeE1d2B065bbe", - "creationCodeHash": "0xfec6d35d9de8a7234e77484ee4fa5d6867697d607b591ed5a8e01679fa1f0394", - "runtimeCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - "txHash": "0x0a89f7f97368815af26d0fc00cd0157a57717db97bbb734b66c894c5a65e8df6" - } - }, - "Staking": { - "address": "0x2d44C0e097F6cD0f514edAC633d82E01280B4A5c", - "initArgs": [ - { - "name": "controller", - "value": "0x23B1aD50996ff1b21552cDD6430868D60bF342c5" - }, - { - "name": "minimumIndexerStake", - "value": "100000000000000000000000" - }, - { - "name": "thawingPeriod", - "value": 6646 - }, - { - "name": "protocolPercentage", - "value": 10000 - }, - { - "name": "curationPercentage", - "value": 100000 - }, - { - "name": "channelDisputeEpochs", - "value": 2 - }, - { - "name": "maxAllocationEpochs", - "value": 6 - }, - { - "name": "delegationUnbondingPeriod", - "value": 6 - }, - { - "name": "delegationRatio", - "value": 16 - }, - { - "name": "rebateAlphaNumerator", - "value": 77 - }, - { - "name": "rebateAlphaDenominator", - "value": 100 - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x9febc188c44c0d9b1162c100833e59e72c1795d999c2ff906f723bfca118c12d", - "proxy": true, - "implementation": { - "address": "0xcf464bf68d3183ae631c1f61c3f66792f9c05da9", - "creationCodeHash": "0x4844ebc5802b1a2171f41f8dac6f83c50421bbf5a334e5a251d9ec257d45554d", - "runtimeCodeHash": "0x4844ebc5802b1a2171f41f8dac6f83c50421bbf5a334e5a251d9ec257d45554d", - "txHash": "0xef6d48956c31adced59d6ee6954c921b645021100dec573e30255fa6b2e35d2b", - "libraries": { - "LibCobbDouglas": "0x504dc6069d7307c3fba5caa7674a927a0a511563" - } - } - }, - "DisputeManager": { - "address": "0x4e4B04008C0f7875CDe80e7546d8b3b03Cf1eCf1", - "initArgs": [ - { - "name": "controller", - "value": "0x23B1aD50996ff1b21552cDD6430868D60bF342c5" - }, - { - "name": "arbitrator", - "value": "0xE1FDD398329C6b74C14cf19100316f0826a492d3" - }, - { - "name": "minimumDeposit", - "value": "10000000000000000000000" - }, - { - "name": "fishermanRewardPercentage", - "value": 500000 - }, - { - "name": "slashingPercentage", - "value": 25000 - } - ], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0xb629195f313e8214e48f5a3fc741072d710f9f9ed6079331908341761f582760", - "proxy": true, - "implementation": { - "address": "0x9CD121b6018A78c08Fc72b9948A6a43E6CEfBC07", - "creationCodeHash": "0x7508a0ca0cb8f88ef54aa2cd1a464dc93a51b5cf500f763f7192a704985d91f3", - "runtimeCodeHash": "0xc8f0b38c011925dbf847c05e73e2b23e8e14553121d58600173ba61731b0a807", - "txHash": "0x7679f2bfab93883af93bdf1166a42d1b3a87c1cbb3fb3cff5c7f860644409fd5" - } - }, - "BancorFormula": { - "address": "0xB4B6857dFcE1a31851c0fAde87E94Fc05053916D", - "creationCodeHash": "0x17f6de9ab8a9bcf03a548c01d620a32caf1f29be8d90a9688ebee54295f857ef", - "runtimeCodeHash": "0x97a57f69b0029383398d02587a3a357168950d61622fe9f9710bf02b59740d63", - "txHash": "0x6cf07aed737aee71fa67c40864495124d7c6c6f203c3ed482cbdcc12f998dbb1" - }, - "Controller": { - "address": "0x23B1aD50996ff1b21552cDD6430868D60bF342c5", - "creationCodeHash": "0x7f37a1844c38fffd5390d2114804ffc4e5cf66dfb5c7bd67a32a4f5d10eebd2d", - "runtimeCodeHash": "0x929c62381fbed59483f832611752177cc2642e1e35fedeeb6cd9703e278448a0", - "txHash": "0x8cbcfe1f800f88c501f8bdeab8aaf36134e88757c28cbbe74d58c13be36832da" - }, - "GraphProxyAdmin": { - "address": "0xC96d71957bbB06f75e1b0ae9d22AC751BE14975C", - "creationCodeHash": "0x26a6f47e71ad242e264768571ce7223bf5a86fd0113ab6cb8200f65820232904", - "runtimeCodeHash": "0xd5330527cfb09df657adc879d8ad704ce6b8d5917265cabbd3eb073d1399f122", - "txHash": "0xca463b34d7967c4351d24b2af779817bd1da75e53a48957cfa32abd1ebf3a56c" - }, - "GraphGovernance": { - "address": "0x47241861A3918eaa9097c0345bb5A91660D7AEE1", - "initArgs": ["0x1679a1d1caf1252ba43fb8fc17ebf914a0c725ae"], - "creationCodeHash": "0xa02709eb59b9cca8bee1271845b42db037dc1d042dad93410ba532d378a7c79f", - "runtimeCodeHash": "0xdb307489fd9a4a438b5b48909e12020b209280ad777561c0a7451655db097e75", - "txHash": "0x5101e33eb13504780b225a2557a7062bef93cada0838937e02e879fb3d5c2c01", - "proxy": true, - "implementation": { - "address": "0xa96F8468362e6A109ABFaAF6BBfDa303347B450e", - "creationCodeHash": "0x5bd7ee7fbf6eb49914ffc91c747d18c0909ca18c495a8b163499ebfdd82b29d2", - "runtimeCodeHash": "0xd77099bdfc3f66aec158303be46e92f8e434271d6b0c7643753cd8ac96b460b9", - "txHash": "0xb12705249777b5d955dd25ea7aebf46c5d1e3062b10bc9a0a5755b40f55e11e9" - } - }, - "AllocationExchange": { - "address": "0x58ce0a0f41449e349c1a91dd9f3d9286bf32c161", - "initArgs": [ - { - "name": "graphToken", - "value": "0x54fe55d5d255b8460fb3bc52d5d676f9ae5697cd" - }, - { - "name": "staking", - "value": "0x2d44c0e097f6cd0f514edac633d82e01280b4a5c" - }, - { - "name": "governor", - "value": "0x87d11bd744b882b7bc5a6b5450cba8c35d90eb10" - }, - { - "name": "authority", - "value": "0x2c0c3c48190d7c69f469eb586aa75e4705cfa203" - } - ], - "creationCodeHash": "0x2f052eda2faa00c8dd54ec9bffab95be79075e528f24c3fa9f722c77dcc26d20", - "runtimeCodeHash": "0xec837eb756268aa8a18c5d3182a5c2bf89bd0369f1de07ffa33b1ec5d3bef41a", - "txHash": "0x359cf3945b2584f45633c6f6f37ce5a46129b462232107bb29c456d7fdcb66d0" - }, - "SubgraphNFTDescriptor": { - "address": "0x08400283bCCa108d95e25dC2A7498c236134C8a4", - "creationCodeHash": "0x7ac0757e66857e512df199569ee11c47a61b00a8d812469b79afa5dafa98c0ed", - "runtimeCodeHash": "0x9a34ad6b202bdfa95ea85654ea2e0dd40a4b8b10847f1c3d3d805fa95a078a3d", - "txHash": "0x1bf820c2defccdb8a125b8b417463a2577b255b208e7d815045dcee188786f5a" - }, - "SubgraphNFT": { - "address": "0xcF1Cd42a28caEE626b6A6Cc62b6d0e9a290bAe20", - "creationCodeHash": "0x8c9929ec6293458209f9cbadd96821604765e3656fe3c7b289b99194ede15336", - "runtimeCodeHash": "0x6309a51754b6bec245685c7a81059dc28e3756f1045f18d059abc9294f454a6a", - "txHash": "0xe16a77593a05fdf02bc61bdf61393327e38bc055dcf57832bed308dff8249ef0" - } - }, "5": { "GraphProxyAdmin": { "address": "0x6D47902c3358E0BCC06171DE935cB23D8E276fdd", @@ -814,6 +496,34 @@ }, "IEthereumDIDRegistry": { "address": "0xdCa7EF03e98e0DC2B855bE647C39ABe984fcF21B" + }, + "BridgeEscrow": { + "address": "0x8e4145358af77516B886D865e2EcacC0Fd832B75", + "initArgs": ["0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x190ea3c8f731a77a8fd1cbce860f9561f233adeafe559b33201b7d21ccd298cf", + "proxy": true, + "implementation": { + "address": "0xDD569E05D54fBF5d02fE4a26aC03Ea00317A0A2e", + "creationCodeHash": "0x6a1fc897c0130a1c99221cde1938d247de13a0861111ac47ad81c691f323df1a", + "runtimeCodeHash": "0xc8e31a4ebea0c3e43ceece974071ba0b6db2bed6725190795e07a2d369d2a8ab", + "txHash": "0x369038dcc8d8e70d40782dd761a82cc453c7a4f1939284c724a5a72119e3e566" + } + }, + "L1GraphTokenGateway": { + "address": "0xc82fF7b51c3e593D709BA3dE1b3a0d233D1DEca1", + "initArgs": ["0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x4a06731591df5c5f77c11bf8df7851234873eb6727fbbc93f5595a223f7cf3fc", + "proxy": true, + "implementation": { + "address": "0x06A7A68d0D0D496693508ad3f50A8EA962333B7D", + "creationCodeHash": "0x9dac8130793923c7f35f3943b755b7a88e2de9a25d0ae5c0b8cb020b6479151a", + "runtimeCodeHash": "0xcd798129b77d26c0b29369138d2d8dd413fcf6cb9b3838c5f95f50d9839a388a", + "txHash": "0xa4d75169094cd8601ec507234695d83042e888ec2ab49b0ce150d7aae908d895" + } } }, "1337": { @@ -1010,5 +720,215 @@ "runtimeCodeHash": "0x07012b5692ec6cbeb7a6986e061fc5026a2f76545b07bfd9182985de002fa281", "txHash": "0xe3d870434e38ee37142a86e0fc54063df59c02c3b70135f070c3a1025c5e8246" } + }, + "421613": { + "GraphProxyAdmin": { + "address": "0x4037466bb242f51575d32E8B1be693b3E5Cd1386", + "creationCodeHash": "0x68b304ac6bce7380d5e0f6b14a122f628bffebcc75f8205cb60f0baf578b79c3", + "runtimeCodeHash": "0x8d9ba87a745cf82ab407ebabe6c1490197084d320efb6c246d94bcc80e804417", + "txHash": "0x9c4d5f8c0ab5a5bc36b0a063ab1ff04372ce7d917c0b200b94544b5da4f0230d" + }, + "BancorFormula": { + "address": "0x71319060b9fdeD6174b6368bE04F9A1b7c9aCe48", + "creationCodeHash": "0x7ae36017eddb326ddd79c7363781366121361d42fdb201cf57c57424eede46f4", + "runtimeCodeHash": "0xed6701e196ad93718e28c2a2a44d110d9e9906085bcfe4faf4f6604b08f0116c", + "txHash": "0x7fe8cabb7a4fe56311591aa8d68d6c82cb0d5c232fc5aaf28bed4d1ece0e42e5" + }, + "Controller": { + "address": "0x7f734E995010Aa8d28b912703093d532C37b6EAb", + "creationCodeHash": "0x798f913fbaa1b2547c917e3dc31679089ab27cba442c511c159803acdba28c15", + "runtimeCodeHash": "0x00ae0824f79c4e48d2d23a8d4e6d075f04f44f3ea30a4f4305c345bb98117c62", + "txHash": "0x6213da3e6367ef47cd6e1fe23e4d83296f16153a64236a5c91f865f2ec84c089" + }, + "EpochManager": { + "address": "0x8ECedc7631f4616D7f4074f9fC9D0368674794BE", + "initArgs": ["0x7f734E995010Aa8d28b912703093d532C37b6EAb", "554"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x62b0d6b8556be9443397ad1f6030fdc47b1a4a3ebcc63f34cdf4091420aec84b", + "proxy": true, + "implementation": { + "address": "0xAaB195Ed1B445A2A0E357494d9036bC746227AE2", + "creationCodeHash": "0x83bc0b08dbe1a9259666ec209f06223863f7bb9cfbf917a2d4b795c771a727fe", + "runtimeCodeHash": "0xed60261c6dc84ebc16830c36f3ee370a92802601d5a2fe1c3c19f5120dcbc2eb", + "txHash": "0xd4f8780490f63432580e3dd5b2b4d9b39e904e8b4ac5cfd23540658cbafe449d" + } + }, + "L2GraphToken": { + "address": "0x18C924BD5E8b83b47EFaDD632b7178E2Fd36073D", + "initArgs": ["0xEfc519BEd6a43a14f1BBBbA9e796C4931f7A5540"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x7ec14b524141af953959b537c1acbea9b49b12ee906563a6172123b09ab3d1f6", + "proxy": true, + "implementation": { + "address": "0x5dcAcF820D7b9F0640e8a23a5a857675A774C34a", + "creationCodeHash": "0x6c4146427aafa7375a569154be95c8c931bf83aab0315706dd78bdf79c889e4c", + "runtimeCodeHash": "0x004371d1d80011906953dcba17c648503fc94b94e1e0365c8d8c706ff91f93e9", + "txHash": "0xb748498a2ebc90e20dc8da981be832f4e00f08ea9ff289880738705e45d6aeca" + } + }, + "GraphCurationToken": { + "address": "0x2B757ad83e4ed51ecaE8D4dC9AdE8E3Fa29F7BdC", + "creationCodeHash": "0x1ee42ee271cefe20c33c0de904501e618ac4b56debca67c634d0564cecea9ff2", + "runtimeCodeHash": "0x340e8f378c0117b300f3ec255bc5c3a273f9ab5bd2940fa8eb3b5065b21f86dc", + "txHash": "0x1aa753cd01fa4505c71f6866dae35faee723d181141ed91b6e5cf3082ee90f9b" + }, + "ServiceRegistry": { + "address": "0x07ECDD4278D83Cd2425cA86256634f666b659e53", + "initArgs": ["0x7f734E995010Aa8d28b912703093d532C37b6EAb"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x8a13420fdc91139297ab1497fbf5b443c156bbc7b9d2a1ac97fb9f23abde2723", + "proxy": true, + "implementation": { + "address": "0xd18D4B4e84eA4713E04060c93bD079A974BE6C4a", + "creationCodeHash": "0x50808e8cce93cf78a23c9e6dd7984356bd2bd93be30b358982909140dd61f6ff", + "runtimeCodeHash": "0xaef79c87f7e80107c0dc568cf1f8950459b5174ee3aa565ec487556a655e71db", + "txHash": "0x2d6043d89a5f5c4f3d0df0f50264ab7efebc898be0b5d358a00715ba9f657a89" + } + }, + "Curation": { + "address": "0x7080AAcC4ADF4b1E72615D6eb24CDdE40a04f6Ca", + "initArgs": [ + "0x7f734E995010Aa8d28b912703093d532C37b6EAb", + "0x71319060b9fdeD6174b6368bE04F9A1b7c9aCe48", + "0x2B757ad83e4ed51ecaE8D4dC9AdE8E3Fa29F7BdC", + "1000000", + "10000", + "1000000000000000000" + ], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x2e5744fa4eca56cf6902e27fcc0509487f39bdb0d29b9eb0181db986235289a0", + "proxy": true, + "implementation": { + "address": "0xDA6c9d39b49c3d41CaC2030c6B75b40Efea09817", + "creationCodeHash": "0xa5fa77df71a72c5aadba812345978c291c5fa1a3a23129b6eba3a38ac85d8b5d", + "runtimeCodeHash": "0x1d265e9f658778b48a0247cfef79bfc9304d1faa1f1e085f2fea85629f68e2d5", + "txHash": "0x815eda87a2599d6f2c7458c7b164e7307d05018f0dd72073a50971d424313377" + } + }, + "SubgraphNFTDescriptor": { + "address": "0x30545f313bD2eb0F85E4f808Ae4D2C016efE78b2", + "creationCodeHash": "0xf16e8ff11d852eea165195ac9e0dfa00f98e48f6ce3c77c469c7df9bf195b651", + "runtimeCodeHash": "0x39583196f2bcb85789b6e64692d8c0aa56f001c46f0ca3d371abbba2c695860f", + "txHash": "0x060839a09e89cbd47adbb8c04cc76b21a00785600a4e8b44939dd928391777e1" + }, + "SubgraphNFT": { + "address": "0x5571D8FE183AD1367dF21eE9968690f0Eabdc593", + "constructorArgs": ["0xEfc519BEd6a43a14f1BBBbA9e796C4931f7A5540"], + "creationCodeHash": "0xc1e58864302084de282dffe54c160e20dd96c6cfff45e00e6ebfc15e04136982", + "runtimeCodeHash": "0x7216e736a8a8754e88688fbf5c0c7e9caf35c55ecc3a0c5a597b951c56cf7458", + "txHash": "0xc11917ffedda6867648fa2cb62cca1df3c0ed485a0a0885284e93a2c5d33455c" + }, + "GNS": { + "address": "0x6bf9104e054537301cC23A1023Ca30A6Df79eB21", + "initArgs": [ + "0x7f734E995010Aa8d28b912703093d532C37b6EAb", + "0x71319060b9fdeD6174b6368bE04F9A1b7c9aCe48", + "0x5571D8FE183AD1367dF21eE9968690f0Eabdc593" + ], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x3c2509730e06249d970818319bb507185d4fdea13d5600cef87928a718950c19", + "proxy": true, + "implementation": { + "address": "0x7eCb82A9Cf9B370d3fC2Ef66E38F38EDFAeaa125", + "creationCodeHash": "0xb0be24e926bb24420bb5a8d3f7bd0b70a545fdddbf8cb177a42478adf4435aae", + "runtimeCodeHash": "0x4cb62b9def5b691e43ed06808b18efe682fcefb7739909be0d6c87f1eda724cd", + "txHash": "0xf1d41fc99ed716a0c890ea62e13ee108ddcb4ecfc74efb715a4ef05605ce449b" + } + }, + "Staking": { + "address": "0xcd549d0C43d915aEB21d3a331dEaB9B7aF186D26", + "initArgs": [ + "0x7f734E995010Aa8d28b912703093d532C37b6EAb", + "100000000000000000000000", + "6646", + "10000", + "100000", + "2", + "4", + "12", + "16", + "77", + "100" + ], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0xc98ebdd0a80b97ef8f6305903ef6496a7781db76a5b1b3c3c3b2b10dbd9a7af5", + "proxy": true, + "implementation": { + "address": "0x8E56ee65Ed613f2AecA8898D19497D288601bdeb", + "creationCodeHash": "0x75b63ef816627315c635cae7f95917764e2cb797496280cdeaa9b3230bf7f7bc", + "runtimeCodeHash": "0x461ccf91c7c6188c94c6df430b6954dfd9c5cc2a79a5e4db21422e11b663d319", + "txHash": "0xb9ce53dafab3dcaad25b24d9f998888225103265bd2d84cb1545b4e06e96e3b6", + "libraries": { + "LibCobbDouglas": "0x86f0f6cd9a38A851E3AB8f110be06B77C199eC1F" + } + } + }, + "RewardsManager": { + "address": "0x5F06ABd1CfAcF7AE99530D7Fed60E085f0B15e8D", + "initArgs": ["0x7f734E995010Aa8d28b912703093d532C37b6EAb"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0xd4cfa95475e9e867fb24babd6a00a5b6b01d2267533e2412986aa1ff94d51c02", + "proxy": true, + "implementation": { + "address": "0x80b54Ba64d8a207785969d9ae0dA984EfE8D10dF", + "creationCodeHash": "0x98aaabec491a17401ca37209db0613c91285de061e859574526f841a4dd60c4a", + "runtimeCodeHash": "0x2795a83531898957014373bd4595f1f9a381ecfaf787bdfc64380563af06f06a", + "txHash": "0xb4bc7ae32ec98394c448f8773bdd3049ab83e236acb6823a7a322d88ecfbfd99" + } + }, + "DisputeManager": { + "address": "0x16DEF7E0108A5467A106dbD7537f8591f470342E", + "initArgs": [ + "0x7f734E995010Aa8d28b912703093d532C37b6EAb", + "0xF89688d5d44d73cc4dE880857A3940487076e5A4", + "10000000000000000000000", + "500000", + "25000", + "25000" + ], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x70188c9243c2226ac793ac8c0a9eecd76c9b44e53f7f6f97fa177a34808421a0", + "proxy": true, + "implementation": { + "address": "0x39aEdA1d6ea3B62b76C7c439beBfFCb5369a175C", + "creationCodeHash": "0x2e77ad7a1627b6e04bece0fe18b3ab543ef4a2d6914f2e5e640b2c8175aca3a8", + "runtimeCodeHash": "0x0186afe711eff4ceea28620d091e3c6034fd15be05894119c74a38b020e3a554", + "txHash": "0x4efbd28e55866c0292309964f47bd805922ad417e5980e14e055ad693024582d" + } + }, + "AllocationExchange": { + "address": "0x61809D6Cde07f27D2fcDCb67a42d0Af1988Be5e8", + "constructorArgs": [ + "0x18C924BD5E8b83b47EFaDD632b7178E2Fd36073D", + "0xcd549d0C43d915aEB21d3a331dEaB9B7aF186D26", + "0x05F359b1319f1Ca9b799CB6386F31421c2c49dBA", + "0xD06f366678AE139a94b2AaC2913608De568F1D03" + ], + "creationCodeHash": "0x96c5b59557c161d80f1617775a7b9537a89b0ecf2258598b3a37724be91ae80a", + "runtimeCodeHash": "0xed3d9cce65ddfa8a237d4d7d294ffdb13a082e0adcda3bbd313029cfae1365f3", + "txHash": "0x1df63329a21dca69d20e03c076dd89c350970d35319eeefab028cebbc78d29dc" + }, + "L2GraphTokenGateway": { + "address": "0xef2757855d2802bA53733901F90C91645973f743", + "initArgs": ["0x7f734E995010Aa8d28b912703093d532C37b6EAb"], + "creationCodeHash": "0xcdd28bb3db05f1267ca0f5ea29536c61841be5937ce711b813924f8ff38918cc", + "runtimeCodeHash": "0x4ca8c37c807bdfda1d6dcf441324b7ea14c6ddec5db37c20c2bf05aeae49bc0d", + "txHash": "0x47bde4e3ad0bc077897a3de65058c4b7dd710aa447ec25942f716321cbdc590d", + "proxy": true, + "implementation": { + "address": "0xc68cd0d2ca533232Fd86D6e48b907338B2E0a74A", + "creationCodeHash": "0xbd52455bd8b14bfc27af623388fe2f9e06ddd4c4be3fc06c51558a912de91770", + "runtimeCodeHash": "0x29e47f693053f978d6b2ac0a327319591bf5b5e8a6e6c0744b8afcc0250bf667", + "txHash": "0xf68a5e1e516ee9a646f19bbe4d58336fdfcf5fc859f84cdac5e68b00bcd3a09a" + } + } } } diff --git a/audits/OpenZeppelin/2022-07-graph-arbitrum-bridge-audit.pdf b/audits/OpenZeppelin/2022-07-graph-arbitrum-bridge-audit.pdf new file mode 100644 index 000000000..850c9a80b Binary files /dev/null and b/audits/OpenZeppelin/2022-07-graph-arbitrum-bridge-audit.pdf differ diff --git a/audits/OpenZeppelin/2022-07-pr552-summary.pdf b/audits/OpenZeppelin/2022-07-pr552-summary.pdf new file mode 100644 index 000000000..d77364c2a Binary files /dev/null and b/audits/OpenZeppelin/2022-07-pr552-summary.pdf differ diff --git a/audits/OpenZeppelin/2022-07-pr568-summary.pdf b/audits/OpenZeppelin/2022-07-pr568-summary.pdf new file mode 100644 index 000000000..fb01c454a Binary files /dev/null and b/audits/OpenZeppelin/2022-07-pr568-summary.pdf differ diff --git a/audits/OpenZeppelin/2022-07-pr569-summary.pdf b/audits/OpenZeppelin/2022-07-pr569-summary.pdf new file mode 100644 index 000000000..e40726fc9 Binary files /dev/null and b/audits/OpenZeppelin/2022-07-pr569-summary.pdf differ diff --git a/audits/OpenZeppelin/2022-07-pr571-summary.pdf b/audits/OpenZeppelin/2022-07-pr571-summary.pdf new file mode 100644 index 000000000..80fddb783 Binary files /dev/null and b/audits/OpenZeppelin/2022-07-pr571-summary.pdf differ diff --git a/audits/OpenZeppelin/2022-09-graph-drip-keeper-reward-audit.pdf b/audits/OpenZeppelin/2022-09-graph-drip-keeper-reward-audit.pdf new file mode 100644 index 000000000..de1b2ffbb Binary files /dev/null and b/audits/OpenZeppelin/2022-09-graph-drip-keeper-reward-audit.pdf differ diff --git a/cli/commands/bridge/to-l1.ts b/cli/commands/bridge/to-l1.ts index 42f319eb4..0913aa2e1 100644 --- a/cli/commands/bridge/to-l1.ts +++ b/cli/commands/bridge/to-l1.ts @@ -102,11 +102,16 @@ export const startSendToL1 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Prom const l2Receipt = new L2TransactionReceipt(receipt) const l2ToL1Message = (await l2Receipt.getL2ToL1Messages(cli.wallet))[0] - logger.info(`The transaction generated an L2 to L1 message in outbox with eth block number:`) - logger.info(l2ToL1Message.event.ethBlockNum.toString()) - logger.info( - `After the dispute period is finalized (in ~1 week), you can finalize this by calling`, - ) + const ethBlockNum = l2ToL1Message.getFirstExecutableBlock(l2Provider) + if (ethBlockNum === null) { + logger.info(`L2 to L1 message can or already has been executed. If not finalized call`) + } else { + logger.info(`The transaction generated an L2 to L1 message in outbox with eth block number:`) + logger.info(ethBlockNum.toString()) + logger.info( + `After the dispute period is finalized (in ~1 week), you can finalize this by calling`, + ) + } logger.info(`finish-send-to-l1 with the following txhash:`) logger.info(l2Receipt.transactionHash) } diff --git a/cli/commands/bridge/to-l2.ts b/cli/commands/bridge/to-l2.ts index 307886e30..01b3f65bc 100644 --- a/cli/commands/bridge/to-l2.ts +++ b/cli/commands/bridge/to-l2.ts @@ -27,7 +27,7 @@ const checkAndRedeemMessage = async (l1ToL2Message: L1ToL2MessageWriter) => { logger.warn('Funds were deposited on L2 but the retryable ticket was not redeemed') logAutoRedeemReason(autoRedeemRec) logger.info('Attempting to redeem...') - await l1ToL2Message.redeem() + await l1ToL2Message.redeem(process.env.CI ? { gasLimit: 2_000_000 } : {}) const redeemAttempt = await l1ToL2Message.getSuccessfulRedeem() if (redeemAttempt.status == L1ToL2MessageStatus.REDEEMED) { l2TxHash = redeemAttempt.l2TxReceipt ? redeemAttempt.l2TxReceipt.transactionHash : 'null' @@ -45,7 +45,8 @@ export const sendToL2 = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { const graphAccount = cliArgs.graphAccount @@ -102,6 +102,44 @@ export const withdraw = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { + // parse args + const ipfs = cliArgs.ipfs + const subgraphDeploymentID = cliArgs.subgraphDeploymentID + const versionPath = cliArgs.versionPath + const subgraphPath = cliArgs.subgraphPath + const tokens = parseGRT(cliArgs.tokens) + + // pin to IPFS + const subgraphDeploymentIDBytes = IPFS.ipfsHashToBytes32(subgraphDeploymentID) + const versionHashBytes = await pinMetadataToIPFS(ipfs, 'version', versionPath) + const subgraphHashBytes = await pinMetadataToIPFS(ipfs, 'subgraph', subgraphPath) + + // craft transaction + const GNS = cli.contracts.GNS + + // build publish tx + const publishTx = await GNS.populateTransaction.publishNewSubgraph( + subgraphDeploymentIDBytes, + versionHashBytes, + subgraphHashBytes, + ) + + // build mint tx + const subgraphID = buildSubgraphID( + cli.walletAddress, + await GNS.nextAccountSeqID(cli.walletAddress), + ) + const mintTx = await GNS.populateTransaction.mintSignal(subgraphID, tokens, 0) + + // ensure approval + await ensureGRTAllowance(cli.wallet, GNS.address, tokens, cli.contracts.GraphToken) + + // send multicall transaction + logger.info(`Publishing and minting on new subgraph for ${cli.walletAddress}...`) + await sendTransaction(cli.wallet, GNS, 'multicall', [[publishTx.data, mintTx.data]]) +} + export const gnsCommand = { command: 'gns', describe: 'GNS contract calls', @@ -172,7 +210,7 @@ export const gnsCommand = { }) .command({ command: 'publishNewVersion', - describe: 'Withdraw unlocked GRT', + describe: 'Publish a new subgraph version', builder: (yargs: Argv) => { return yargs .option('subgraphID', { @@ -307,6 +345,53 @@ export const gnsCommand = { return withdraw(await loadEnv(argv), argv) }, }) + .command({ + command: 'publishAndSignal', + describe: 'Publish a new subgraph and add initial signal', + builder: (yargs: Argv) => { + return yargs + .option('ipfs', { + description: 'ipfs endpoint. ex. https://api.thegraph.com/ipfs/', + type: 'string', + requiresArg: true, + demandOption: true, + }) + .option('subgraphDeploymentID', { + description: 'subgraph deployment ID in base58', + type: 'string', + requiresArg: true, + demandOption: true, + }) + .option('versionPath', { + description: ` filepath to metadata. With JSON format:\n + "description": "", + "label": ""`, + type: 'string', + requiresArg: true, + demandOption: true, + }) + .option('subgraphPath', { + description: ` filepath to metadata. With JSON format:\n + "description": "", + "displayName": "", + "image": "", + "codeRepository": "", + "website": "",`, + type: 'string', + requiresArg: true, + demandOption: true, + }) + .option('tokens', { + description: 'Amount of tokens to deposit', + type: 'string', + requiresArg: true, + demandOption: true, + }) + }, + handler: async (argv: CLIArgs): Promise => { + return publishAndSignal(await loadEnv(argv), argv) + }, + }) }, handler: (): void => { yargs.showHelp() diff --git a/cli/contracts.ts b/cli/contracts.ts index b01727c20..c36e83567 100644 --- a/cli/contracts.ts +++ b/cli/contracts.ts @@ -1,4 +1,15 @@ -import { BaseContract, providers, Signer } from 'ethers' +import { + BaseContract, + Contract, + ContractFunction, + ContractReceipt, + ContractTransaction, + providers, + Signer, +} from 'ethers' +import { Provider } from '@ethersproject/providers' +import lodash from 'lodash' +import fs from 'fs' import { AddressBook } from './address-book' import { chainIdIsL2 } from './cross-chain' @@ -68,13 +79,26 @@ export const loadContracts = ( addressBook: AddressBook, chainId: number | string, signerOrProvider?: Signer | providers.Provider, + enableTXLogging = false, ): NetworkContracts => { const contracts = {} for (const contractName of addressBook.listEntries()) { + const contractEntry = addressBook.getEntry(contractName) + try { - contracts[contractName] = loadAddressBookContract(contractName, addressBook, signerOrProvider) + let contract = getContractAt(contractName, contractEntry.address) + if (enableTXLogging) { + contract.connect = getWrappedConnect(contract, contractName) + contract = wrapCalls(contract, contractName) + } + contracts[contractName] = contract + + if (signerOrProvider) { + contracts[contractName] = contracts[contractName].connect(signerOrProvider) + } + // On L2 networks, we alias L2GraphToken as GraphToken - if (signerOrProvider && chainIdIsL2(chainId) && contractName == 'L2GraphToken') { + if (chainIdIsL2(chainId) && contractName == 'L2GraphToken') { contracts['GraphToken'] = contracts[contractName] } } catch (err) { @@ -83,3 +107,77 @@ export const loadContracts = ( } return contracts as NetworkContracts } + +// Returns a contract connect function that wrapps contract calls with wrapCalls +function getWrappedConnect( + contract: Contract, + contractName: string, +): (signerOrProvider: string | Provider | Signer) => Contract { + const call = contract.connect.bind(contract) + const override = (signerOrProvider: string | Provider | Signer): Contract => { + const connectedContract = call(signerOrProvider) + connectedContract.connect = getWrappedConnect(connectedContract, contractName) + return wrapCalls(connectedContract, contractName) + } + return override +} + +// Returns a contract with wrapped calls +// The wrapper will run the tx, wait for confirmation and log the details +function wrapCalls(contract: Contract, contractName: string): Contract { + const wrappedContract = lodash.cloneDeep(contract) + + for (const fn of Object.keys(contract.functions)) { + const call: ContractFunction = contract.functions[fn] + const override = async (...args: Array): Promise => { + // Make the call + const tx = await call(...args) + logContractCall(tx, contractName, fn, args) + + // Wait for confirmation + const receipt = await contract.provider.waitForTransaction(tx.hash) + logContractReceipt(tx, receipt) + return tx + } + + wrappedContract.functions[fn] = override + wrappedContract[fn] = override + } + + return wrappedContract +} + +function logContractCall( + tx: ContractTransaction, + contractName: string, + fn: string, + args: Array, +) { + const msg = [] + msg.push(`> Sent transaction ${contractName}.${fn}`) + msg.push(` sender: ${tx.from}`) + msg.push(` contract: ${tx.to}`) + msg.push(` params: [ ${args} ]`) + msg.push(` txHash: ${tx.hash}`) + + logToConsoleAndFile(msg) +} + +function logContractReceipt(tx: ContractTransaction, receipt: ContractReceipt) { + const msg = [] + msg.push( + receipt.status ? `✔ Transaction succeeded: ${tx.hash}` : `✖ Transaction failed: ${tx.hash}`, + ) + + logToConsoleAndFile(msg) +} + +function logToConsoleAndFile(msg: string[]) { + const isoDate = new Date().toISOString() + const fileName = `tx-${isoDate.substring(0, 10)}.log` + + msg.map((line) => { + console.log(line) + fs.appendFileSync(fileName, `[${isoDate}] ${line}\n`) + }) +} diff --git a/cli/cross-chain.ts b/cli/cross-chain.ts index ea3b081ac..a0b674624 100644 --- a/cli/cross-chain.ts +++ b/cli/cross-chain.ts @@ -1,4 +1,6 @@ import { L1ToL2MessageGasEstimator } from '@arbitrum/sdk' +import { L1ToL2MessageNoGasParams } from '@arbitrum/sdk/dist/lib/message/L1ToL2MessageCreator' +import { GasOverrides } from '@arbitrum/sdk/dist/lib/message/L1ToL2MessageGasEstimator' import { BigNumber, providers } from 'ethers' import { parseEther } from 'ethers/lib/utils' @@ -44,20 +46,31 @@ export const estimateRetryableTxGas = async ( logger.info('Estimating retryable ticket gas:') const baseFee = (await l1Provider.getBlock('latest')).baseFeePerGas const gasEstimator = new L1ToL2MessageGasEstimator(l2Provider) + const retryableEstimateData: L1ToL2MessageNoGasParams = { + from: gatewayAddress, + to: l2Dest, + data: depositCalldata, + l2CallValue: parseEther('0'), + excessFeeRefundAddress: gatewayAddress, + callValueRefundAddress: gatewayAddress, + } + + const estimateOpts: GasOverrides = {} + if (opts.maxGas) estimateOpts.gasLimit = { base: opts.maxGas } + if (opts.maxSubmissionCost) estimateOpts.maxSubmissionFee = { base: opts.maxSubmissionCost } + if (opts.gasPriceBid) estimateOpts.maxFeePerGas = { base: opts.gasPriceBid } + const gasParams = await gasEstimator.estimateAll( - gatewayAddress, - l2Dest, - depositCalldata, - parseEther('0'), + retryableEstimateData, baseFee as BigNumber, - gatewayAddress, - gatewayAddress, l1Provider, + estimateOpts, ) + // override fixed values return { maxGas: opts.maxGas ?? gasParams.gasLimit, gasPriceBid: opts.gasPriceBid ?? gasParams.maxFeePerGas, - maxSubmissionCost: opts.maxSubmissionCost ?? gasParams.maxSubmissionFee, + maxSubmissionCost: opts.maxSubmissionCost ?? gasParams.maxSubmissionCost, } } diff --git a/cli/helpers.ts b/cli/helpers.ts index 37f241fc4..115336eca 100644 --- a/cli/helpers.ts +++ b/cli/helpers.ts @@ -1,7 +1,8 @@ import fs from 'fs' +import path from 'path' import * as dotenv from 'dotenv' -import { utils, providers, Wallet } from 'ethers' +import { utils, BigNumber, BigNumberish, Signer } from 'ethers' import ipfsHttpClient from 'ipfs-http-client' import inquirer from 'inquirer' @@ -15,6 +16,8 @@ import { jsonToSubgraphMetadata, jsonToVersionMetadata, } from './metadata' +import { solidityKeccak256 } from 'ethers/lib/utils' +import { GraphToken } from '../build/types/GraphToken' dotenv.config() @@ -46,12 +49,14 @@ export class IPFS { export const pinMetadataToIPFS = async ( ipfs: string, type: string, - path?: string, // Only pass path or metadata, not both + filepath?: string, // Only pass path or metadata, not both metadata?: SubgraphMetadata | VersionMetadata, ): Promise => { - if (metadata == undefined && path != undefined) { + if (metadata == undefined && filepath != undefined) { if (type == 'subgraph') { - metadata = jsonToSubgraphMetadata(JSON.parse(fs.readFileSync(__dirname + path).toString())) + metadata = jsonToSubgraphMetadata( + JSON.parse(fs.readFileSync(path.join(__dirname, filepath)).toString()), + ) logger.info('Meta data:') logger.info(` Subgraph Description: ${metadata.description}`) logger.info(`Subgraph Display Name: ${metadata.displayName}`) @@ -59,7 +64,9 @@ export const pinMetadataToIPFS = async ( logger.info(` Subgraph Code Repository: ${metadata.codeRepository}`) logger.info(` Subgraph Website: ${metadata.website}`) } else if (type == 'version') { - metadata = jsonToVersionMetadata(JSON.parse(fs.readFileSync(__dirname + path).toString())) + metadata = jsonToVersionMetadata( + JSON.parse(fs.readFileSync(path.join(__dirname, filepath)).toString()), + ) logger.info('Meta data:') logger.info(` Version Description: ${metadata.description}`) logger.info(` Version Label: ${metadata.label}`) @@ -104,3 +111,23 @@ export const confirm = async (message: string, skip: boolean): Promise } return true } + +export const buildSubgraphID = (account: string, seqID: BigNumber): string => + solidityKeccak256(['address', 'uint256'], [account, seqID]) + +export const ensureGRTAllowance = async ( + owner: Signer, + spender: string, + amount: BigNumberish, + grt: GraphToken, +): Promise => { + const ownerAddress = await owner.getAddress() + const allowance = await grt.allowance(ownerAddress, spender) + const allowTokens = BigNumber.from(amount).sub(allowance) + if (allowTokens.gt(0)) { + console.log( + `\nApproving ${spender} to spend ${allowTokens} tokens on ${ownerAddress} behalf...`, + ) + await grt.connect(owner).approve(spender, amount) + } +} diff --git a/cli/network.ts b/cli/network.ts index 6667a960d..9a0618ff3 100644 --- a/cli/network.ts +++ b/cli/network.ts @@ -93,7 +93,7 @@ export const waitTransaction = async ( ): Promise => { const receipt = await sender.provider.waitForTransaction(tx.hash) const networkName = (await sender.provider.getNetwork()).name - if (networkName === 'kovan' || networkName === 'rinkeby') { + if (networkName === 'goerli') { receipt.status // 1 = success, 0 = failure ? logger.info(`Transaction succeeded: 'https://${networkName}.etherscan.io/tx/${tx.hash}'`) : logger.warn(`Transaction failed: 'https://${networkName}.etherscan.io/tx/${tx.hash}'`) diff --git a/config/graph.arbitrum-goerli.yml b/config/graph.arbitrum-goerli.yml index 68a656a31..f7ab04744 100644 --- a/config/graph.arbitrum-goerli.yml +++ b/config/graph.arbitrum-goerli.yml @@ -1,10 +1,10 @@ general: - arbitrator: &arbitrator "0x113DC95e796836b8F0Fa71eE7fB42f221740c3B0" # Arbitration Council (TODO: update) - governor: &governor "0x3e43EF77fAAd296F65eF172E8eF06F8231c9DeAd" # Graph Council (TODO: update) - authority: &authority "0x79fd74da4c906509862c8fe93e87a9602e370bc4" # Authority that signs payment vouchers (TODO: update) - availabilityOracle: &availabilityOracle "0x5d3B6F98F1cCdF873Df0173CDE7335874a396c4d" # Subgraph Availability Oracle (TODO: update) - pauseGuardian: &pauseGuardian "0x8290362Aba20D17c51995085369E001Bad99B21c" # Protocol pause guardian (TODO: update) - allocationExchangeOwner: &allocationExchangeOwner "0x74Db79268e63302d3FC69FB5a7627F7454a41732" # Allocation Exchange owner (TODO: update) + arbitrator: &arbitrator "0xF89688d5d44d73cc4dE880857A3940487076e5A4" # Arbitration Council (TODO: update) + governor: &governor "0x5CeeeE16F30357d49c50bcd7F520ca6527cf388a" # Graph Council (TODO: update) + authority: &authority "0xD06f366678AE139a94b2AaC2913608De568F1D03" # Authority that signs payment vouchers (TODO: update) + availabilityOracle: &availabilityOracle "0xA99A56fA38a6B9553853c84E11458AeCcdad509B" # Subgraph Availability Oracle (TODO: update) + pauseGuardian: &pauseGuardian "0x4B6C90B9fE29dfa521188B6547989C23d613b79B" # Protocol pause guardian (TODO: update) + allocationExchangeOwner: &allocationExchangeOwner "0x05F359b1319f1Ca9b799CB6386F31421c2c49dBA" # Allocation Exchange owner (TODO: update) contracts: Controller: @@ -68,7 +68,7 @@ contracts: controller: "${{Controller.address}}" bondingCurve: "${{BancorFormula.address}}" curationTokenMaster: "${{GraphCurationToken.address}}" - reserveRatio: 500000 # in parts per million + reserveRatio: 1000000 # in parts per million curationTaxPercentage: 10000 # in parts per million minimumCurationDeposit: "1000000000000000000" # in wei calls: diff --git a/config/graph.arbitrum-localhost.yml b/config/graph.arbitrum-localhost.yml index c58aabf44..52e2b411a 100644 --- a/config/graph.arbitrum-localhost.yml +++ b/config/graph.arbitrum-localhost.yml @@ -1,10 +1,10 @@ general: - arbitrator: &arbitrator "0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0" # Arbitration Council - governor: &governor "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b" # Graph Council - authority: &authority "0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d" # Authority that signs payment vouchers - availabilityOracle: &availabilityOracle "0xd03ea8624C8C5987235048901fB614fDcA89b117" # Subgraph Availability Oracle - pauseGuardian: &pauseGuardian "0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC" # Protocol pause guardian - allocationExchangeOwner: &allocationExchangeOwner "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9" # Allocation Exchange owner + arbitrator: &arbitrator "0x4237154FE0510FdE3575656B60c68a01B9dCDdF8" # Arbitration Council + governor: &governor "0x1257227a2ECA34834940110f7B5e341A5143A2c4" # Graph Council + authority: &authority "0x12B8D08b116E1E3cc29eE9Cf42bB0AA8129C3215" # Authority that signs payment vouchers + availabilityOracle: &availabilityOracle "0x7694a48065f063a767a962610C6717c59F36b445" # Subgraph Availability Oracle + pauseGuardian: &pauseGuardian "0x601060e0DC5349AA55EC73df5A58cB0FC1cD2e3C" # Protocol pause guardian + allocationExchangeOwner: &allocationExchangeOwner "0xbD38F7b67a591A5cc7D642e1026E5095B819d952" # Allocation Exchange owner contracts: Controller: @@ -68,7 +68,7 @@ contracts: controller: "${{Controller.address}}" bondingCurve: "${{BancorFormula.address}}" curationTokenMaster: "${{GraphCurationToken.address}}" - reserveRatio: 500000 # in parts per million + reserveRatio: 1000000 # in parts per million curationTaxPercentage: 10000 # in parts per million minimumCurationDeposit: "1000000000000000000" # in wei calls: diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml index 9588d5f32..aa15603a0 100644 --- a/config/graph.arbitrum-one.yml +++ b/config/graph.arbitrum-one.yml @@ -1,10 +1,10 @@ general: arbitrator: &arbitrator "0x113DC95e796836b8F0Fa71eE7fB42f221740c3B0" # Arbitration Council - governor: &governor "0x3e43EF77fAAd296F65eF172E8eF06F8231c9DeAd" # Graph Council - authority: &authority "0x79fd74da4c906509862c8fe93e87a9602e370bc4" # Authority that signs payment vouchers - availabilityOracle: &availabilityOracle "0x5d3B6F98F1cCdF873Df0173CDE7335874a396c4d" # Subgraph Availability Oracle (TODO: update) - pauseGuardian: &pauseGuardian "0x8290362Aba20D17c51995085369E001Bad99B21c" # Protocol pause guardian (TODO: update) - allocationExchangeOwner: &allocationExchangeOwner "0x74Db79268e63302d3FC69FB5a7627F7454a41732" # Allocation Exchange owner (TODO: update) + governor: &governor "0x8C6de8F8D562f3382417340A6994601eE08D3809" # Graph Council + authority: &authority "0x79f2212de27912bCb25a452fC102C85c142E3eE3" # Authority that signs payment vouchers + availabilityOracle: &availabilityOracle "0xbCAEE36Ce38Ec534c7078db1f90118E72173645B" # Subgraph Availability Oracle + pauseGuardian: &pauseGuardian "0xB0aD33a21b98bCA1761729A105e2E34e27153aAE" # Protocol pause guardian + allocationExchangeOwner: &allocationExchangeOwner "0x270Ea4ea9e8A699f8fE54515E3Bb2c418952623b" # Allocation Exchange owner contracts: Controller: @@ -68,7 +68,7 @@ contracts: controller: "${{Controller.address}}" bondingCurve: "${{BancorFormula.address}}" curationTokenMaster: "${{GraphCurationToken.address}}" - reserveRatio: 500000 # in parts per million + reserveRatio: 1000000 # in parts per million curationTaxPercentage: 10000 # in parts per million minimumCurationDeposit: "1000000000000000000" # in wei calls: diff --git a/contracts/curation/Curation.sol b/contracts/curation/Curation.sol index fc59b33d1..565a51a89 100644 --- a/contracts/curation/Curation.sol +++ b/contracts/curation/Curation.sol @@ -2,17 +2,20 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts/utils/Address.sol"; -import "@openzeppelin/contracts/math/SafeMath.sol"; -import "@openzeppelin/contracts/proxy/Clones.sol"; - -import "../bancor/BancorFormula.sol"; -import "../upgrades/GraphUpgradeable.sol"; -import "../utils/TokenUtils.sol"; - -import "./CurationStorage.sol"; -import "./ICuration.sol"; -import "./GraphCurationToken.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; + +import { BancorFormula } from "../bancor/BancorFormula.sol"; +import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; +import { TokenUtils } from "../utils/TokenUtils.sol"; +import { IRewardsManager } from "../rewards/IRewardsManager.sol"; +import { Managed } from "../governance/Managed.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; +import { CurationV1Storage } from "./CurationStorage.sol"; +import { ICuration } from "./ICuration.sol"; +import { IGraphCurationToken } from "./IGraphCurationToken.sol"; +import { GraphCurationToken } from "./GraphCurationToken.sol"; /** * @title Curation contract diff --git a/contracts/curation/CurationStorage.sol b/contracts/curation/CurationStorage.sol index dd2edd18b..a530d9199 100644 --- a/contracts/curation/CurationStorage.sol +++ b/contracts/curation/CurationStorage.sol @@ -2,7 +2,9 @@ pragma solidity ^0.7.6; -import "../governance/Managed.sol"; +import { ICuration } from "./ICuration.sol"; +import { IGraphCurationToken } from "./IGraphCurationToken.sol"; +import { Managed } from "../governance/Managed.sol"; abstract contract CurationV1Storage is Managed, ICuration { // -- Pool -- diff --git a/contracts/gateway/BridgeEscrow.sol b/contracts/gateway/BridgeEscrow.sol index 605f13a50..3c0fa5c1a 100644 --- a/contracts/gateway/BridgeEscrow.sol +++ b/contracts/gateway/BridgeEscrow.sol @@ -2,9 +2,11 @@ pragma solidity ^0.7.6; -import "../upgrades/GraphUpgradeable.sol"; -import "../governance/Managed.sol"; -import "../token/IGraphToken.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; + +import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; +import { Managed } from "../governance/Managed.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; /** * @title Bridge Escrow @@ -12,17 +14,17 @@ import "../token/IGraphToken.sol"; * a set of spenders that can transfer the tokens; the L1 side of each L2 bridge has to be * approved as a spender. */ -contract BridgeEscrow is GraphUpgradeable, Managed { +contract BridgeEscrow is Initializable, GraphUpgradeable, Managed { /** - * @dev Initialize this contract. + * @notice Initialize the BridgeEscrow contract. * @param _controller Address of the Controller that manages this contract */ - function initialize(address _controller) external onlyImpl { + function initialize(address _controller) external onlyImpl initializer { Managed._initialize(_controller); } /** - * @dev Approve a spender (i.e. a bridge that manages the GRT funds held by the escrow) + * @notice Approve a spender (i.e. a bridge that manages the GRT funds held by the escrow) * @param _spender Address of the spender that will be approved */ function approveAll(address _spender) external onlyGovernor { @@ -30,11 +32,10 @@ contract BridgeEscrow is GraphUpgradeable, Managed { } /** - * @dev Revoke a spender (i.e. a bridge that will no longer manage the GRT funds held by the escrow) + * @notice Revoke a spender (i.e. a bridge that will no longer manage the GRT funds held by the escrow) * @param _spender Address of the spender that will be revoked */ function revokeAll(address _spender) external onlyGovernor { - IGraphToken grt = graphToken(); - grt.decreaseAllowance(_spender, grt.allowance(address(this), _spender)); + graphToken().approve(_spender, 0); } } diff --git a/contracts/gateway/GraphTokenGateway.sol b/contracts/gateway/GraphTokenGateway.sol index 00e8441f5..ca2ad4c95 100644 --- a/contracts/gateway/GraphTokenGateway.sol +++ b/contracts/gateway/GraphTokenGateway.sol @@ -2,23 +2,26 @@ pragma solidity ^0.7.6; -import "../upgrades/GraphUpgradeable.sol"; -import "../arbitrum/ITokenGateway.sol"; -import "../governance/Pausable.sol"; -import "../governance/Managed.sol"; +import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; +import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; +import { Pausable } from "../governance/Pausable.sol"; +import { Managed } from "../governance/Managed.sol"; /** * @title L1/L2 Graph Token Gateway * @dev This includes everything that's shared between the L1 and L2 sides of the bridge. */ abstract contract GraphTokenGateway is GraphUpgradeable, Pausable, Managed, ITokenGateway { + /// @dev Storage gap added in case we need to add state variables to this contract + uint256[50] private __gap; + /** * @dev Check if the caller is the Controller's governor or this contract's pause guardian. */ modifier onlyGovernorOrGuardian() { require( msg.sender == controller.getGovernor() || msg.sender == pauseGuardian, - "Only Governor or Guardian can call" + "Only Governor or Guardian" ); _; } @@ -32,26 +35,36 @@ abstract contract GraphTokenGateway is GraphUpgradeable, Pausable, Managed, ITok _setPauseGuardian(_newPauseGuardian); } - /** - * @dev Override the default pausing from Managed to allow pausing this - * particular contract instead of pausing from the Controller. - */ - function _notPaused() internal view override { - require(!_paused, "Paused (contract)"); - } - /** * @notice Change the paused state of the contract * @param _newPaused New value for the pause state (true means the transfers will be paused) */ function setPaused(bool _newPaused) external onlyGovernorOrGuardian { + if (!_newPaused) { + _checksBeforeUnpause(); + } _setPaused(_newPaused); } /** * @notice Getter to access paused state of this contract + * @return True if the contract is paused, false otherwise */ function paused() external view returns (bool) { return _paused; } + + /** + * @dev Override the default pausing from Managed to allow pausing this + * particular contract instead of pausing from the Controller. + */ + function _notPaused() internal view override { + require(!_paused, "Paused (contract)"); + } + + /** + * @dev Runs state validation before unpausing, reverts if + * something is not set properly + */ + function _checksBeforeUnpause() internal view virtual; } diff --git a/contracts/gateway/ICallhookReceiver.sol b/contracts/gateway/ICallhookReceiver.sol index fb7492bb7..ff0fbfab1 100644 --- a/contracts/gateway/ICallhookReceiver.sol +++ b/contracts/gateway/ICallhookReceiver.sol @@ -3,14 +3,14 @@ /** * @title Interface for contracts that can receive callhooks through the Arbitrum GRT bridge * @dev Any contract that can receive a callhook on L2, sent through the bridge from L1, must - * be whitelisted by the governor, but also implement this interface that contains + * be allowlisted by the governor, but also implement this interface that contains * the function that will actually be called by the L2GraphTokenGateway. */ pragma solidity ^0.7.6; interface ICallhookReceiver { /** - * @dev Receive tokens with a callhook from the bridge + * @notice Receive tokens with a callhook from the bridge * @param _from Token sender in L1 * @param _amount Amount of tokens that were transferred * @param _data ABI-encoded callhook data diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index bd2c7036f..d946b78a7 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -3,11 +3,18 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/utils/Address.sol"; -import "@openzeppelin/contracts/math/SafeMath.sol"; - -import "../arbitrum/L1ArbitrumMessenger.sol"; -import "./GraphTokenGateway.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; +import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +import { L1ArbitrumMessenger } from "../arbitrum/L1ArbitrumMessenger.sol"; +import { IBridge } from "../arbitrum/IBridge.sol"; +import { IInbox } from "../arbitrum/IInbox.sol"; +import { IOutbox } from "../arbitrum/IOutbox.sol"; +import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; +import { Managed } from "../governance/Managed.sol"; +import { GraphTokenGateway } from "./GraphTokenGateway.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; /** * @title L1 Graph Token Gateway Contract @@ -18,31 +25,31 @@ import "./GraphTokenGateway.sol"; * (See: https://github.com/OffchainLabs/arbitrum/tree/master/packages/arb-bridge-peripherals/contracts/tokenbridge * and https://github.com/livepeer/arbitrum-lpt-bridge) */ -contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { - using SafeMath for uint256; +contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMessenger { + using SafeMathUpgradeable for uint256; - // Address of the Graph Token contract on L2 + /// Address of the Graph Token contract on L2 address public l2GRT; - // Address of the Arbitrum Inbox + /// Address of the Arbitrum Inbox address public inbox; - // Address of the Arbitrum Gateway Router on L1 + /// Address of the Arbitrum Gateway Router on L1 address public l1Router; - // Address of the L2GraphTokenGateway on L2 that is the counterpart of this gateway + /// Address of the L2GraphTokenGateway on L2 that is the counterpart of this gateway address public l2Counterpart; - // Address of the BridgeEscrow contract that holds the GRT in the bridge + /// Address of the BridgeEscrow contract that holds the GRT in the bridge address public escrow; - // Addresses for which this mapping is true are allowed to send callhooks in outbound transfers - mapping(address => bool) public callhookWhitelist; - // Total amount minted from L2 + /// Addresses for which this mapping is true are allowed to send callhooks in outbound transfers + mapping(address => bool) public callhookAllowlist; + /// Total amount minted from L2 uint256 public totalMintedFromL2; - // Accumulated allowance for tokens minted from L2 at lastL2MintAllowanceUpdateBlock + /// Accumulated allowance for tokens minted from L2 at lastL2MintAllowanceUpdateBlock uint256 public accumulatedL2MintAllowanceSnapshot; - // Block at which new L2 allowance starts accumulating + /// Block at which new L2 allowance starts accumulating uint256 public lastL2MintAllowanceUpdateBlock; - // New L2 mint allowance per block + /// New L2 mint allowance per block uint256 public l2MintAllowancePerBlock; - // Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 + /// Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 event DepositInitiated( address l1Token, address indexed from, @@ -51,7 +58,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { uint256 amount ); - // Emitted when an incoming transfer is finalized, i.e tokens are withdrawn from L2 to L1 + /// Emitted when an incoming transfer is finalized, i.e tokens are withdrawn from L2 to L1 event WithdrawalFinalized( address l1Token, address indexed from, @@ -60,25 +67,25 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { uint256 amount ); - // Emitted when the Arbitrum Inbox and Gateway Router addresses have been updated + /// Emitted when the Arbitrum Inbox and Gateway Router addresses have been updated event ArbitrumAddressesSet(address inbox, address l1Router); - // Emitted when the L2 GRT address has been updated + /// Emitted when the L2 GRT address has been updated event L2TokenAddressSet(address l2GRT); - // Emitted when the counterpart L2GraphTokenGateway address has been updated + /// Emitted when the counterpart L2GraphTokenGateway address has been updated event L2CounterpartAddressSet(address l2Counterpart); - // Emitted when the escrow address has been updated + /// Emitted when the escrow address has been updated event EscrowAddressSet(address escrow); - // Emitted when an address is added to the callhook whitelist - event AddedToCallhookWhitelist(address newWhitelisted); - // Emitted when an address is removed from the callhook whitelist - event RemovedFromCallhookWhitelist(address notWhitelisted); - // Emitted when the L2 mint allowance per block is updated + /// Emitted when an address is added to the callhook allowlist + event AddedToCallhookAllowlist(address newAllowlisted); + /// Emitted when an address is removed from the callhook allowlist + event RemovedFromCallhookAllowlist(address notAllowlisted); + /// Emitted when the L2 mint allowance per block is updated event L2MintAllowanceUpdated( uint256 accumulatedL2MintAllowanceSnapshot, uint256 l2MintAllowancePerBlock, uint256 lastL2MintAllowanceUpdateBlock ); - // Emitted when tokens are minted due to an incoming transfer from L2 + /// Emitted when tokens are minted due to an incoming transfer from L2 event TokensMintedFromL2(uint256 amount); /** @@ -88,6 +95,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { */ modifier onlyL2Counterpart() { require(inbox != address(0), "INBOX_NOT_SET"); + require(l2Counterpart != address(0), "L2_COUNTERPART_NOT_SET"); // a message coming from the counterpart gateway was executed by the bridge IBridge bridge = IInbox(inbox).bridge(); @@ -100,38 +108,41 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { } /** - * @dev Initialize this contract. - * The contract will be paused. + * @notice Initialize the L1GraphTokenGateway contract. + * @dev The contract will be paused. * Note some parameters have to be set separately as they are generally * not expected to be available at initialization time: * - inbox and l1Router using setArbitrumAddresses * - l2GRT using setL2TokenAddress * - l2Counterpart using setL2CounterpartAddress * - escrow using setEscrowAddress - * - whitelisted callhook callers using addToCallhookWhitelist + * - allowlisted callhook callers using addToCallhookAllowlist * - pauseGuardian using setPauseGuardian * @param _controller Address of the Controller that manages this contract */ - function initialize(address _controller) external onlyImpl { + function initialize(address _controller) external onlyImpl initializer { Managed._initialize(_controller); _paused = true; } /** - * @dev Sets the addresses for L1 contracts provided by Arbitrum + * @notice Sets the addresses for L1 contracts provided by Arbitrum * @param _inbox Address of the Inbox that is part of the Arbitrum Bridge * @param _l1Router Address of the Gateway Router */ function setArbitrumAddresses(address _inbox, address _l1Router) external onlyGovernor { require(_inbox != address(0), "INVALID_INBOX"); require(_l1Router != address(0), "INVALID_L1_ROUTER"); + require(!callhookAllowlist[_l1Router], "ROUTER_CANT_BE_ALLOWLISTED"); + require(AddressUpgradeable.isContract(_inbox), "INBOX_MUST_BE_CONTRACT"); + require(AddressUpgradeable.isContract(_l1Router), "ROUTER_MUST_BE_CONTRACT"); inbox = _inbox; l1Router = _l1Router; emit ArbitrumAddressesSet(_inbox, _l1Router); } /** - * @dev Sets the address of the L2 Graph Token + * @notice Sets the address of the L2 Graph Token * @param _l2GRT Address of the GRT contract on L2 */ function setL2TokenAddress(address _l2GRT) external onlyGovernor { @@ -141,7 +152,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { } /** - * @dev Sets the address of the counterpart gateway on L2 + * @notice Sets the address of the counterpart gateway on L2 * @param _l2Counterpart Address of the corresponding L2GraphTokenGateway on Arbitrum */ function setL2CounterpartAddress(address _l2Counterpart) external onlyGovernor { @@ -151,37 +162,40 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { } /** - * @dev Sets the address of the escrow contract on L1 + * @notice Sets the address of the escrow contract on L1 * @param _escrow Address of the BridgeEscrow */ function setEscrowAddress(address _escrow) external onlyGovernor { - require(_escrow != address(0) && Address.isContract(_escrow), "INVALID_ESCROW"); + require(_escrow != address(0), "INVALID_ESCROW"); + require(AddressUpgradeable.isContract(_escrow), "MUST_BE_CONTRACT"); escrow = _escrow; emit EscrowAddressSet(_escrow); } /** - * @dev Adds an address to the callhook whitelist. + * @notice Adds an address to the callhook allowlist. * This address will be allowed to include callhooks when transferring tokens. - * @param _newWhitelisted Address to add to the whitelist + * @param _newAllowlisted Address to add to the allowlist */ - function addToCallhookWhitelist(address _newWhitelisted) external onlyGovernor { - require(_newWhitelisted != address(0), "INVALID_ADDRESS"); - require(!callhookWhitelist[_newWhitelisted], "ALREADY_WHITELISTED"); - callhookWhitelist[_newWhitelisted] = true; - emit AddedToCallhookWhitelist(_newWhitelisted); + function addToCallhookAllowlist(address _newAllowlisted) external onlyGovernor { + require(_newAllowlisted != address(0), "INVALID_ADDRESS"); + require(_newAllowlisted != l1Router, "CANT_ALLOW_ROUTER"); + require(AddressUpgradeable.isContract(_newAllowlisted), "MUST_BE_CONTRACT"); + require(!callhookAllowlist[_newAllowlisted], "ALREADY_ALLOWLISTED"); + callhookAllowlist[_newAllowlisted] = true; + emit AddedToCallhookAllowlist(_newAllowlisted); } /** - * @dev Removes an address from the callhook whitelist. + * @notice Removes an address from the callhook allowlist. * This address will no longer be allowed to include callhooks when transferring tokens. - * @param _notWhitelisted Address to remove from the whitelist + * @param _notAllowlisted Address to remove from the allowlist */ - function removeFromCallhookWhitelist(address _notWhitelisted) external onlyGovernor { - require(_notWhitelisted != address(0), "INVALID_ADDRESS"); - require(callhookWhitelist[_notWhitelisted], "NOT_WHITELISTED"); - callhookWhitelist[_notWhitelisted] = false; - emit RemovedFromCallhookWhitelist(_notWhitelisted); + function removeFromCallhookAllowlist(address _notAllowlisted) external onlyGovernor { + require(_notAllowlisted != address(0), "INVALID_ADDRESS"); + require(callhookAllowlist[_notAllowlisted], "NOT_ALLOWLISTED"); + callhookAllowlist[_notAllowlisted] = false; + emit RemovedFromCallhookAllowlist(_notAllowlisted); } /** @@ -240,15 +254,15 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * The ticket must be redeemed on L2 to receive tokens at the specified address. * Note that the caller must previously allow the gateway to spend the specified amount of GRT. * @dev maxGas and gasPriceBid must be set using Arbitrum's NodeInterface.estimateRetryableTicket method. - * Also note that whitelisted senders (some protocol contracts) can include additional calldata + * Also note that allowlisted senders (some protocol contracts) can include additional calldata * for a callhook to be executed on the L2 side when the tokens are received. In this case, the L2 transaction * can revert if the callhook reverts, potentially locking the tokens on the bridge if the callhook - * never succeeds. This requires extra care when adding contracts to the whitelist, but is necessary to ensure that + * never succeeds. This requires extra care when adding contracts to the allowlist, but is necessary to ensure that * the tickets can be retried in the case of a temporary failure, and to ensure the atomicity of callhooks * with token transfers. * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) * @param _to Recipient address on L2 - * @param _amount Amount of tokens to tranfer + * @param _amount Amount of tokens to transfer * @param _maxGas Gas limit for L2 execution of the ticket * @param _gasPriceBid Price per gas on L2 * @param _data Encoded maxSubmissionCost and sender address along with additional calldata @@ -263,8 +277,8 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { bytes calldata _data ) external payable override notPaused returns (bytes memory) { IGraphToken token = graphToken(); + require(_amount != 0, "INVALID_ZERO_AMOUNT"); require(_l1Token == address(token), "TOKEN_NOT_GRT"); - require(_amount > 0, "INVALID_ZERO_AMOUNT"); require(_to != address(0), "INVALID_DESTINATION"); // nested scopes to avoid stack too deep errors @@ -275,20 +289,12 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { bytes memory outboundCalldata; { bytes memory extraData; - (from, maxSubmissionCost, extraData) = parseOutboundData(_data); + (from, maxSubmissionCost, extraData) = _parseOutboundData(_data); require( - extraData.length == 0 || callhookWhitelist[msg.sender] == true, + extraData.length == 0 || callhookAllowlist[msg.sender] == true, "CALL_HOOK_DATA_NOT_ALLOWED" ); - require(maxSubmissionCost > 0, "NO_SUBMISSION_COST"); - - { - // makes sure only sufficient ETH is supplied as required for successful redemption on L2 - // if a user does not desire immediate redemption they should provide - // a msg.value of AT LEAST maxSubmissionCost - uint256 expectedEth = maxSubmissionCost.add(_maxGas.mul(_gasPriceBid)); - require(msg.value >= expectedEth, "WRONG_ETH_VALUE"); - } + require(maxSubmissionCost != 0, "NO_SUBMISSION_COST"); outboundCalldata = getOutboundCalldata(_l1Token, from, _to, _amount, extraData); } { @@ -323,7 +329,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * and the encoded exitNum is assumed to be 0. * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) * @param _from Address of the sender - * @param _to Recepient address on L1 + * @param _to Recipient address on L1 * @param _amount Amount of tokens transferred */ function finalizeInboundTransfer( @@ -347,37 +353,27 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { } /** - * @notice Decodes calldata required for migration of tokens - * @dev Data must include maxSubmissionCost, extraData can be left empty. When the router - * sends an outbound message, data also contains the from address. - * @param _data encoded callhook data - * @return Sender of the tx - * @return Base ether value required to keep retryable ticket alive - * @return Additional data sent to L2 + * @notice Calculate the L2 address of a bridged token + * @dev In our case, this would only work for GRT. + * @param _l1ERC20 address of L1 GRT contract + * @return L2 address of the bridged GRT token */ - function parseOutboundData(bytes memory _data) - private - view - returns ( - address, - uint256, - bytes memory - ) - { - address from; - uint256 maxSubmissionCost; - bytes memory extraData; - if (msg.sender == l1Router) { - // Data encoded by the Gateway Router includes the sender address - (from, extraData) = abi.decode(_data, (address, bytes)); - } else { - from = msg.sender; - extraData = _data; + function calculateL2TokenAddress(address _l1ERC20) external view override returns (address) { + IGraphToken token = graphToken(); + if (_l1ERC20 != address(token)) { + return address(0); } - // User-encoded data contains the max retryable ticket submission cost - // and additional L2 calldata - (maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); - return (from, maxSubmissionCost, extraData); + return l2GRT; + } + + /** + * @notice Get the address of the L2GraphTokenGateway + * @dev This is added for compatibility with the Arbitrum Router's + * gateway registration process. + * @return Address of the L2 gateway connected to this gateway + */ + function counterpartGateway() external view returns (address) { + return l2Counterpart; } /** @@ -388,7 +384,7 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { * @param _from Address on L1 from which we're transferring tokens * @param _to Address on L2 to which we're transferring tokens * @param _amount Amount of GRT to transfer - * @param _data Additional call data for the L2 transaction, which must be empty unless the caller is whitelisted + * @param _data Additional call data for the L2 transaction, which must be empty unless the caller is allowlisted * @return Encoded calldata (including function selector) for the L2 transaction */ function getOutboundCalldata( @@ -398,8 +394,6 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { uint256 _amount, bytes memory _data ) public pure returns (bytes memory) { - bytes memory emptyBytes; - return abi.encodeWithSelector( ITokenGateway.finalizeInboundTransfer.selector, @@ -407,22 +401,53 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger { _from, _to, _amount, - abi.encode(emptyBytes, _data) + _data ); } /** - * @notice Calculate the L2 address of a bridged token - * @dev In our case, this would only work for GRT. - * @param _l1ERC20 address of L1 GRT contract - * @return L2 address of the bridged GRT token + * @dev Runs state validation before unpausing, reverts if + * something is not set properly */ - function calculateL2TokenAddress(address _l1ERC20) external view override returns (address) { - IGraphToken token = graphToken(); - if (_l1ERC20 != address(token)) { - return address(0); + function _checksBeforeUnpause() internal view override { + require(inbox != address(0), "INBOX_NOT_SET"); + require(l1Router != address(0), "ROUTER_NOT_SET"); + require(l2Counterpart != address(0), "L2_COUNTERPART_NOT_SET"); + require(escrow != address(0), "ESCROW_NOT_SET"); + } + + /** + * @notice Decodes calldata required for migration of tokens + * @dev Data must include maxSubmissionCost, extraData can be left empty. When the router + * sends an outbound message, data also contains the from address. + * @param _data encoded callhook data + * @return Sender of the tx + * @return Base ether value required to keep retryable ticket alive + * @return Additional data sent to L2 + */ + function _parseOutboundData(bytes calldata _data) + private + view + returns ( + address, + uint256, + bytes memory + ) + { + address from; + uint256 maxSubmissionCost; + bytes memory extraData; + if (msg.sender == l1Router) { + // Data encoded by the Gateway Router includes the sender address + (from, extraData) = abi.decode(_data, (address, bytes)); + } else { + from = msg.sender; + extraData = _data; } - return l2GRT; + // User-encoded data contains the max retryable ticket submission cost + // and additional L2 calldata + (maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); + return (from, maxSubmissionCost, extraData); } /** diff --git a/contracts/governance/Controller.sol b/contracts/governance/Controller.sol index 2e4418a59..bc287d2be 100644 --- a/contracts/governance/Controller.sol +++ b/contracts/governance/Controller.sol @@ -2,10 +2,10 @@ pragma solidity ^0.7.6; -import "./IController.sol"; -import "./IManaged.sol"; -import "./Governed.sol"; -import "./Pausable.sol"; +import { IController } from "./IController.sol"; +import { IManaged } from "./IManaged.sol"; +import { Governed } from "./Governed.sol"; +import { Pausable } from "./Pausable.sol"; /** * @title Graph Controller contract @@ -13,13 +13,14 @@ import "./Pausable.sol"; * https://github.com/livepeer/protocol/blob/streamflow/contracts/Controller.sol */ contract Controller is Governed, Pausable, IController { - // Track contract ids to contract proxy address - mapping(bytes32 => address) private registry; + /// @dev Track contract ids to contract proxy address + mapping(bytes32 => address) private _registry; + /// Emitted when the proxy address for a protocol contract has been set event SetContractProxy(bytes32 indexed id, address contractAddress); /** - * @dev Contract constructor. + * @notice Controller contract constructor. */ constructor() { Governed._initialize(msg.sender); @@ -58,7 +59,7 @@ contract Controller is Governed, Pausable, IController { onlyGovernor { require(_contractAddress != address(0), "Contract address must be set"); - registry[_id] = _contractAddress; + _registry[_id] = _contractAddress; emit SetContractProxy(_id, _contractAddress); } @@ -67,16 +68,17 @@ contract Controller is Governed, Pausable, IController { * @param _id Contract id (keccak256 hash of contract name) */ function unsetContractProxy(bytes32 _id) external override onlyGovernor { - registry[_id] = address(0); + _registry[_id] = address(0); emit SetContractProxy(_id, address(0)); } /** * @notice Get contract proxy address by its id * @param _id Contract id + * @return Address of the proxy contract for the provided id */ - function getContractProxy(bytes32 _id) public view override returns (address) { - return registry[_id]; + function getContractProxy(bytes32 _id) external view override returns (address) { + return _registry[_id]; } /** @@ -86,7 +88,7 @@ contract Controller is Governed, Pausable, IController { */ function updateController(bytes32 _id, address _controller) external override onlyGovernor { require(_controller != address(0), "Controller must be set"); - return IManaged(registry[_id]).setController(_controller); + return IManaged(_registry[_id]).setController(_controller); } // -- Pausing -- @@ -94,17 +96,19 @@ contract Controller is Governed, Pausable, IController { /** * @notice Change the partial paused state of the contract * Partial pause is intended as a partial pause of the protocol + * @param _toPause True if the contracts should be (partially) paused, false otherwise */ - function setPartialPaused(bool _partialPaused) external override onlyGovernorOrGuardian { - _setPartialPaused(_partialPaused); + function setPartialPaused(bool _toPause) external override onlyGovernorOrGuardian { + _setPartialPaused(_toPause); } /** * @notice Change the paused state of the contract * Full pause most of protocol functions + * @param _toPause True if the contracts should be paused, false otherwise */ - function setPaused(bool _paused) external override onlyGovernorOrGuardian { - _setPaused(_paused); + function setPaused(bool _toPause) external override onlyGovernorOrGuardian { + _setPaused(_toPause); } /** @@ -118,6 +122,7 @@ contract Controller is Governed, Pausable, IController { /** * @notice Getter to access paused + * @return True if the contracts are paused, false otherwise */ function paused() external view override returns (bool) { return _paused; @@ -125,6 +130,7 @@ contract Controller is Governed, Pausable, IController { /** * @notice Getter to access partial pause status + * @return True if the contracts are partially paused, false otherwise */ function partialPaused() external view override returns (bool) { return _partialPaused; diff --git a/contracts/governance/Governed.sol b/contracts/governance/Governed.sol index 2a87389d9..f692b2d19 100644 --- a/contracts/governance/Governed.sol +++ b/contracts/governance/Governed.sol @@ -6,15 +6,19 @@ pragma solidity ^0.7.6; * @title Graph Governance contract * @dev All contracts that will be owned by a Governor entity should extend this contract. */ -contract Governed { +abstract contract Governed { // -- State -- + /// Address of the governor address public governor; + /// Address of the new governor that is pending acceptance address public pendingGovernor; // -- Events -- + /// Emitted when a new owner/governor has been set, but is pending acceptance event NewPendingOwnership(address indexed from, address indexed to); + /// Emitted when a new owner/governor has accepted their role event NewOwnership(address indexed from, address indexed to); /** @@ -26,14 +30,15 @@ contract Governed { } /** - * @dev Initialize the governor to the contract caller. + * @dev Initialize the governor for this contract + * @param _initGovernor Address of the governor */ function _initialize(address _initGovernor) internal { governor = _initGovernor; } /** - * @dev Admin function to begin change of governor. The `_newGovernor` must call + * @notice Admin function to begin change of governor. The `_newGovernor` must call * `acceptOwnership` to finalize the transfer. * @param _newGovernor Address of new `governor` */ @@ -47,19 +52,20 @@ contract Governed { } /** - * @dev Admin function for pending governor to accept role and update governor. + * @notice Admin function for pending governor to accept role and update governor. * This function must called by the pending governor. */ function acceptOwnership() external { + address oldPendingGovernor = pendingGovernor; + require( - pendingGovernor != address(0) && msg.sender == pendingGovernor, + oldPendingGovernor != address(0) && msg.sender == oldPendingGovernor, "Caller must be pending governor" ); address oldGovernor = governor; - address oldPendingGovernor = pendingGovernor; - governor = pendingGovernor; + governor = oldPendingGovernor; pendingGovernor = address(0); emit NewOwnership(oldGovernor, governor); diff --git a/contracts/governance/Managed.sol b/contracts/governance/Managed.sol index ce18fb009..6b8fe624e 100644 --- a/contracts/governance/Managed.sol +++ b/contracts/governance/Managed.sol @@ -2,14 +2,16 @@ pragma solidity ^0.7.6; -import "./IController.sol"; +import { IController } from "./IController.sol"; -import "../curation/ICuration.sol"; -import "../epochs/IEpochManager.sol"; -import "../rewards/IRewardsManager.sol"; -import "../staking/IStaking.sol"; -import "../token/IGraphToken.sol"; -import "../arbitrum/ITokenGateway.sol"; +import { ICuration } from "../curation/ICuration.sol"; +import { IEpochManager } from "../epochs/IEpochManager.sol"; +import { IRewardsManager } from "../rewards/IRewardsManager.sol"; +import { IStaking } from "../staking/IStaking.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; +import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; + +import { IManaged } from "./IManaged.sol"; /** * @title Graph Managed contract @@ -20,60 +22,92 @@ import "../arbitrum/ITokenGateway.sol"; * Inspired by Livepeer: * https://github.com/livepeer/protocol/blob/streamflow/contracts/Controller.sol */ -contract Managed { +abstract contract Managed is IManaged { // -- State -- - // Controller that contract is registered with + /// Controller that contract is registered with IController public controller; - mapping(bytes32 => address) private addressCache; + /// @dev Cache for the addresses of the contracts retrieved from the controller + mapping(bytes32 => address) private _addressCache; + /// @dev Gap for future storage variables uint256[10] private __gap; + // Immutables + bytes32 private immutable CURATION = keccak256("Curation"); + bytes32 private immutable EPOCH_MANAGER = keccak256("EpochManager"); + bytes32 private immutable REWARDS_MANAGER = keccak256("RewardsManager"); + bytes32 private immutable STAKING = keccak256("Staking"); + bytes32 private immutable GRAPH_TOKEN = keccak256("GraphToken"); + bytes32 private immutable GRAPH_TOKEN_GATEWAY = keccak256("GraphTokenGateway"); + // -- Events -- + /// Emitted when a contract parameter has been updated event ParameterUpdated(string param); + /// Emitted when the controller address has been set event SetController(address controller); - /** - * @dev Emitted when contract with `nameHash` is synced to `contractAddress`. - */ + /// Emitted when contract with `nameHash` is synced to `contractAddress`. event ContractSynced(bytes32 indexed nameHash, address contractAddress); // -- Modifiers -- + /** + * @dev Revert if the controller is paused or partially paused + */ function _notPartialPaused() internal view { require(!controller.paused(), "Paused"); require(!controller.partialPaused(), "Partial-paused"); } + /** + * @dev Revert if the controller is paused + */ function _notPaused() internal view virtual { require(!controller.paused(), "Paused"); } + /** + * @dev Revert if the caller is not the governor + */ function _onlyGovernor() internal view { - require(msg.sender == controller.getGovernor(), "Caller must be Controller governor"); + require(msg.sender == controller.getGovernor(), "Only Controller governor"); } + /** + * @dev Revert if the caller is not the Controller + */ function _onlyController() internal view { require(msg.sender == address(controller), "Caller must be Controller"); } + /** + * @dev Revert if the controller is paused or partially paused + */ modifier notPartialPaused() { _notPartialPaused(); _; } + /** + * @dev Revert if the controller is paused + */ modifier notPaused() { _notPaused(); _; } - // Check if sender is controller. + /** + * @dev Revert if the caller is not the Controller + */ modifier onlyController() { _onlyController(); _; } - // Check if sender is the governor. + /** + * @dev Revert if the caller is not the governor + */ modifier onlyGovernor() { _onlyGovernor(); _; @@ -82,7 +116,8 @@ contract Managed { // -- Functions -- /** - * @dev Initialize the controller. + * @dev Initialize a Managed contract + * @param _controller Address for the Controller that manages this contract */ function _initialize(address _controller) internal { _setController(_controller); @@ -92,7 +127,7 @@ contract Managed { * @notice Set Controller. Only callable by current controller. * @param _controller Controller contract address */ - function setController(address _controller) external onlyController { + function setController(address _controller) external override onlyController { _setController(_controller); } @@ -107,59 +142,60 @@ contract Managed { } /** - * @dev Return Curation interface. + * @dev Return Curation interface * @return Curation contract registered with Controller */ function curation() internal view returns (ICuration) { - return ICuration(_resolveContract(keccak256("Curation"))); + return ICuration(_resolveContract(CURATION)); } /** - * @dev Return EpochManager interface. + * @dev Return EpochManager interface * @return Epoch manager contract registered with Controller */ function epochManager() internal view returns (IEpochManager) { - return IEpochManager(_resolveContract(keccak256("EpochManager"))); + return IEpochManager(_resolveContract(EPOCH_MANAGER)); } /** - * @dev Return RewardsManager interface. + * @dev Return RewardsManager interface * @return Rewards manager contract registered with Controller */ function rewardsManager() internal view returns (IRewardsManager) { - return IRewardsManager(_resolveContract(keccak256("RewardsManager"))); + return IRewardsManager(_resolveContract(REWARDS_MANAGER)); } /** - * @dev Return Staking interface. + * @dev Return Staking interface * @return Staking contract registered with Controller */ function staking() internal view returns (IStaking) { - return IStaking(_resolveContract(keccak256("Staking"))); + return IStaking(_resolveContract(STAKING)); } /** - * @dev Return GraphToken interface. + * @dev Return GraphToken interface * @return Graph token contract registered with Controller */ function graphToken() internal view returns (IGraphToken) { - return IGraphToken(_resolveContract(keccak256("GraphToken"))); + return IGraphToken(_resolveContract(GRAPH_TOKEN)); } /** - * @dev Return GraphTokenGateway (L1 or L2) interface. + * @dev Return GraphTokenGateway (L1 or L2) interface * @return Graph token gateway contract registered with Controller */ function graphTokenGateway() internal view returns (ITokenGateway) { - return ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); + return ITokenGateway(_resolveContract(GRAPH_TOKEN_GATEWAY)); } /** - * @dev Resolve a contract address from the cache or the Controller if not found. + * @dev Resolve a contract address from the cache or the Controller if not found + * @param _nameHash keccak256 hash of the contract name * @return Address of the contract */ function _resolveContract(bytes32 _nameHash) internal view returns (address) { - address contractAddress = addressCache[_nameHash]; + address contractAddress = _addressCache[_nameHash]; if (contractAddress == address(0)) { contractAddress = controller.getContractProxy(_nameHash); } @@ -173,15 +209,15 @@ contract Managed { function _syncContract(string memory _name) internal { bytes32 nameHash = keccak256(abi.encodePacked(_name)); address contractAddress = controller.getContractProxy(nameHash); - if (addressCache[nameHash] != contractAddress) { - addressCache[nameHash] = contractAddress; + if (_addressCache[nameHash] != contractAddress) { + _addressCache[nameHash] = contractAddress; emit ContractSynced(nameHash, contractAddress); } } /** - * @dev Sync protocol contract addresses from the Controller registry. - * This function will cache all the contracts using the latest addresses + * @notice Sync protocol contract addresses from the Controller registry + * @dev This function will cache all the contracts using the latest addresses * Anyone can call the function whenever a Proxy contract change in the * controller to ensure the protocol is using the latest version */ diff --git a/contracts/governance/Pausable.sol b/contracts/governance/Pausable.sol index b1ecbdf30..552b0aa15 100644 --- a/contracts/governance/Pausable.sol +++ b/contracts/governance/Pausable.sol @@ -2,26 +2,36 @@ pragma solidity ^0.7.6; -contract Pausable { - // Partial paused paused exit and enter functions for GRT, but not internal - // functions, such as allocating +abstract contract Pausable { + /** + * @dev "Partial paused" pauses exit and enter functions for GRT, but not internal + * functions, such as allocating + */ bool internal _partialPaused; - // Paused will pause all major protocol functions + /** + * @dev Paused will pause all major protocol functions + */ bool internal _paused; - // Time last paused for both pauses + /// Timestamp for the last time the partial pause was set uint256 public lastPausePartialTime; + /// Timestamp for the last time the full pause was set uint256 public lastPauseTime; - // Pause guardian is a separate entity from the governor that can pause + /// Pause guardian is a separate entity from the governor that can + /// pause and unpause the protocol, fully or partially address public pauseGuardian; + /// Emitted when the partial pause state changed event PartialPauseChanged(bool isPaused); + /// Emitted when the full pause state changed event PauseChanged(bool isPaused); + /// Emitted when the pause guardian is changed event NewPauseGuardian(address indexed oldPauseGuardian, address indexed pauseGuardian); /** - * @notice Change the partial paused state of the contract + * @dev Change the partial paused state of the contract + * @param _toPause New value for the partial pause state (true means the contracts will be partially paused) */ function _setPartialPaused(bool _toPause) internal { if (_toPause == _partialPaused) { @@ -35,7 +45,8 @@ contract Pausable { } /** - * @notice Change the paused state of the contract + * @dev Change the paused state of the contract + * @param _toPause New value for the pause state (true means the contracts will be paused) */ function _setPaused(bool _toPause) internal { if (_toPause == _paused) { @@ -49,7 +60,7 @@ contract Pausable { } /** - * @notice Change the Pause Guardian + * @dev Change the Pause Guardian * @param newPauseGuardian The address of the new Pause Guardian */ function _setPauseGuardian(address newPauseGuardian) internal { diff --git a/contracts/l2/gateway/L2GraphTokenGateway.sol b/contracts/l2/gateway/L2GraphTokenGateway.sol index 8757a06ca..7af102fb4 100644 --- a/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -3,14 +3,16 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts/math/SafeMath.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; -import "../../arbitrum/L2ArbitrumMessenger.sol"; -import "../../arbitrum/AddressAliasHelper.sol"; -import "../../gateway/GraphTokenGateway.sol"; -import "../../gateway/ICallhookReceiver.sol"; -import "../token/L2GraphToken.sol"; +import { L2ArbitrumMessenger } from "../../arbitrum/L2ArbitrumMessenger.sol"; +import { AddressAliasHelper } from "../../arbitrum/AddressAliasHelper.sol"; +import { ITokenGateway } from "../../arbitrum/ITokenGateway.sol"; +import { Managed } from "../../governance/Managed.sol"; +import { GraphTokenGateway } from "../../gateway/GraphTokenGateway.sol"; +import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; +import { L2GraphToken } from "../token/L2GraphToken.sol"; /** * @title L2 Graph Token Gateway Contract @@ -21,22 +23,22 @@ import "../token/L2GraphToken.sol"; * and https://github.com/livepeer/arbitrum-lpt-bridge) */ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, ReentrancyGuardUpgradeable { - using SafeMath for uint256; + using SafeMathUpgradeable for uint256; - // Address of the Graph Token contract on L1 + /// Address of the Graph Token contract on L1 address public l1GRT; - // Address of the L1GraphTokenGateway that is the counterpart of this gateway on L1 + /// Address of the L1GraphTokenGateway that is the counterpart of this gateway on L1 address public l1Counterpart; - // Address of the Arbitrum Gateway Router on L2 + /// Address of the Arbitrum Gateway Router on L2 address public l2Router; - // Calldata included in an outbound transfer, stored as a structure for convenience and stack depth + /// @dev Calldata included in an outbound transfer, stored as a structure for convenience and stack depth struct OutboundCalldata { address from; bytes extraData; } - // Emitted when an incoming transfer is finalized, i.e. tokens were deposited from L1 to L2 + /// Emitted when an incoming transfer is finalized, i.e. tokens were deposited from L1 to L2 event DepositFinalized( address indexed l1Token, address indexed from, @@ -44,7 +46,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran uint256 amount ); - // Emitted when an outbound transfer is initiated, i.e. tokens are being withdrawn from L2 back to L1 + /// Emitted when an outbound transfer is initiated, i.e. tokens are being withdrawn from L2 back to L1 event WithdrawalInitiated( address l1Token, address indexed from, @@ -54,11 +56,11 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran uint256 amount ); - // Emitted when the Arbitrum Gateway Router address on L2 has been updated + /// Emitted when the Arbitrum Gateway Router address on L2 has been updated event L2RouterSet(address l2Router); - // Emitted when the L1 Graph Token address has been updated + /// Emitted when the L1 Graph Token address has been updated event L1TokenAddressSet(address l1GRT); - // Emitted when the address of the counterpart gateway on L1 has been updated + /// Emitted when the address of the counterpart gateway on L1 has been updated event L1CounterpartAddressSet(address l1Counterpart); /** @@ -74,8 +76,8 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran } /** - * @dev Initialize this contract. - * The contract will be paused. + * @notice Initialize the L2GraphTokenGateway contract. + * @dev The contract will be paused. * Note some parameters have to be set separately as they are generally * not expected to be available at initialization time: * - l2Router using setL2Router @@ -84,14 +86,14 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran * - pauseGuardian using setPauseGuardian * @param _controller Address of the Controller that manages this contract */ - function initialize(address _controller) external onlyImpl { + function initialize(address _controller) external onlyImpl initializer { Managed._initialize(_controller); _paused = true; __ReentrancyGuard_init(); } /** - * @dev Sets the address of the Arbitrum Gateway Router on L2 + * @notice Sets the address of the Arbitrum Gateway Router on L2 * @param _l2Router Address of the L2 Router (provided by Arbitrum) */ function setL2Router(address _l2Router) external onlyGovernor { @@ -101,7 +103,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran } /** - * @dev Sets the address of the Graph Token on L1 + * @notice Sets the address of the Graph Token on L1 * @param _l1GRT L1 address of the Graph Token contract */ function setL1TokenAddress(address _l1GRT) external onlyGovernor { @@ -111,7 +113,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran } /** - * @dev Sets the address of the counterpart gateway on L1 + * @notice Sets the address of the counterpart gateway on L1 * @param _l1Counterpart Address of the L1GraphTokenGateway on L1 */ function setL1CounterpartAddress(address _l1Counterpart) external onlyGovernor { @@ -120,6 +122,61 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran emit L1CounterpartAddressSet(_l1Counterpart); } + /** + * @notice Burns L2 tokens and initiates a transfer to L1. + * The tokens will be received on L1 only after the wait period (7 days) is over, + * and will require an Outbox.executeTransaction to finalize. + * @dev no additional callhook data is allowed + * @param _l1Token L1 Address of GRT (needed for compatibility with Arbitrum Gateway Router) + * @param _to Recipient address on L1 + * @param _amount Amount of tokens to burn + * @param _data Contains sender and additional data to send to L1 + * @return ID of the withdraw tx + */ + function outboundTransfer( + address _l1Token, + address _to, + uint256 _amount, + bytes calldata _data + ) external returns (bytes memory) { + return outboundTransfer(_l1Token, _to, _amount, 0, 0, _data); + } + + /** + * @notice Receives token amount from L1 and mints the equivalent tokens to the receiving address + * @dev Only accepts transactions from the L1 GRT Gateway. + * The function is payable for ITokenGateway compatibility, but msg.value must be zero. + * Note that allowlisted senders (some protocol contracts) can include additional calldata + * for a callhook to be executed on the L2 side when the tokens are received. In this case, the L2 transaction + * can revert if the callhook reverts, potentially locking the tokens on the bridge if the callhook + * never succeeds. This requires extra care when adding contracts to the allowlist, but is necessary to ensure that + * the tickets can be retried in the case of a temporary failure, and to ensure the atomicity of callhooks + * with token transfers. + * @param _l1Token L1 Address of GRT + * @param _from Address of the sender on L1 + * @param _to Recipient address on L2 + * @param _amount Amount of tokens transferred + * @param _data Extra callhook data, only used when the sender is allowlisted + */ + function finalizeInboundTransfer( + address _l1Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable override nonReentrant notPaused onlyL1Counterpart { + require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); + require(msg.value == 0, "INVALID_NONZERO_VALUE"); + + L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeMint(_to, _amount); + + if (_data.length > 0) { + ICallhookReceiver(_to).onTokenTransfer(_from, _amount, _data); + } + + emit DepositFinalized(_l1Token, _from, _to, _amount); + } + /** * @notice Burns L2 tokens and initiates a transfer to L1. * The tokens will be available on L1 only after the wait period (7 days) is over, @@ -141,15 +198,15 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran uint256, // unused on L2 uint256, // unused on L2 bytes calldata _data - ) public payable override notPaused nonReentrant returns (bytes memory) { + ) public payable override nonReentrant notPaused returns (bytes memory) { require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); - require(_amount > 0, "INVALID_ZERO_AMOUNT"); + require(_amount != 0, "INVALID_ZERO_AMOUNT"); require(msg.value == 0, "INVALID_NONZERO_VALUE"); require(_to != address(0), "INVALID_DESTINATION"); OutboundCalldata memory outboundCalldata; - (outboundCalldata.from, outboundCalldata.extraData) = parseOutboundData(_data); + (outboundCalldata.from, outboundCalldata.extraData) = _parseOutboundData(_data); require(outboundCalldata.extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED"); // from needs to approve this contract to burn the amount first @@ -174,26 +231,6 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran return abi.encode(id); } - /** - * @notice Burns L2 tokens and initiates a transfer to L1. - * The tokens will be received on L1 only after the wait period (7 days) is over, - * and will require an Outbox.executeTransaction to finalize. - * @dev no additional callhook data is allowed - * @param _l1Token L1 Address of GRT (needed for compatibility with Arbitrum Gateway Router) - * @param _to Recipient address on L1 - * @param _amount Amount of tokens to burn - * @param _data Contains sender and additional data to send to L1 - * @return ID of the withdraw tx - */ - function outboundTransfer( - address _l1Token, - address _to, - uint256 _amount, - bytes calldata _data - ) external returns (bytes memory) { - return outboundTransfer(_l1Token, _to, _amount, 0, 0, _data); - } - /** * @notice Calculate the L2 address of a bridged token * @dev In our case, this would only work for GRT. @@ -207,46 +244,6 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran return address(graphToken()); } - /** - * @notice Receives token amount from L1 and mints the equivalent tokens to the receiving address - * @dev Only accepts transactions from the L1 GRT Gateway. - * The function is payable for ITokenGateway compatibility, but msg.value must be zero. - * Note that whitelisted senders (some protocol contracts) can include additional calldata - * for a callhook to be executed on the L2 side when the tokens are received. In this case, the L2 transaction - * can revert if the callhook reverts, potentially locking the tokens on the bridge if the callhook - * never succeeds. This requires extra care when adding contracts to the whitelist, but is necessary to ensure that - * the tickets can be retried in the case of a temporary failure, and to ensure the atomicity of callhooks - * with token transfers. - * @param _l1Token L1 Address of GRT - * @param _from Address of the sender on L1 - * @param _to Recipient address on L2 - * @param _amount Amount of tokens transferred - * @param _data Extra callhook data, only used when the sender is whitelisted - */ - function finalizeInboundTransfer( - address _l1Token, - address _from, - address _to, - uint256 _amount, - bytes calldata _data - ) external payable override notPaused onlyL1Counterpart nonReentrant { - require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); - require(msg.value == 0, "INVALID_NONZERO_VALUE"); - - L2GraphToken(calculateL2TokenAddress(l1GRT)).bridgeMint(_to, _amount); - - if (_data.length > 0) { - bytes memory callhookData; - { - bytes memory gatewayData; - (gatewayData, callhookData) = abi.decode(_data, (bytes, bytes)); - } - ICallhookReceiver(_to).onTokenTransfer(_from, _amount, callhookData); - } - - emit DepositFinalized(_l1Token, _from, _to, _amount); - } - /** * @notice Creates calldata required to send tx to L1 * @dev encodes the target function with its params which @@ -276,6 +273,16 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran ); } + /** + * @dev Runs state validation before unpausing, reverts if + * something is not set properly + */ + function _checksBeforeUnpause() internal view override { + require(l2Router != address(0), "L2_ROUTER_NOT_SET"); + require(l1Counterpart != address(0), "L1_COUNTERPART_NOT_SET"); + require(l1GRT != address(0), "L1_GRT_NOT_SET"); + } + /** * @notice Decodes calldata required for migration of tokens * @dev extraData can be left empty @@ -283,7 +290,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran * @return Sender of the tx * @return Any other data sent to L1 */ - function parseOutboundData(bytes memory _data) private view returns (address, bytes memory) { + function _parseOutboundData(bytes calldata _data) private view returns (address, bytes memory) { address from; bytes memory extraData; if (msg.sender == l2Router) { diff --git a/contracts/l2/token/GraphTokenUpgradeable.sol b/contracts/l2/token/GraphTokenUpgradeable.sol index 6d003c1c3..3f7882b5c 100644 --- a/contracts/l2/token/GraphTokenUpgradeable.sol +++ b/contracts/l2/token/GraphTokenUpgradeable.sol @@ -2,12 +2,11 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol"; -import "@openzeppelin/contracts/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/math/SafeMath.sol"; +import { ERC20BurnableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol"; +import { ECDSAUpgradeable } from "@openzeppelin/contracts-upgradeable/cryptography/ECDSAUpgradeable.sol"; -import "../../upgrades/GraphUpgradeable.sol"; -import "../../governance/Governed.sol"; +import { GraphUpgradeable } from "../../upgrades/GraphUpgradeable.sol"; +import { Governed } from "../../governance/Governed.sol"; /** * @title GraphTokenUpgradeable contract @@ -25,44 +24,54 @@ import "../../governance/Governed.sol"; * initializer functions and upgradeable OpenZeppelin contracts instead of * the original's constructor + non-upgradeable approach. */ -contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20BurnableUpgradeable { - using SafeMath for uint256; - +abstract contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20BurnableUpgradeable { // -- EIP712 -- // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-domainseparator - bytes32 private constant DOMAIN_TYPE_HASH = + /// @dev Hash of the EIP-712 Domain type + bytes32 private immutable DOMAIN_TYPE_HASH = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" ); - bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Token"); - bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); - bytes32 private constant DOMAIN_SALT = + /// @dev Hash of the EIP-712 Domain name + bytes32 private immutable DOMAIN_NAME_HASH = keccak256("Graph Token"); + /// @dev Hash of the EIP-712 Domain version + bytes32 private immutable DOMAIN_VERSION_HASH = keccak256("0"); + /// @dev EIP-712 Domain salt + bytes32 private immutable DOMAIN_SALT = 0xe33842a7acd1d5a1d28f25a931703e5605152dc48d64dc4716efdae1f5659591; // Randomly generated salt - bytes32 private constant PERMIT_TYPEHASH = + /// @dev Hash of the EIP-712 permit type + bytes32 private immutable PERMIT_TYPEHASH = keccak256( "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" ); // -- State -- - // solhint-disable-next-line var-name-mixedcase - bytes32 private DOMAIN_SEPARATOR; + /// @dev EIP-712 Domain separator + bytes32 private DOMAIN_SEPARATOR; // solhint-disable-line var-name-mixedcase + /// @dev Addresses for which this mapping is true are allowed to mint tokens mapping(address => bool) private _minters; + /// Nonces for permit signatures for each token holder mapping(address => uint256) public nonces; + /// @dev Storage gap added in case we need to add state variables to this contract + uint256[47] private __gap; // -- Events -- + /// Emitted when a new minter is added event MinterAdded(address indexed account); + /// Emitted when a minter is removed event MinterRemoved(address indexed account); + /// @dev Reverts if the caller is not an authorized minter modifier onlyMinter() { require(isMinter(msg.sender), "Only minter can call"); _; } /** - * @dev Approve token allowance by validating a message signed by the holder. + * @notice Approve token allowance by validating a message signed by the holder. * @param _owner Address of the token holder * @param _spender Address of the approved spender * @param _value Amount of tokens to approve the spender @@ -80,6 +89,7 @@ contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20BurnableUpgra bytes32 _r, bytes32 _s ) external { + require(_deadline == 0 || block.timestamp <= _deadline, "GRT: expired permit"); bytes32 digest = keccak256( abi.encodePacked( "\x19\x01", @@ -90,16 +100,15 @@ contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20BurnableUpgra ) ); - address recoveredAddress = ECDSA.recover(digest, _v, _r, _s); + address recoveredAddress = ECDSAUpgradeable.recover(digest, _v, _r, _s); require(_owner == recoveredAddress, "GRT: invalid permit"); - require(_deadline == 0 || block.timestamp <= _deadline, "GRT: expired permit"); - nonces[_owner] = nonces[_owner].add(1); + nonces[_owner] = nonces[_owner] + 1; _approve(_owner, _spender, _value); } /** - * @dev Add a new minter. + * @notice Add a new minter. * @param _account Address of the minter */ function addMinter(address _account) external onlyGovernor { @@ -108,24 +117,24 @@ contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20BurnableUpgra } /** - * @dev Remove a minter. + * @notice Remove a minter. * @param _account Address of the minter */ function removeMinter(address _account) external onlyGovernor { - require(_minters[_account], "NOT_A_MINTER"); + require(isMinter(_account), "NOT_A_MINTER"); _removeMinter(_account); } /** - * @dev Renounce to be a minter. + * @notice Renounce being a minter. */ function renounceMinter() external { - require(_minters[msg.sender], "NOT_A_MINTER"); + require(isMinter(msg.sender), "NOT_A_MINTER"); _removeMinter(msg.sender); } /** - * @dev Mint new tokens. + * @notice Mint new tokens. * @param _to Address to send the newly minted tokens * @param _amount Amount of tokens to mint */ @@ -134,7 +143,7 @@ contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20BurnableUpgra } /** - * @dev Return if the `_account` is a minter or not. + * @notice Return if the `_account` is a minter or not. * @param _account Address to check * @return True if the `_account` is minter */ diff --git a/contracts/l2/token/L2GraphToken.sol b/contracts/l2/token/L2GraphToken.sol index ec6ca4eb8..639444870 100644 --- a/contracts/l2/token/L2GraphToken.sol +++ b/contracts/l2/token/L2GraphToken.sol @@ -2,10 +2,8 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts/math/SafeMath.sol"; - -import "./GraphTokenUpgradeable.sol"; -import "../../arbitrum/IArbToken.sol"; +import { GraphTokenUpgradeable } from "./GraphTokenUpgradeable.sol"; +import { IArbToken } from "../../arbitrum/IArbToken.sol"; /** * @title L2 Graph Token Contract @@ -13,20 +11,18 @@ import "../../arbitrum/IArbToken.sol"; * through the L2GraphTokenGateway. */ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { - using SafeMath for uint256; - - // Address of the gateway (on L2) that is allowed to mint tokens + /// Address of the gateway (on L2) that is allowed to mint tokens address public gateway; - // Address of the corresponding Graph Token contract on L1 + /// Address of the corresponding Graph Token contract on L1 address public override l1Address; - // Emitted when the bridge / gateway has minted new tokens, i.e. tokens were transferred to L2 + /// Emitted when the bridge / gateway has minted new tokens, i.e. tokens were transferred to L2 event BridgeMinted(address indexed account, uint256 amount); - // Emitted when the bridge / gateway has burned tokens, i.e. tokens were transferred back to L1 + /// Emitted when the bridge / gateway has burned tokens, i.e. tokens were transferred back to L1 event BridgeBurned(address indexed account, uint256 amount); - // Emitted when the address of the gateway has been updated + /// Emitted when the address of the gateway has been updated event GatewaySet(address gateway); - // Emitted when the address of the Graph Token contract on L1 has been updated + /// Emitted when the address of the Graph Token contract on L1 has been updated event L1AddressSet(address l1Address); /** @@ -38,14 +34,14 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { } /** - * @dev L2 Graph Token Contract initializer. - * Note some parameters have to be set separately as they are generally + * @notice L2 Graph Token Contract initializer. + * @dev Note some parameters have to be set separately as they are generally * not expected to be available at initialization time: * - gateway using setGateway * - l1Address using setL1Address * @param _owner Governance address that owns this contract */ - function initialize(address _owner) external onlyImpl { + function initialize(address _owner) external onlyImpl initializer { require(_owner != address(0), "Owner must be set"); // Initial supply hard coded to 0 as tokens are only supposed // to be minted through the bridge. @@ -53,17 +49,17 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { } /** - * @dev Sets the address of the L2 gateway allowed to mint tokens + * @notice Sets the address of the L2 gateway allowed to mint tokens * @param _gw Address for the L2GraphTokenGateway that will be allowed to mint tokens */ function setGateway(address _gw) external onlyGovernor { require(_gw != address(0), "INVALID_GATEWAY"); gateway = _gw; - emit GatewaySet(gateway); + emit GatewaySet(_gw); } /** - * @dev Sets the address of the counterpart token on L1 + * @notice Sets the address of the counterpart token on L1 * @param _addr Address for the GraphToken contract on L1 */ function setL1Address(address _addr) external onlyGovernor { @@ -73,7 +69,7 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { } /** - * @dev Increases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L2) + * @notice Increases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L2) * @param _account Address to credit with the new tokens * @param _amount Number of tokens to mint */ @@ -83,7 +79,7 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { } /** - * @dev Decreases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L1). + * @notice Decreases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L1). * @param _account Address from which to extract the tokens * @param _amount Number of tokens to burn */ diff --git a/contracts/upgrades/GraphProxy.sol b/contracts/upgrades/GraphProxy.sol index 69c88fd28..d3f6eacec 100644 --- a/contracts/upgrades/GraphProxy.sol +++ b/contracts/upgrades/GraphProxy.sol @@ -2,9 +2,11 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts/utils/Address.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; -import "./GraphProxyStorage.sol"; +import { GraphProxyStorage } from "./GraphProxyStorage.sol"; + +import { IGraphProxy } from "./IGraphProxy.sol"; /** * @title Graph Proxy @@ -13,13 +15,13 @@ import "./GraphProxyStorage.sol"; * This contract implements a proxy that is upgradeable by an admin. * https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#transparent-proxies-and-function-clashes */ -contract GraphProxy is GraphProxyStorage { +contract GraphProxy is GraphProxyStorage, IGraphProxy { /** * @dev Modifier used internally that will delegate the call to the implementation unless * the sender is the admin. */ modifier ifAdmin() { - if (msg.sender == _admin()) { + if (msg.sender == _getAdmin()) { _; } else { _fallback(); @@ -31,7 +33,7 @@ contract GraphProxy is GraphProxyStorage { * the sender is the admin or pending implementation. */ modifier ifAdminOrPendingImpl() { - if (msg.sender == _admin() || msg.sender == _pendingImplementation()) { + if (msg.sender == _getAdmin() || msg.sender == _getPendingImplementation()) { _; } else { _fallback(); @@ -39,7 +41,7 @@ contract GraphProxy is GraphProxyStorage { } /** - * @dev Contract constructor. + * @notice GraphProxy contract constructor. * @param _impl Address of the initial implementation * @param _admin Address of the proxy admin */ @@ -58,91 +60,114 @@ contract GraphProxy is GraphProxyStorage { } /** - * @dev Returns the current admin. + * @notice Fallback function that delegates calls to implementation. Will run if call data + * is empty. + */ + receive() external payable { + _fallback(); + } + + /** + * @notice Fallback function that delegates calls to implementation. Will run if no other + * function in the contract matches the call data. + */ + fallback() external payable { + _fallback(); + } + + /** + * @notice Get the current admin * - * NOTE: Only the admin and implementation can call this function. + * @dev NOTE: Only the admin and implementation can call this function. * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + * + * @return The address of the current admin */ - function admin() external ifAdminOrPendingImpl returns (address) { - return _admin(); + function admin() external override ifAdminOrPendingImpl returns (address) { + return _getAdmin(); } /** - * @dev Returns the current implementation. + * @notice Get the current implementation. * - * NOTE: Only the admin can call this function. + * @dev NOTE: Only the admin can call this function. * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` + * + * @return The address of the current implementation for this proxy */ - function implementation() external ifAdminOrPendingImpl returns (address) { - return _implementation(); + function implementation() external override ifAdminOrPendingImpl returns (address) { + return _getImplementation(); } /** - * @dev Returns the current pending implementation. + * @notice Get the current pending implementation. * - * NOTE: Only the admin can call this function. + * @dev NOTE: Only the admin can call this function. * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0x9e5eddc59e0b171f57125ab86bee043d9128098c3a6b9adb4f2e86333c2f6f8c` + * + * @return The address of the current pending implementation for this proxy */ - function pendingImplementation() external ifAdminOrPendingImpl returns (address) { - return _pendingImplementation(); + function pendingImplementation() external override ifAdminOrPendingImpl returns (address) { + return _getPendingImplementation(); } /** - * @dev Changes the admin of the proxy. + * @notice Changes the admin of the proxy. + * + * @dev NOTE: Only the admin can call this function. * - * NOTE: Only the admin can call this function. + * @param _newAdmin Address of the new admin */ - function setAdmin(address _newAdmin) external ifAdmin { - require(_newAdmin != address(0), "Cannot change the admin of a proxy to the zero address"); + function setAdmin(address _newAdmin) external override ifAdmin { + require(_newAdmin != address(0), "Admin cant be the zero address"); _setAdmin(_newAdmin); } /** - * @dev Upgrades to a new implementation contract. + * @notice Upgrades to a new implementation contract. + * @dev NOTE: Only the admin can call this function. * @param _newImplementation Address of implementation contract - * - * NOTE: Only the admin can call this function. */ - function upgradeTo(address _newImplementation) external ifAdmin { + function upgradeTo(address _newImplementation) external override ifAdmin { _setPendingImplementation(_newImplementation); } /** - * @dev Admin function for new implementation to accept its role as implementation. + * @notice Admin function for new implementation to accept its role as implementation. */ - function acceptUpgrade() external ifAdminOrPendingImpl { + function acceptUpgrade() external override ifAdminOrPendingImpl { _acceptUpgrade(); } /** - * @dev Admin function for new implementation to accept its role as implementation. + * @notice Admin function for new implementation to accept its role as implementation, + * calling a function on the new implementation. + * @param data Calldata (including selector) for the function to delegatecall into the implementation */ - function acceptUpgradeAndCall(bytes calldata data) external ifAdminOrPendingImpl { + function acceptUpgradeAndCall(bytes calldata data) external override ifAdminOrPendingImpl { _acceptUpgrade(); // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = _implementation().delegatecall(data); - require(success); + (bool success, ) = _getImplementation().delegatecall(data); + require(success, "Impl call failed"); } /** * @dev Admin function for new implementation to accept its role as implementation. */ function _acceptUpgrade() internal { - address _pendingImplementation = _pendingImplementation(); - require(Address.isContract(_pendingImplementation), "Implementation must be a contract"); - require( - _pendingImplementation != address(0) && msg.sender == _pendingImplementation, - "Caller must be the pending implementation" - ); + address _pendingImplementation = _getPendingImplementation(); + require(Address.isContract(_pendingImplementation), "Impl must be a contract"); + require(_pendingImplementation != address(0), "Impl cannot be zero address"); + require(msg.sender == _pendingImplementation, "Only pending implementation"); _setImplementation(_pendingImplementation); _setPendingImplementation(address(0)); @@ -154,8 +179,9 @@ contract GraphProxy is GraphProxyStorage { * external caller. */ function _fallback() internal { - require(msg.sender != _admin(), "Cannot fallback to proxy target"); + require(msg.sender != _getAdmin(), "Cannot fallback to proxy target"); + // solhint-disable-next-line no-inline-assembly assembly { // (a) get free memory pointer let ptr := mload(0x40) @@ -183,20 +209,4 @@ contract GraphProxy is GraphProxyStorage { } } } - - /** - * @dev Fallback function that delegates calls to implementation. Will run if no other - * function in the contract matches the call data. - */ - fallback() external payable { - _fallback(); - } - - /** - * @dev Fallback function that delegates calls to implementation. Will run if call data - * is empty. - */ - receive() external payable { - _fallback(); - } } diff --git a/contracts/upgrades/GraphProxyAdmin.sol b/contracts/upgrades/GraphProxyAdmin.sol index 3775b9df1..d96dbd449 100644 --- a/contracts/upgrades/GraphProxyAdmin.sol +++ b/contracts/upgrades/GraphProxyAdmin.sol @@ -2,10 +2,10 @@ pragma solidity ^0.7.6; -import "../governance/Governed.sol"; +import { Governed } from "../governance/Governed.sol"; -import "./IGraphProxy.sol"; -import "./GraphUpgradeable.sol"; +import { IGraphProxy } from "./IGraphProxy.sol"; +import { GraphUpgradeable } from "./GraphUpgradeable.sol"; /** * @title GraphProxyAdmin @@ -16,79 +16,85 @@ import "./GraphUpgradeable.sol"; */ contract GraphProxyAdmin is Governed { /** - * @dev Contract constructor. + * @notice Contract constructor. */ constructor() { Governed._initialize(msg.sender); } /** - * @dev Returns the current implementation of a proxy. - * This is needed because only the proxy admin can query it. + * @notice Returns the current implementation of a proxy. + * @dev This is needed because only the proxy admin can query it. + * @param _proxy Address of the proxy for which to get the implementation. * @return The address of the current implementation of the proxy. */ - function getProxyImplementation(IGraphProxy _proxy) public view returns (address) { + function getProxyImplementation(IGraphProxy _proxy) external view returns (address) { // We need to manually run the static call since the getter cannot be flagged as view // bytes4(keccak256("implementation()")) == 0x5c60da1b (bool success, bytes memory returndata) = address(_proxy).staticcall(hex"5c60da1b"); - require(success); + require(success, "Proxy impl call failed"); return abi.decode(returndata, (address)); } /** - * @dev Returns the pending implementation of a proxy. - * This is needed because only the proxy admin can query it. + * @notice Returns the pending implementation of a proxy. + * @dev This is needed because only the proxy admin can query it. + * @param _proxy Address of the proxy for which to get the pending implementation. * @return The address of the pending implementation of the proxy. */ - function getProxyPendingImplementation(IGraphProxy _proxy) public view returns (address) { + function getProxyPendingImplementation(IGraphProxy _proxy) external view returns (address) { // We need to manually run the static call since the getter cannot be flagged as view // bytes4(keccak256("pendingImplementation()")) == 0x396f7b23 (bool success, bytes memory returndata) = address(_proxy).staticcall(hex"396f7b23"); - require(success); + require(success, "Proxy pendingImpl call failed"); return abi.decode(returndata, (address)); } /** - * @dev Returns the admin of a proxy. Only the admin can query it. + * @notice Returns the admin of a proxy. Only the admin can query it. + * @param _proxy Address of the proxy for which to get the admin. * @return The address of the current admin of the proxy. */ - function getProxyAdmin(IGraphProxy _proxy) public view returns (address) { + function getProxyAdmin(IGraphProxy _proxy) external view returns (address) { // We need to manually run the static call since the getter cannot be flagged as view // bytes4(keccak256("admin()")) == 0xf851a440 (bool success, bytes memory returndata) = address(_proxy).staticcall(hex"f851a440"); - require(success); + require(success, "Proxy admin call failed"); return abi.decode(returndata, (address)); } /** - * @dev Changes the admin of a proxy. + * @notice Changes the admin of a proxy. * @param _proxy Proxy to change admin. * @param _newAdmin Address to transfer proxy administration to. */ - function changeProxyAdmin(IGraphProxy _proxy, address _newAdmin) public onlyGovernor { + function changeProxyAdmin(IGraphProxy _proxy, address _newAdmin) external onlyGovernor { _proxy.setAdmin(_newAdmin); } /** - * @dev Upgrades a proxy to the newest implementation of a contract. + * @notice Upgrades a proxy to the newest implementation of a contract. * @param _proxy Proxy to be upgraded. * @param _implementation the address of the Implementation. */ - function upgrade(IGraphProxy _proxy, address _implementation) public onlyGovernor { + function upgrade(IGraphProxy _proxy, address _implementation) external onlyGovernor { _proxy.upgradeTo(_implementation); } /** - * @dev Accepts a proxy. + * @notice Accepts a proxy. * @param _implementation Address of the implementation accepting the proxy. * @param _proxy Address of the proxy being accepted. */ - function acceptProxy(GraphUpgradeable _implementation, IGraphProxy _proxy) public onlyGovernor { + function acceptProxy(GraphUpgradeable _implementation, IGraphProxy _proxy) + external + onlyGovernor + { _implementation.acceptProxy(_proxy); } /** - * @dev Accepts a proxy and call a function on the implementation. + * @notice Accepts a proxy and call a function on the implementation. * @param _implementation Address of the implementation accepting the proxy. * @param _proxy Address of the proxy being accepted. * @param _data Encoded function to call on the implementation after accepting the proxy. diff --git a/contracts/upgrades/GraphProxyStorage.sol b/contracts/upgrades/GraphProxyStorage.sol index 950f9776b..b308c0a0c 100644 --- a/contracts/upgrades/GraphProxyStorage.sol +++ b/contracts/upgrades/GraphProxyStorage.sol @@ -8,7 +8,7 @@ pragma solidity ^0.7.6; * This contract does not actually define state variables managed by the compiler * but uses fixed slot locations. */ -contract GraphProxyStorage { +abstract contract GraphProxyStorage { /** * @dev Storage slot with the address of the current implementation. * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is @@ -59,15 +59,16 @@ contract GraphProxyStorage { * @dev Modifier to check whether the `msg.sender` is the admin. */ modifier onlyAdmin() { - require(msg.sender == _admin(), "Caller must be admin"); + require(msg.sender == _getAdmin(), "Caller must be admin"); _; } /** * @return adm The admin slot. */ - function _admin() internal view returns (address adm) { + function _getAdmin() internal view returns (address adm) { bytes32 slot = ADMIN_SLOT; + // solhint-disable-next-line no-inline-assembly assembly { adm := sload(slot) } @@ -78,20 +79,23 @@ contract GraphProxyStorage { * @param _newAdmin Address of the new proxy admin */ function _setAdmin(address _newAdmin) internal { + address oldAdmin = _getAdmin(); bytes32 slot = ADMIN_SLOT; + // solhint-disable-next-line no-inline-assembly assembly { sstore(slot, _newAdmin) } - emit AdminUpdated(_admin(), _newAdmin); + emit AdminUpdated(oldAdmin, _newAdmin); } /** * @dev Returns the current implementation. * @return impl Address of the current implementation */ - function _implementation() internal view returns (address impl) { + function _getImplementation() internal view returns (address impl) { bytes32 slot = IMPLEMENTATION_SLOT; + // solhint-disable-next-line no-inline-assembly assembly { impl := sload(slot) } @@ -101,8 +105,9 @@ contract GraphProxyStorage { * @dev Returns the current pending implementation. * @return impl Address of the current pending implementation */ - function _pendingImplementation() internal view returns (address impl) { + function _getPendingImplementation() internal view returns (address impl) { bytes32 slot = PENDING_IMPLEMENTATION_SLOT; + // solhint-disable-next-line no-inline-assembly assembly { impl := sload(slot) } @@ -113,9 +118,10 @@ contract GraphProxyStorage { * @param _newImplementation Address of the new implementation */ function _setImplementation(address _newImplementation) internal { - address oldImplementation = _implementation(); + address oldImplementation = _getImplementation(); bytes32 slot = IMPLEMENTATION_SLOT; + // solhint-disable-next-line no-inline-assembly assembly { sstore(slot, _newImplementation) } @@ -128,9 +134,10 @@ contract GraphProxyStorage { * @param _newImplementation Address of the new pending implementation */ function _setPendingImplementation(address _newImplementation) internal { - address oldPendingImplementation = _pendingImplementation(); + address oldPendingImplementation = _getPendingImplementation(); bytes32 slot = PENDING_IMPLEMENTATION_SLOT; + // solhint-disable-next-line no-inline-assembly assembly { sstore(slot, _newImplementation) } diff --git a/contracts/upgrades/GraphUpgradeable.sol b/contracts/upgrades/GraphUpgradeable.sol index 2331cae27..92ce80ad7 100644 --- a/contracts/upgrades/GraphUpgradeable.sol +++ b/contracts/upgrades/GraphUpgradeable.sol @@ -2,13 +2,13 @@ pragma solidity ^0.7.6; -import "./IGraphProxy.sol"; +import { IGraphProxy } from "./IGraphProxy.sol"; /** * @title Graph Upgradeable * @dev This contract is intended to be inherited from upgradeable contracts. */ -contract GraphUpgradeable { +abstract contract GraphUpgradeable { /** * @dev Storage slot with the address of the current implementation. * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is @@ -29,7 +29,7 @@ contract GraphUpgradeable { * @dev Check if the caller is the implementation. */ modifier onlyImpl() { - require(msg.sender == _implementation(), "Caller must be the implementation"); + require(msg.sender == _implementation(), "Only implementation"); _; } @@ -39,22 +39,26 @@ contract GraphUpgradeable { */ function _implementation() internal view returns (address impl) { bytes32 slot = IMPLEMENTATION_SLOT; + // solhint-disable-next-line no-inline-assembly assembly { impl := sload(slot) } } /** - * @dev Accept to be an implementation of proxy. + * @notice Accept to be an implementation of proxy. + * @param _proxy Proxy to accept */ function acceptProxy(IGraphProxy _proxy) external onlyProxyAdmin(_proxy) { _proxy.acceptUpgrade(); } /** - * @dev Accept to be an implementation of proxy and then call a function from the new + * @notice Accept to be an implementation of proxy and then call a function from the new * implementation as specified by `_data`, which should be an encoded function call. This is * useful to initialize new storage variables in the proxied contract. + * @param _proxy Proxy to accept + * @param _data Calldata for the initialization function call (including selector) */ function acceptProxyAndCall(IGraphProxy _proxy, bytes calldata _data) external diff --git a/e2e/deployment/config/controller.test.ts b/e2e/deployment/config/controller.test.ts index 8fbdf4834..647cb19f5 100644 --- a/e2e/deployment/config/controller.test.ts +++ b/e2e/deployment/config/controller.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import hre, { ethers } from 'hardhat' import { NamedAccounts } from '../../../gre/type-extensions' -import GraphChain from '../../../gre/helpers/network' +import GraphChain from '../../../gre/helpers/chain' describe('Controller configuration', () => { const graph = hre.graph() diff --git a/e2e/deployment/config/l1/bridgeEscrow.test.ts b/e2e/deployment/config/l1/bridgeEscrow.test.ts index 2304a2063..1aeba6357 100644 --- a/e2e/deployment/config/l1/bridgeEscrow.test.ts +++ b/e2e/deployment/config/l1/bridgeEscrow.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import hre from 'hardhat' -import GraphChain from '../../../../gre/helpers/network' +import GraphChain from '../../../../gre/helpers/chain' describe('[L1] BridgeEscrow configuration', function () { const graph = hre.graph() diff --git a/e2e/deployment/config/l1/graphToken.test.ts b/e2e/deployment/config/l1/graphToken.test.ts index 41e6322d0..0d222c263 100644 --- a/e2e/deployment/config/l1/graphToken.test.ts +++ b/e2e/deployment/config/l1/graphToken.test.ts @@ -1,7 +1,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' import hre from 'hardhat' -import GraphChain from '../../../../gre/helpers/network' +import GraphChain from '../../../../gre/helpers/chain' describe('[L1] GraphToken', () => { const graph = hre.graph() diff --git a/e2e/deployment/config/l1/l1GraphTokenGateway.test.ts b/e2e/deployment/config/l1/l1GraphTokenGateway.test.ts index d2424d0ce..a957f6882 100644 --- a/e2e/deployment/config/l1/l1GraphTokenGateway.test.ts +++ b/e2e/deployment/config/l1/l1GraphTokenGateway.test.ts @@ -1,7 +1,8 @@ import { expect } from 'chai' import hre from 'hardhat' -import GraphChain from '../../../../gre/helpers/network' +import GraphChain from '../../../../gre/helpers/chain' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { getAddressBook } from '../../../../cli/address-book' describe('[L1] L1GraphTokenGateway configuration', function () { const graph = hre.graph() @@ -13,7 +14,7 @@ describe('[L1] L1GraphTokenGateway configuration', function () { unauthorized = (await graph.getTestAccounts())[0] }) - it('bridge should be unpaused', async function () { + it('bridge should not be paused', async function () { const paused = await L1GraphTokenGateway.paused() expect(paused).eq(false) }) @@ -23,10 +24,47 @@ describe('[L1] L1GraphTokenGateway configuration', function () { expect(controller).eq(Controller.address) }) + it('l2GRT should match the L2 GraphToken deployed address', async function () { + const l2GRT = await L1GraphTokenGateway.l2GRT() + expect(l2GRT).eq(graph.l2.contracts.GraphToken.address) + }) + + it('l2Counterpart should match the deployed L2 GraphTokenGateway address', async function () { + const l2Counterpart = await L1GraphTokenGateway.l2Counterpart() + expect(l2Counterpart).eq(graph.l2.contracts.L2GraphTokenGateway.address) + }) + + it('escrow should match the deployed L1 BridgeEscrow address', async function () { + const escrow = await L1GraphTokenGateway.escrow() + expect(escrow).eq(graph.l1.contracts.BridgeEscrow.address) + }) + + it("inbox should match Arbitrum's Inbox address", async function () { + const inbox = await L1GraphTokenGateway.inbox() + + // TODO: is there a cleaner way to get the router address? + const arbitrumAddressBook = process.env.ARBITRUM_ADDRESS_BOOK ?? 'arbitrum-addresses-local.json' + const arbAddressBook = getAddressBook(arbitrumAddressBook, graph.l1.chainId.toString()) + const arbIInbox = arbAddressBook.getEntry('IInbox') + + expect(inbox.toLowerCase()).eq(arbIInbox.address.toLowerCase()) + }) + + it("l1Router should match Arbitrum's router address", async function () { + const l1Router = await L1GraphTokenGateway.l1Router() + + // TODO: is there a cleaner way to get the router address? + const arbitrumAddressBook = process.env.ARBITRUM_ADDRESS_BOOK ?? 'arbitrum-addresses-local.json' + const arbAddressBook = getAddressBook(arbitrumAddressBook, graph.l1.chainId.toString()) + const arbL2Router = arbAddressBook.getEntry('L1GatewayRouter') + + expect(l1Router).eq(arbL2Router.address) + }) + describe('calls with unauthorized user', () => { it('initialize should revert', async function () { const tx = L1GraphTokenGateway.connect(unauthorized).initialize(unauthorized.address) - await expect(tx).revertedWith('Caller must be the implementation') + await expect(tx).revertedWith('Only implementation') }) it('setArbitrumAddresses should revert', async function () { @@ -34,38 +72,38 @@ describe('[L1] L1GraphTokenGateway configuration', function () { unauthorized.address, unauthorized.address, ) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('setL2TokenAddress should revert', async function () { const tx = L1GraphTokenGateway.connect(unauthorized).setL2TokenAddress(unauthorized.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('setL2CounterpartAddress should revert', async function () { const tx = L1GraphTokenGateway.connect(unauthorized).setL2CounterpartAddress( unauthorized.address, ) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('setEscrowAddress should revert', async function () { const tx = L1GraphTokenGateway.connect(unauthorized).setEscrowAddress(unauthorized.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) - it('addToCallhookWhitelist should revert', async function () { - const tx = L1GraphTokenGateway.connect(unauthorized).addToCallhookWhitelist( + it('addToCallhookAllowlist should revert', async function () { + const tx = L1GraphTokenGateway.connect(unauthorized).addToCallhookAllowlist( unauthorized.address, ) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) - it('removeFromCallhookWhitelist should revert', async function () { - const tx = L1GraphTokenGateway.connect(unauthorized).removeFromCallhookWhitelist( + it('removeFromCallhookAllowlist should revert', async function () { + const tx = L1GraphTokenGateway.connect(unauthorized).removeFromCallhookAllowlist( unauthorized.address, ) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('finalizeInboundTransfer should revert', async function () { @@ -77,7 +115,7 @@ describe('[L1] L1GraphTokenGateway configuration', function () { '0x00', ) - await expect(tx).revertedWith('INBOX_NOT_SET') + await expect(tx).revertedWith('NOT_FROM_BRIDGE') }) }) }) diff --git a/e2e/deployment/config/l1/rewardsManager.test.ts b/e2e/deployment/config/l1/rewardsManager.test.ts index 8f1d3a48c..d46f15397 100644 --- a/e2e/deployment/config/l1/rewardsManager.test.ts +++ b/e2e/deployment/config/l1/rewardsManager.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import hre from 'hardhat' -import GraphChain from '../../../../gre/helpers/network' +import GraphChain from '../../../../gre/helpers/chain' describe('[L1] RewardsManager configuration', () => { const graph = hre.graph() diff --git a/e2e/deployment/config/l2/l2GraphToken.test.ts b/e2e/deployment/config/l2/l2GraphToken.test.ts index e486405ed..6b5f03c92 100644 --- a/e2e/deployment/config/l2/l2GraphToken.test.ts +++ b/e2e/deployment/config/l2/l2GraphToken.test.ts @@ -1,7 +1,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' import hre from 'hardhat' -import GraphChain from '../../../../gre/helpers/network' +import GraphChain from '../../../../gre/helpers/chain' describe('[L2] L2GraphToken', () => { const graph = hre.graph() @@ -14,6 +14,16 @@ describe('[L2] L2GraphToken', () => { unauthorized = (await graph.getTestAccounts())[0] }) + it('l1Address should match the L1 GraphToken deployed address', async function () { + const l1Address = await L2GraphToken.l1Address() + expect(l1Address).eq(graph.l1.contracts.GraphToken.address) + }) + + it('gateway should match the L2 GraphTokenGateway deployed address', async function () { + const gateway = await L2GraphToken.gateway() + expect(gateway).eq(graph.l2.contracts.L2GraphTokenGateway.address) + }) + describe('calls with unauthorized user', () => { it('mint should revert', async function () { const tx = L2GraphToken.connect(unauthorized).mint( diff --git a/e2e/deployment/config/l2/l2GraphTokenGateway.test.ts b/e2e/deployment/config/l2/l2GraphTokenGateway.test.ts index 04732498b..46df97686 100644 --- a/e2e/deployment/config/l2/l2GraphTokenGateway.test.ts +++ b/e2e/deployment/config/l2/l2GraphTokenGateway.test.ts @@ -1,7 +1,8 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' import hre from 'hardhat' -import GraphChain from '../../../../gre/helpers/network' +import { getAddressBook } from '../../../../cli/address-book' +import GraphChain from '../../../../gre/helpers/chain' describe('[L2] L2GraphTokenGateway configuration', function () { const graph = hre.graph() @@ -13,7 +14,7 @@ describe('[L2] L2GraphTokenGateway configuration', function () { unauthorized = (await graph.getTestAccounts())[0] }) - it('bridge should be unpaused', async function () { + it('bridge should not be paused', async function () { const paused = await L2GraphTokenGateway.paused() expect(paused).eq(false) }) @@ -23,27 +24,48 @@ describe('[L2] L2GraphTokenGateway configuration', function () { expect(controller).eq(Controller.address) }) + it('l1GRT should match the L1 GraphToken deployed address', async function () { + const l1GRT = await L2GraphTokenGateway.l1GRT() + expect(l1GRT).eq(graph.l1.contracts.GraphToken.address) + }) + + it('l1Counterpart should match the deployed L1 GraphTokenGateway address', async function () { + const l1Counterpart = await L2GraphTokenGateway.l1Counterpart() + expect(l1Counterpart).eq(graph.l1.contracts.L1GraphTokenGateway.address) + }) + + it("l2Router should match Arbitrum's router address", async function () { + const l2Router = await L2GraphTokenGateway.l2Router() + + // TODO: is there a cleaner way to get the router address? + const arbitrumAddressBook = process.env.ARBITRUM_ADDRESS_BOOK ?? 'arbitrum-addresses-local.json' + const arbAddressBook = getAddressBook(arbitrumAddressBook, graph.l2.chainId.toString()) + const arbL2Router = arbAddressBook.getEntry('L2GatewayRouter') + + expect(l2Router).eq(arbL2Router.address) + }) + describe('calls with unauthorized user', () => { it('initialize should revert', async function () { const tx = L2GraphTokenGateway.connect(unauthorized).initialize(unauthorized.address) - await expect(tx).revertedWith('Caller must be the implementation') + await expect(tx).revertedWith('Only implementation') }) it('setL2Router should revert', async function () { const tx = L2GraphTokenGateway.connect(unauthorized).setL2Router(unauthorized.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('setL1TokenAddress should revert', async function () { const tx = L2GraphTokenGateway.connect(unauthorized).setL1TokenAddress(unauthorized.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('setL1CounterpartAddress should revert', async function () { const tx = L2GraphTokenGateway.connect(unauthorized).setL1CounterpartAddress( unauthorized.address, ) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('finalizeInboundTransfer should revert', async function () { diff --git a/e2e/deployment/config/l2/rewardsManager.test.ts b/e2e/deployment/config/l2/rewardsManager.test.ts index 37b34da5d..5329abec8 100644 --- a/e2e/deployment/config/l2/rewardsManager.test.ts +++ b/e2e/deployment/config/l2/rewardsManager.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import hre from 'hardhat' -import GraphChain from '../../../../gre/helpers/network' +import GraphChain from '../../../../gre/helpers/chain' describe('[L2] RewardsManager configuration', () => { const graph = hre.graph() diff --git a/e2e/deployment/init/l1/bridgeEscrow.test.ts b/e2e/deployment/init/l1/bridgeEscrow.test.ts new file mode 100644 index 000000000..2f0333eb2 --- /dev/null +++ b/e2e/deployment/init/l1/bridgeEscrow.test.ts @@ -0,0 +1,17 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import GraphChain from '../../../../gre/helpers/chain' + +describe('BridgeEscrow initialization', () => { + const graph = hre.graph() + const { BridgeEscrow, GraphToken, L1GraphTokenGateway } = graph.contracts + + before(async function () { + if (GraphChain.isL2(graph.chainId)) this.skip() + }) + + it("should allow L1GraphTokenGateway contract to spend MAX_UINT256 tokens on BridgeEscrow's behalf", async function () { + const allowance = await GraphToken.allowance(BridgeEscrow.address, L1GraphTokenGateway.address) + expect(allowance).eq(hre.ethers.constants.MaxUint256) + }) +}) diff --git a/e2e/deployment/init/l1/graphToken.test.ts b/e2e/deployment/init/l1/graphToken.test.ts index aa0cc04e3..39766b479 100644 --- a/e2e/deployment/init/l1/graphToken.test.ts +++ b/e2e/deployment/init/l1/graphToken.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import hre from 'hardhat' import { getItemValue } from '../../../../cli/config' -import GraphChain from '../../../../gre/helpers/network' +import GraphChain from '../../../../gre/helpers/chain' describe('[L1] GraphToken initialization', () => { const graph = hre.graph() diff --git a/e2e/deployment/init/l2/graphToken.test.ts b/e2e/deployment/init/l2/graphToken.test.ts index 90b232531..048531283 100644 --- a/e2e/deployment/init/l2/graphToken.test.ts +++ b/e2e/deployment/init/l2/graphToken.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import hre from 'hardhat' -import GraphChain from '../../../../gre/helpers/network' +import GraphChain from '../../../../gre/helpers/chain' describe('[L2] GraphToken initialization', () => { const graph = hre.graph() diff --git a/e2e/scenarios/fixtures/bridge.ts b/e2e/scenarios/fixtures/bridge.ts new file mode 100644 index 000000000..a1441ec89 --- /dev/null +++ b/e2e/scenarios/fixtures/bridge.ts @@ -0,0 +1,29 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumber } from 'ethers' +import { toGRT } from '../../../cli/network' + +export interface BridgeFixture { + deploymentFile: string + funder: SignerWithAddress + accountsToFund: { + signer: SignerWithAddress + amount: BigNumber + }[] +} + +// Signers +// 0: l1Deployer +// 1: l2Deployer + +export const getBridgeFixture = (signers: SignerWithAddress[]): BridgeFixture => { + return { + deploymentFile: 'localNetwork.json', + funder: signers[0], + accountsToFund: [ + { + signer: signers[1], + amount: toGRT(10_000_000), + }, + ], + } +} diff --git a/e2e/scenarios/lib/curation.ts b/e2e/scenarios/lib/curation.ts index 1dc35cbb2..b0c2d927b 100644 --- a/e2e/scenarios/lib/curation.ts +++ b/e2e/scenarios/lib/curation.ts @@ -16,6 +16,6 @@ export const signal = async ( // Add signal console.log(`\nAdd ${amount} in signal to subgraphId ${subgraphId}..`) await sendTransaction(curator, contracts.GNS, 'mintSignal', [subgraphId, amount, 0], { - gasLimit: 2000000, + gasLimit: 4_000_000, }) } diff --git a/e2e/scenarios/lib/helpers.ts b/e2e/scenarios/lib/helpers.ts index db9da32cb..e9a1eca0e 100644 --- a/e2e/scenarios/lib/helpers.ts +++ b/e2e/scenarios/lib/helpers.ts @@ -1,14 +1,20 @@ export function getGraphOptsFromArgv(): { - graphConfig: string | undefined addressBook: string | undefined + graphConfig: string | undefined + l1GraphConfig: string | undefined + l2GraphConfig: string | undefined + disableSecureAccounts?: boolean | undefined } { const argv = process.argv.slice(2) - const getArgv = (index: number) => + const getArgv: any = (index: number) => argv[index] && argv[index] !== 'undefined' ? argv[index] : undefined return { - graphConfig: getArgv(0), - addressBook: getArgv(1), + addressBook: getArgv(0), + graphConfig: getArgv(1), + l1GraphConfig: getArgv(2), + l2GraphConfig: getArgv(3), + disableSecureAccounts: getArgv(4), } } diff --git a/e2e/scenarios/lib/staking.ts b/e2e/scenarios/lib/staking.ts index d221afccd..53465cfb8 100644 --- a/e2e/scenarios/lib/staking.ts +++ b/e2e/scenarios/lib/staking.ts @@ -42,7 +42,7 @@ export const allocateFrom = async ( 'allocateFrom', [indexer.address, subgraphDeploymentID, amount, allocationId, metadata, proof], { - gasLimit: 2000000, + gasLimit: 4_000_000, }, ) } @@ -56,6 +56,6 @@ export const closeAllocation = async ( console.log(`\nClosing ${allocationId}...`) await sendTransaction(indexer, contracts.Staking, 'closeAllocation', [allocationId, poi], { - gasLimit: 2000000, + gasLimit: 4_000_000, }) } diff --git a/e2e/scenarios/lib/subgraph.ts b/e2e/scenarios/lib/subgraph.ts index c17c4e543..f97f1d76b 100644 --- a/e2e/scenarios/lib/subgraph.ts +++ b/e2e/scenarios/lib/subgraph.ts @@ -26,10 +26,14 @@ export const publishNewSubgraph = async ( publisher.address, await contracts.GNS.nextAccountSeqID(publisher.address), ) - await sendTransaction(publisher, contracts.GNS, 'publishNewSubgraph', [ - deploymentId, - randomHexBytes(), - randomHexBytes(), - ]) + await sendTransaction( + publisher, + contracts.GNS, + 'publishNewSubgraph', + [deploymentId, randomHexBytes(), randomHexBytes()], + { + gasLimit: 4_000_000, + }, + ) return subgraphId } diff --git a/e2e/scenarios/send-grt-to-l2.test.ts b/e2e/scenarios/send-grt-to-l2.test.ts new file mode 100644 index 000000000..9b81371b6 --- /dev/null +++ b/e2e/scenarios/send-grt-to-l2.test.ts @@ -0,0 +1,23 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { getBridgeFixture, BridgeFixture } from './fixtures/bridge' + +describe('Bridge GRT to L2', () => { + const graph = hre.graph() + let bridgeFixture: BridgeFixture + + before(async () => { + const l1Deployer = await graph.l1.getDeployer() + const l2Deployer = await graph.l2.getDeployer() + bridgeFixture = getBridgeFixture([l1Deployer, l2Deployer]) + }) + + describe('GRT balances', () => { + it(`L2 balances should match bridged amount`, async function () { + for (const account of bridgeFixture.accountsToFund) { + const l2GrtBalance = await graph.l2.contracts.GraphToken.balanceOf(account.signer.address) + expect(l2GrtBalance).eq(account.amount) + } + }) + }) +}) diff --git a/e2e/scenarios/send-grt-to-l2.ts b/e2e/scenarios/send-grt-to-l2.ts new file mode 100644 index 000000000..9884c9816 --- /dev/null +++ b/e2e/scenarios/send-grt-to-l2.ts @@ -0,0 +1,40 @@ +// ### Scenario description ### +// Bridge action > Bridge GRT tokens from L1 to L2 +// This scenario will bridge GRT tokens from L1 to L2. See fixtures for details. +// Run with: +// npx hardhat e2e:scenario send-grt-to-l2 --network --graph-config config/graph..yml + +import hre from 'hardhat' +import { TASK_BRIDGE_TO_L2 } from '../../tasks/bridge/to-l2' +import { getGraphOptsFromArgv } from './lib/helpers' +import { getBridgeFixture } from './fixtures/bridge' + +async function main() { + const graphOpts = getGraphOptsFromArgv() + const graph = hre.graph(graphOpts) + + const l1Deployer = await graph.l1.getDeployer() + const l2Deployer = await graph.l2.getDeployer() + + const bridgeFixture = getBridgeFixture([l1Deployer, l2Deployer]) + + // == Send GRT to L2 accounts + for (const account of bridgeFixture.accountsToFund) { + await hre.run(TASK_BRIDGE_TO_L2, { + ...graphOpts, + amount: account.amount.toString(), + sender: bridgeFixture.funder.address, + recipient: account.signer.address, + deploymentFile: bridgeFixture.deploymentFile, + }) + } +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exitCode = 1 + }) diff --git a/gre/README.md b/gre/README.md index 2ebb2365a..596e0122c 100644 --- a/gre/README.md +++ b/gre/README.md @@ -8,8 +8,9 @@ GRE is a hardhat plugin that extends hardhat's runtime environment to inject add - Exposes protocol configuration via graph config file and address book - Provides account management methods for convenience - Multichain! Supports both L1 and L2 layers of the protocol simultaneously +- Integrates seamlessly with [hardhat-secure-accounts](https://www.npmjs.com/package/hardhat-secure-accounts) -### Usage +## Usage #### Example Import GRE using `import './gre/gre'` on your hardhat config file and then: @@ -80,7 +81,7 @@ networks: { ... }, graph: { - addressBook: 'addresses.json' + addressBook: 'addresses.json' l1GraphConfig: 'config/graph.mainnet.yml' l2GraphConfig: 'config/graph.arbitrum-one.yml' } @@ -122,7 +123,7 @@ The priority for the address book is: 1) `hre.graph({ ... })` init parameter `addressBook` 2) `graph.addressBook` graph config parameter `addressBook` in hardhat config file -### API +## API GRE exposes functionality via a simple API: @@ -142,6 +143,7 @@ The interface for both `l1` and `l2` objects looks like this: export interface GraphNetworkEnvironment { chainId: number contracts: NetworkContracts + provider: EthersProviderWrapper graphConfig: any addressBook: AddressBook getNamedAccounts: () => Promise @@ -168,7 +170,7 @@ Returns an object with all the contracts available in the network. Connects usin **Graph Config** -Returns an object that grants raw access to the graph config file for the protocol. The graph config file is a YAML file that contains all the parameters with which the protocol was deployed. +Returns an object that grants raw access to the YAML parse of the graph config file for the protocol. The graph config file is a YAML file that contains all the parameters with which the protocol was deployed. > TODO: add better APIs to interact with the graph config file. @@ -226,4 +228,26 @@ It's important to note that the deployer is not a named account as it's derived Returns an object with wallets derived from the mnemonic or private key provided via hardhat network configuration. These wallets are not connected to a provider. **Account management: getWallet** -Returns a wallet derived from the mnemonic or private key provided via hardhat network configuration that matches a given address. This wallet is not connected to a provider. \ No newline at end of file +Returns a wallet derived from the mnemonic or private key provided via hardhat network configuration that matches a given address. This wallet is not connected to a provider. + +#### Integration with hardhat-secure-accounts + +[hardhat-secure-accounts](https://www.npmjs.com/package/hardhat-secure-accounts) is a hardhat plugin that allows you to use encrypted keystore files to store your private keys. GRE has built-in support to use this plugin. By default is enabled but can be disabled by setting the `disableSecureAccounts` option to `true` when instantiating the GRE object. When enabled, each time you call any of the account management methods you will be prompted for an account name and password to unlock: + +```js +// Without secure accounts +> const graph = hre.graph({ disableSecureAccounts: true }) +> const deployer = await g.l1.getDeployer() +> deployer.address +'0xBc7f4d3a85B820fDB1058FD93073Eb6bc9AAF59b' + +// With secure accounts +> const graph = hre.graph() +> const deployer = await g.l1.getDeployer() +== Using secure accounts, please unlock an account for L1(goerli) +Available accounts: goerli-deployer, arbitrum-goerli-deployer, rinkeby-deployer, test-mnemonic +Choose an account to unlock (use tab to autocomplete): test-mnemonic +Enter the password for this account: ************ +> deployer.address +'0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1' +``` diff --git a/gre/accounts.ts b/gre/accounts.ts index 6baf3da6a..b316fba7d 100644 --- a/gre/accounts.ts +++ b/gre/accounts.ts @@ -4,7 +4,7 @@ import { derivePrivateKeys } from 'hardhat/internal/core/providers/util' import { Wallet } from 'ethers' import { getItemValue, readConfig } from '../cli/config' import { AccountNames, NamedAccounts } from './type-extensions' -import { getNetworkName } from './config' +import { getNetworkName } from './helpers/network' import { HttpNetworkHDAccountsConfig, NetworksConfig } from 'hardhat/types' const namedAccountList: AccountNames[] = [ diff --git a/gre/config.ts b/gre/config.ts index 9899f6d13..c642f068d 100644 --- a/gre/config.ts +++ b/gre/config.ts @@ -1,19 +1,16 @@ import fs from 'fs' -import path from 'path' -import { NetworkConfig, NetworksConfig } from 'hardhat/types/config' import { HardhatRuntimeEnvironment } from 'hardhat/types/runtime' -import { HttpNetworkConfig } from 'hardhat/types/config' import { GraphRuntimeEnvironmentOptions } from './type-extensions' import { GREPluginError } from './helpers/error' -import GraphNetwork, { counterpartName } from './helpers/network' - -import { createProvider } from 'hardhat/internal/core/providers/construction' +import GraphNetwork from './helpers/chain' import { EthersProviderWrapper } from '@nomiclabs/hardhat-ethers/internal/ethers-provider-wrapper' -import { logDebug, logWarn } from './logger' -import { HARDHAT_NETWORK_NAME } from 'hardhat/plugins' +import { logDebug } from './helpers/logger' +import { normalizePath } from './helpers/utils' +import { getNetworkConfig } from './helpers/network' +import { getDefaultProvider } from './providers' interface GREChains { l1ChainId: number @@ -89,7 +86,7 @@ export function getChains(mainChainId: number | undefined): GREChains { } } -export function getProviders( +export function getDefaultProviders( hre: HardhatRuntimeEnvironment, l1ChainId: number, l2ChainId: number, @@ -97,41 +94,20 @@ export function getProviders( ): GREProviders { logDebug('== Getting providers') - const getProvider = ( - networks: NetworksConfig, - chainId: number, - mainNetworkName: string, - isMainProvider: boolean, - chainLabel: string, - ): EthersProviderWrapper | undefined => { - const network = getNetworkConfig(networks, chainId, mainNetworkName) as HttpNetworkConfig - const networkName = getNetworkName(networks, chainId, mainNetworkName) - - logDebug(`Provider url for ${chainLabel}(${networkName}): ${network?.url}`) - - // Ensure at least main provider is configured - // For Hardhat network we don't need url to create a provider - if ( - isMainProvider && - (network === undefined || network.url === undefined) && - networkName !== HARDHAT_NETWORK_NAME - ) { - throw new GREPluginError(`Must set a provider url for chain: ${chainId}!`) - } - - if (network === undefined || networkName === undefined) { - return undefined - } - - // Build provider as EthersProviderWrapper instead of JsonRpcProvider - // This allows us to use hardhat's account management methods for free - const ethereumProvider = createProvider(networkName, network) - const ethersProviderWrapper = new EthersProviderWrapper(ethereumProvider) - return ethersProviderWrapper - } - - const l1Provider = getProvider(hre.config.networks, l1ChainId, hre.network.name, isHHL1, 'L1') - const l2Provider = getProvider(hre.config.networks, l2ChainId, hre.network.name, !isHHL1, 'L2') + const l1Provider = getDefaultProvider( + hre.config.networks, + l1ChainId, + hre.network.name, + isHHL1, + 'L1', + ) + const l2Provider = getDefaultProvider( + hre.config.networks, + l2ChainId, + hre.network.name, + !isHHL1, + 'L2', + ) return { l1Provider, @@ -211,61 +187,3 @@ export function getGraphConfigPaths( l2GraphConfigPath: l2GraphConfigPath, } } - -function getNetworkConfig( - networks: NetworksConfig, - chainId: number, - mainNetworkName: string, -): (NetworkConfig & { name: string }) | undefined { - const candidateNetworks = Object.keys(networks) - .map((n) => ({ ...networks[n], name: n })) - .filter((n) => n.chainId === chainId) - - if (candidateNetworks.length > 1) { - logWarn( - `Found multiple networks with chainId ${chainId}, trying to use main network name to desambiguate`, - ) - - const filteredByMainNetworkName = candidateNetworks.filter((n) => n.name === mainNetworkName) - - if (filteredByMainNetworkName.length === 1) { - logDebug(`Found network with chainId ${chainId} and name ${mainNetworkName}`) - return filteredByMainNetworkName[0] - } else { - logWarn(`Could not desambiguate with main network name, trying secondary network name`) - const secondaryNetworkName = counterpartName(mainNetworkName) - const filteredBySecondaryNetworkName = candidateNetworks.filter( - (n) => n.name === secondaryNetworkName, - ) - - if (filteredBySecondaryNetworkName.length === 1) { - logDebug(`Found network with chainId ${chainId} and name ${mainNetworkName}`) - return filteredBySecondaryNetworkName[0] - } else { - throw new GREPluginError( - `Could not desambiguate network with chainID ${chainId}. Use case not supported!`, - ) - } - } - } else if (candidateNetworks.length === 1) { - return candidateNetworks[0] - } else { - return undefined - } -} - -export function getNetworkName( - networks: NetworksConfig, - chainId: number, - mainNetworkName: string, -): string | undefined { - const network = getNetworkConfig(networks, chainId, mainNetworkName) - return network?.name -} - -function normalizePath(_path: string, graphPath: string) { - if (!path.isAbsolute(_path)) { - _path = path.join(graphPath, _path) - } - return _path -} diff --git a/gre/gre.ts b/gre/gre.ts index f30c41504..d7e18e594 100644 --- a/gre/gre.ts +++ b/gre/gre.ts @@ -10,13 +10,16 @@ import { GraphRuntimeEnvironment, GraphRuntimeEnvironmentOptions, } from './type-extensions' -import { getChains, getProviders, getAddressBookPath, getGraphConfigPaths } from './config' +import { getChains, getDefaultProviders, getAddressBookPath, getGraphConfigPaths } from './config' import { getDeployer, getNamedAccounts, getTestAccounts, getWallet, getWallets } from './accounts' -import { logDebug, logWarn } from './logger' +import { logDebug, logWarn } from './helpers/logger' import path from 'path' import { EthersProviderWrapper } from '@nomiclabs/hardhat-ethers/internal/ethers-provider-wrapper' import { Wallet } from 'ethers' +import 'hardhat-secure-accounts' +import { getSecureAccountsProvider } from './providers' + // Graph Runtime Environment (GRE) extensions for the HRE extendConfig((config: HardhatConfig, userConfig: Readonly) => { @@ -43,8 +46,46 @@ extendEnvironment((hre: HardhatRuntimeEnvironment) => { logDebug('*** Initializing Graph Runtime Environment (GRE) ***') logDebug(`Main network: ${hre.network.name}`) + const enableTxLogging = opts.enableTxLogging ?? false + logDebug(`Tx logging: ${enableTxLogging ? 'enabled' : 'disabled'}`) + + const secureAccounts = !( + opts.disableSecureAccounts ?? + hre.config.graph.disableSecureAccounts ?? + false + ) + logDebug(`Secure accounts: ${secureAccounts ? 'enabled' : 'disabled'}`) + const { l1ChainId, l2ChainId, isHHL1 } = getChains(hre.network.config.chainId) - const { l1Provider, l2Provider } = getProviders(hre, l1ChainId, l2ChainId, isHHL1) + + // Default providers for L1 and L2 + const { l1Provider, l2Provider } = getDefaultProviders(hre, l1ChainId, l2ChainId, isHHL1) + + // Getters to unlock secure account providers for L1 and L2 + const l1UnlockProvider = () => + getSecureAccountsProvider( + hre.accounts, + hre.config.networks, + l1ChainId, + hre.network.name, + isHHL1, + 'L1', + opts.l1AccountName, + opts.l1AccountPassword, + ) + + const l2UnlockProvider = () => + getSecureAccountsProvider( + hre.accounts, + hre.config.networks, + l2ChainId, + hre.network.name, + !isHHL1, + 'L2', + opts.l2AccountName, + opts.l2AccountPassword, + ) + const addressBookPath = getAddressBookPath(hre, opts) const { l1GraphConfigPath, l2GraphConfigPath } = getGraphConfigPaths( hre, @@ -69,8 +110,11 @@ extendEnvironment((hre: HardhatRuntimeEnvironment) => { l1GraphConfigPath, addressBookPath, isHHL1, + enableTxLogging, + secureAccounts, l1GetWallets, l1GetWallet, + l1UnlockProvider, ) const l2Graph: GraphNetworkEnvironment | null = buildGraphNetworkEnvironment( @@ -79,8 +123,11 @@ extendEnvironment((hre: HardhatRuntimeEnvironment) => { l2GraphConfigPath, addressBookPath, isHHL1, + enableTxLogging, + secureAccounts, l2GetWallets, l2GetWallet, + l2UnlockProvider, ) const gre: GraphRuntimeEnvironment = { @@ -102,8 +149,11 @@ function buildGraphNetworkEnvironment( graphConfigPath: string | undefined, addressBookPath: string, isHHL1: boolean, + enableTxLogging: boolean, + secureAccounts: boolean, getWallets: () => Promise, getWallet: (address: string) => Promise, + unlockProvider: () => Promise, ): GraphNetworkEnvironment | null { if (graphConfigPath === undefined) { logWarn( @@ -121,6 +171,9 @@ function buildGraphNetworkEnvironment( return null } + // Upgrade provider to secure accounts if feature is enabled + const getUpdatedProvider = async () => (secureAccounts ? await unlockProvider() : provider) + return { chainId: chainId, provider: provider, @@ -129,10 +182,14 @@ function buildGraphNetworkEnvironment( contracts: lazyObject(() => loadContracts(getAddressBook(addressBookPath, chainId.toString()), chainId, provider), ), - getDeployer: lazyFunction(() => () => getDeployer(provider)), - getNamedAccounts: lazyFunction(() => () => getNamedAccounts(provider, graphConfigPath)), - getTestAccounts: lazyFunction(() => () => getTestAccounts(provider, graphConfigPath)), getWallets: lazyFunction(() => () => getWallets()), getWallet: lazyFunction(() => (address: string) => getWallet(address)), + getDeployer: lazyFunction(() => async () => getDeployer(await getUpdatedProvider())), + getNamedAccounts: lazyFunction( + () => async () => getNamedAccounts(await getUpdatedProvider(), graphConfigPath), + ), + getTestAccounts: lazyFunction( + () => async () => getTestAccounts(await getUpdatedProvider(), graphConfigPath), + ), } } diff --git a/gre/helpers/chain.ts b/gre/helpers/chain.ts new file mode 100644 index 000000000..4530904b6 --- /dev/null +++ b/gre/helpers/chain.ts @@ -0,0 +1,69 @@ +class MapWithGetKey extends Map { + getKey(value: K): K | undefined { + for (const [k, v] of this.entries()) { + if (v === value) { + return k + } + } + return + } +} + +const chainMap = new MapWithGetKey([ + [1, 42161], // Ethereum Mainnet - Arbitrum One + [4, 421611], // Ethereum Rinkeby - Arbitrum Rinkeby + [5, 421613], // Ethereum Goerli - Arbitrum Goerli + [1337, 412346], // Localhost - Arbitrum Localhost +]) + +// Hardhat network names as per our convention +const nameMap = new MapWithGetKey([ + ['mainnet', 'arbitrum-one'], // Ethereum Mainnet - Arbitrum One + ['rinkeby', 'arbitrum-rinkeby'], // Ethereum Rinkeby - Arbitrum Rinkeby + ['goerli', 'arbitrum-goerli'], // Ethereum Goerli - Arbitrum Goerli + ['localnitrol1', 'localnitrol2'], // Arbitrum testnode L1 - Arbitrum testnode L2 +]) + +export const l1Chains = Array.from(chainMap.keys()) +export const l2Chains = Array.from(chainMap.values()) +export const chains = [...l1Chains, ...l2Chains] + +export const l1ChainNames = Array.from(nameMap.keys()) +export const l2ChainNames = Array.from(nameMap.values()) +export const chainNames = [...l1ChainNames, ...l2ChainNames] + +export const isL1 = (chainId: number): boolean => l1Chains.includes(chainId) +export const isL2 = (chainId: number): boolean => l2Chains.includes(chainId) +export const isSupported = (chainId: number | undefined): boolean => + chainId !== undefined && chains.includes(chainId) + +export const isL1Name = (name: string): boolean => l1ChainNames.includes(name) +export const isL2Name = (name: string): boolean => l2ChainNames.includes(name) +export const isSupportedName = (name: string | undefined): boolean => + name !== undefined && chainNames.includes(name) + +export const l1ToL2 = (chainId: number): number | undefined => chainMap.get(chainId) +export const l2ToL1 = (chainId: number): number | undefined => chainMap.getKey(chainId) +export const counterpart = (chainId: number): number | undefined => { + if (!isSupported(chainId)) return + return isL1(chainId) ? l1ToL2(chainId) : l2ToL1(chainId) +} + +export const l1ToL2Name = (name: string): string | undefined => nameMap.get(name) +export const l2ToL1Name = (name: string): string | undefined => nameMap.getKey(name) +export const counterpartName = (name: string): string | undefined => { + if (!isSupportedName(name)) return + return isL1Name(name) ? l1ToL2Name(name) : l2ToL1Name(name) +} + +export default { + l1Chains, + l2Chains, + chains, + isL1, + isL2, + isSupported, + l1ToL2, + l2ToL1, + counterpart, +} diff --git a/gre/helpers/error.ts b/gre/helpers/error.ts index f9aafa748..46a2d7122 100644 --- a/gre/helpers/error.ts +++ b/gre/helpers/error.ts @@ -1,5 +1,5 @@ import { HardhatPluginError } from 'hardhat/plugins' -import { logError } from '../logger' +import { logError } from './logger' export class GREPluginError extends HardhatPluginError { constructor(message: string) { diff --git a/gre/logger.ts b/gre/helpers/logger.ts similarity index 100% rename from gre/logger.ts rename to gre/helpers/logger.ts diff --git a/gre/helpers/network.ts b/gre/helpers/network.ts index 4530904b6..eb2980556 100644 --- a/gre/helpers/network.ts +++ b/gre/helpers/network.ts @@ -1,69 +1,55 @@ -class MapWithGetKey extends Map { - getKey(value: K): K | undefined { - for (const [k, v] of this.entries()) { - if (v === value) { - return k +import { NetworkConfig, NetworksConfig } from 'hardhat/types/config' +import { logDebug, logWarn } from './logger' +import { GREPluginError } from './error' +import { counterpartName } from './chain' + +export function getNetworkConfig( + networks: NetworksConfig, + chainId: number, + mainNetworkName: string, +): (NetworkConfig & { name: string }) | undefined { + const candidateNetworks = Object.keys(networks) + .map((n) => ({ ...networks[n], name: n })) + .filter((n) => n.chainId === chainId) + + if (candidateNetworks.length > 1) { + logWarn( + `Found multiple networks with chainId ${chainId}, trying to use main network name to desambiguate`, + ) + + const filteredByMainNetworkName = candidateNetworks.filter((n) => n.name === mainNetworkName) + + if (filteredByMainNetworkName.length === 1) { + logDebug(`Found network with chainId ${chainId} and name ${mainNetworkName}`) + return filteredByMainNetworkName[0] + } else { + logWarn(`Could not desambiguate with main network name, trying secondary network name`) + const secondaryNetworkName = counterpartName(mainNetworkName) + const filteredBySecondaryNetworkName = candidateNetworks.filter( + (n) => n.name === secondaryNetworkName, + ) + + if (filteredBySecondaryNetworkName.length === 1) { + logDebug(`Found network with chainId ${chainId} and name ${mainNetworkName}`) + return filteredBySecondaryNetworkName[0] + } else { + throw new GREPluginError( + `Could not desambiguate network with chainID ${chainId}. Use case not supported!`, + ) } } - return + } else if (candidateNetworks.length === 1) { + return candidateNetworks[0] + } else { + return undefined } } -const chainMap = new MapWithGetKey([ - [1, 42161], // Ethereum Mainnet - Arbitrum One - [4, 421611], // Ethereum Rinkeby - Arbitrum Rinkeby - [5, 421613], // Ethereum Goerli - Arbitrum Goerli - [1337, 412346], // Localhost - Arbitrum Localhost -]) - -// Hardhat network names as per our convention -const nameMap = new MapWithGetKey([ - ['mainnet', 'arbitrum-one'], // Ethereum Mainnet - Arbitrum One - ['rinkeby', 'arbitrum-rinkeby'], // Ethereum Rinkeby - Arbitrum Rinkeby - ['goerli', 'arbitrum-goerli'], // Ethereum Goerli - Arbitrum Goerli - ['localnitrol1', 'localnitrol2'], // Arbitrum testnode L1 - Arbitrum testnode L2 -]) - -export const l1Chains = Array.from(chainMap.keys()) -export const l2Chains = Array.from(chainMap.values()) -export const chains = [...l1Chains, ...l2Chains] - -export const l1ChainNames = Array.from(nameMap.keys()) -export const l2ChainNames = Array.from(nameMap.values()) -export const chainNames = [...l1ChainNames, ...l2ChainNames] - -export const isL1 = (chainId: number): boolean => l1Chains.includes(chainId) -export const isL2 = (chainId: number): boolean => l2Chains.includes(chainId) -export const isSupported = (chainId: number | undefined): boolean => - chainId !== undefined && chains.includes(chainId) - -export const isL1Name = (name: string): boolean => l1ChainNames.includes(name) -export const isL2Name = (name: string): boolean => l2ChainNames.includes(name) -export const isSupportedName = (name: string | undefined): boolean => - name !== undefined && chainNames.includes(name) - -export const l1ToL2 = (chainId: number): number | undefined => chainMap.get(chainId) -export const l2ToL1 = (chainId: number): number | undefined => chainMap.getKey(chainId) -export const counterpart = (chainId: number): number | undefined => { - if (!isSupported(chainId)) return - return isL1(chainId) ? l1ToL2(chainId) : l2ToL1(chainId) -} - -export const l1ToL2Name = (name: string): string | undefined => nameMap.get(name) -export const l2ToL1Name = (name: string): string | undefined => nameMap.getKey(name) -export const counterpartName = (name: string): string | undefined => { - if (!isSupportedName(name)) return - return isL1Name(name) ? l1ToL2Name(name) : l2ToL1Name(name) -} - -export default { - l1Chains, - l2Chains, - chains, - isL1, - isL2, - isSupported, - l1ToL2, - l2ToL1, - counterpart, +export function getNetworkName( + networks: NetworksConfig, + chainId: number, + mainNetworkName: string, +): string | undefined { + const network = getNetworkConfig(networks, chainId, mainNetworkName) + return network?.name } diff --git a/gre/helpers/utils.ts b/gre/helpers/utils.ts new file mode 100644 index 000000000..e8930621e --- /dev/null +++ b/gre/helpers/utils.ts @@ -0,0 +1,8 @@ +import path from 'path' + +export function normalizePath(_path: string, graphPath: string): string { + if (!path.isAbsolute(_path)) { + _path = path.join(graphPath, _path) + } + return _path +} diff --git a/gre/providers.ts b/gre/providers.ts new file mode 100644 index 000000000..61187e6c1 --- /dev/null +++ b/gre/providers.ts @@ -0,0 +1,97 @@ +import { HardhatRuntimeEnvironment, Network } from 'hardhat/types/runtime' +import { NetworksConfig, HttpNetworkConfig } from 'hardhat/types/config' +import { EthersProviderWrapper } from '@nomiclabs/hardhat-ethers/internal/ethers-provider-wrapper' +import { HARDHAT_NETWORK_NAME } from 'hardhat/plugins' +import { createProvider } from 'hardhat/internal/core/providers/construction' + +import { getNetworkConfig, getNetworkName } from './helpers/network' +import { logDebug } from './helpers/logger' + +import { GREPluginError } from './helpers/error' +import { AccountsRuntimeEnvironment } from 'hardhat-secure-accounts/dist/src/type-extensions' + +export const getDefaultProvider = ( + networks: NetworksConfig, + chainId: number, + mainNetworkName: string, + isMainProvider: boolean, + chainLabel: string, +): EthersProviderWrapper | undefined => { + const { networkConfig, networkName } = getNetworkData( + networks, + chainId, + mainNetworkName, + isMainProvider, + chainLabel, + ) + + if (networkConfig === undefined || networkName === undefined) { + return undefined + } + + logDebug(`Creating provider for ${chainLabel}(${networkName})`) + const ethereumProvider = createProvider(networkName, networkConfig) + const ethersProviderWrapper = new EthersProviderWrapper(ethereumProvider) + return ethersProviderWrapper +} + +export const getSecureAccountsProvider = async ( + accounts: AccountsRuntimeEnvironment, + networks: NetworksConfig, + chainId: number, + mainNetworkName: string, + isMainProvider: boolean, + chainLabel: string, + accountName?: string, + accountPassword?: string, +): Promise => { + const { networkConfig, networkName } = getNetworkData( + networks, + chainId, + mainNetworkName, + isMainProvider, + chainLabel, + ) + + if (networkConfig === undefined || networkName === undefined) { + return undefined + } + + logDebug(`Using secure accounts provider for ${chainLabel}(${networkName})`) + if (accountName === undefined || accountPassword === undefined) { + console.log( + `== Using secure accounts, please unlock an account for ${chainLabel}(${networkName})`, + ) + } + + return await accounts.getProvider( + { name: networkName, config: networkConfig } as Network, + accountName, + accountPassword, + ) +} + +const getNetworkData = ( + networks: NetworksConfig, + chainId: number, + mainNetworkName: string, + isMainProvider: boolean, + chainLabel: string, +): { networkConfig: HttpNetworkConfig | undefined; networkName: string | undefined } => { + const networkConfig = getNetworkConfig(networks, chainId, mainNetworkName) as HttpNetworkConfig + const networkName = getNetworkName(networks, chainId, mainNetworkName) + + logDebug(`Provider url for ${chainLabel}(${networkName}): ${networkConfig?.url}`) + + // Ensure at least main provider is configured + // For Hardhat network we don't need url to create a provider + if ( + isMainProvider && + (networkConfig === undefined || networkConfig.url === undefined) && + networkName !== HARDHAT_NETWORK_NAME + ) { + throw new GREPluginError(`Must set a provider url for chain: ${chainId}!`) + } + + return { networkConfig, networkName } +} diff --git a/gre/test/accounts.test.ts b/gre/test/accounts.test.ts index f309aed95..74b4020cd 100644 --- a/gre/test/accounts.test.ts +++ b/gre/test/accounts.test.ts @@ -83,3 +83,100 @@ describe('GRE usage > account management', function () { }) }) }) + +describe('GRE usage > secure accounts', function () { + useEnvironment('graph-config', 'hardhat') + + let graph: GraphRuntimeEnvironment + let graphSecureAccounts: GraphRuntimeEnvironment + + beforeEach(function () { + graph = this.hre.graph({ + disableSecureAccounts: true, + }) + + graphSecureAccounts = this.hre.graph({ + disableSecureAccounts: false, + l1AccountName: 'test-account', + l1AccountPassword: 'batman-with-cheese', + l2AccountName: 'test-account-l2', + l2AccountPassword: 'batman-with-cheese', + }) + }) + + describe('getDeployer', function () { + it('should return different accounts', async function () { + const deployer = await graph.l1.getDeployer() + const deployerSecureAccount = await graphSecureAccounts.l1.getDeployer() + + expect(deployer.address).not.to.equal(deployerSecureAccount.address) + expect(deployer.address).to.equal('0x2770fb12b368a9aBf4A02DB34B0F6057fC03BD0d') + expect(deployerSecureAccount.address).to.equal('0xC108fda1b5b2903751594298769Efd4904b146bD') + }) + + it('should return accounts capable of signing messages', async function () { + const deployer = await graph.l1.getDeployer() + const deployerSecureAccount = await graphSecureAccounts.l1.getDeployer() + + expect(deployer.signMessage('test')).to.eventually.be.fulfilled + expect(deployerSecureAccount.signMessage('test')).to.eventually.be.fulfilled + }) + }) + + describe('getNamedAccounts', function () { + it('should return the same accounts', async function () { + const accounts = await graph.l1.getNamedAccounts() + const secureAccounts = await graphSecureAccounts.l1.getNamedAccounts() + + const accountNames = Object.keys(accounts) + const secureAccountNames = Object.keys(secureAccounts) + + expect(accountNames.length).to.equal(secureAccountNames.length) + + for (const name of accountNames) { + const account = accounts[name] + const secureAccount = secureAccounts[name] + + expect(account.address).to.equal(secureAccount.address) + } + }) + + it('should return accounts incapable of signing messages', async function () { + const accounts = await graph.l1.getNamedAccounts() + const secureAccounts = await graphSecureAccounts.l1.getNamedAccounts() + + const accountNames = Object.keys(accounts) + + for (const name of accountNames) { + const account = accounts[name] + const secureAccount = secureAccounts[name] + + expect(account.signMessage('test')).to.eventually.be.rejectedWith(/unknown account/) + expect(secureAccount.signMessage('test')).to.eventually.be.rejected + } + }) + }) + + describe('getTestAccounts', function () { + it('should return different accounts', async function () { + const accounts = await graph.l1.getTestAccounts() + const secureAccounts = await graphSecureAccounts.l1.getTestAccounts() + + expect(accounts.length).to.equal(secureAccounts.length) + + for (let i = 0; i < accounts.length; i++) { + expect(accounts[i].address).not.to.equal(secureAccounts[i].address) + } + }) + + it('should return accounts capable of signing messages', async function () { + const accounts = await graph.l1.getTestAccounts() + const secureAccounts = await graphSecureAccounts.l1.getTestAccounts() + + for (let i = 0; i < accounts.length; i++) { + expect(accounts[i].signMessage('test')).to.eventually.be.fulfilled + expect(secureAccounts[i].signMessage('test')).to.eventually.be.fulfilled + } + }) + }) +}) diff --git a/gre/test/config.test.ts b/gre/test/config.test.ts index de6287e2e..e150b41c3 100644 --- a/gre/test/config.test.ts +++ b/gre/test/config.test.ts @@ -1,15 +1,10 @@ import { expect } from 'chai' import { useEnvironment } from './helpers' - -import { - getAddressBookPath, - getChains, - getGraphConfigPaths, - getNetworkName, - getProviders, -} from '../config' import path from 'path' +import { getAddressBookPath, getChains, getDefaultProviders, getGraphConfigPaths } from '../config' +import { getNetworkName } from '../helpers/network' + describe('GRE init functions', function () { describe('getAddressBookPath with graph-config project', function () { useEnvironment('graph-config') @@ -66,45 +61,45 @@ describe('GRE init functions', function () { }) }) - describe('getProviders with graph-config project', function () { + describe('getDefaultProviders with graph-config project', function () { useEnvironment('graph-config') it('should return L1 and L2 providers for supported networks (HH L1)', function () { - const { l1Provider, l2Provider } = getProviders(this.hre, 5, 421613, true) + const { l1Provider, l2Provider } = getDefaultProviders(this.hre, 5, 421613, true) expect(l1Provider).to.be.an('object') expect(l2Provider).to.be.an('object') }) it('should return L1 and L2 providers for supported networks (HH L2)', function () { - const { l1Provider, l2Provider } = getProviders(this.hre, 5, 421613, false) + const { l1Provider, l2Provider } = getDefaultProviders(this.hre, 5, 421613, false) expect(l1Provider).to.be.an('object') expect(l2Provider).to.be.an('object') }) it('should return only L1 provider if L2 is not supported (HH L1)', function () { - const { l1Provider, l2Provider } = getProviders(this.hre, 5, 123456, true) + const { l1Provider, l2Provider } = getDefaultProviders(this.hre, 5, 123456, true) expect(l1Provider).to.be.an('object') expect(l2Provider).to.be.undefined }) it('should return only L2 provider if L1 is not supported (HH L2)', function () { - const { l1Provider, l2Provider } = getProviders(this.hre, 123456, 421613, false) + const { l1Provider, l2Provider } = getDefaultProviders(this.hre, 123456, 421613, false) expect(l1Provider).to.be.undefined expect(l2Provider).to.be.an('object') }) }) - describe('getProviders with graph-config-bad project', function () { + describe('getDefaultProviders with graph-config-bad project', function () { useEnvironment('graph-config-bad') it('should throw if main network is not defined in hardhat config (HH L1)', function () { - expect(() => getProviders(this.hre, 4, 421611, true)).to.throw( + expect(() => getDefaultProviders(this.hre, 4, 421611, true)).to.throw( /Must set a provider url for chain: /, ) }) it('should throw if main network is not defined in hardhat config (HH L2)', function () { - expect(() => getProviders(this.hre, 5, 421613, false)).to.throw( + expect(() => getDefaultProviders(this.hre, 5, 421613, false)).to.throw( /Must set a provider url for chain: /, ) }) @@ -114,7 +109,7 @@ describe('GRE init functions', function () { useEnvironment('graph-config-desambiguate', 'localnitrol1') it('should use main network name to desambiguate if multiple chains are defined with same chainId', async function () { - const { l1Provider, l2Provider } = getProviders(this.hre, 1337, 412346, true) + const { l1Provider, l2Provider } = getDefaultProviders(this.hre, 1337, 412346, true) expect(l1Provider).to.be.an('object') expect(l2Provider).to.be.an('object') @@ -129,7 +124,7 @@ describe('GRE init functions', function () { useEnvironment('graph-config-desambiguate', 'localnitrol2') it('should use secondary network name to desambiguate if multiple chains are defined with same chainId', async function () { - const { l1Provider, l2Provider } = getProviders(this.hre, 1337, 412346, true) + const { l1Provider, l2Provider } = getDefaultProviders(this.hre, 1337, 412346, true) expect(l1Provider).to.be.an('object') expect(l2Provider).to.be.an('object') diff --git a/gre/test/files/config/graph.goerli.yml b/gre/test/files/config/graph.goerli.yml index e69de29bb..d15d1d25a 100644 --- a/gre/test/files/config/graph.goerli.yml +++ b/gre/test/files/config/graph.goerli.yml @@ -0,0 +1,7 @@ +general: + arbitrator: &arbitrator "0xFD01aa87BeB04D0ac764FC298aCFd05FfC5439cD" # Arbitration Council + governor: &governor "0xf1135bFF22512FF2A585b8d4489426CE660f204c" # Graph Council + authority: &authority "0x52e498aE9B8A5eE2A5Cd26805F06A9f29A7F489F" # Authority that signs payment vouchers + availabilityOracle: &availabilityOracle "0x14053D40ea2E81D3AB0739728a54ab84F21200F9" # Subgraph Availability Oracle + pauseGuardian: &pauseGuardian "0x6855D551CaDe60754D145fb5eDCD90912D860262" # Protocol pause guardian + allocationExchangeOwner: &allocationExchangeOwner "0xf1135bFF22512FF2A585b8d4489426CE660f204c" # Allocation Exchange owner diff --git a/gre/test/fixture-projects/graph-config/.accounts/test-account-l2.json b/gre/test/fixture-projects/graph-config/.accounts/test-account-l2.json new file mode 100644 index 000000000..ab1ae36bb --- /dev/null +++ b/gre/test/fixture-projects/graph-config/.accounts/test-account-l2.json @@ -0,0 +1,28 @@ +{ + "address": "5baa8472c470a400f830e2458ddb97b13cc8eb32", + "id": "ead5a876-efae-4cdf-aeab-ab81907427c8", + "version": 3, + "Crypto": { + "cipher": "aes-128-ctr", + "cipherparams": { "iv": "5e90eb61380cee63382bd8c935eea554" }, + "ciphertext": "67800c67ab32b8baf2df4a697aa1108ee7f91b5a182ff2e29fa562009e1bbd9f", + "kdf": "scrypt", + "kdfparams": { + "salt": "415db4971651654fb4b381f86525c273e4c7470a69307f7c83f71ec38aca7d12", + "n": 131072, + "dklen": 32, + "p": 1, + "r": 8 + }, + "mac": "f5611372940c7da01e774aaf35046a5b3c4eec050d482b9f0912707ba645e681" + }, + "x-ethers": { + "client": "ethers.js", + "gethFilename": "UTC--2022-08-25T14-48-23.0Z--5baa8472c470a400f830e2458ddb97b13cc8eb32", + "mnemonicCounter": "b84bf04ecd5d0ab111950ee4cf168d86", + "mnemonicCiphertext": "672a53846059b4e8bae97747d684529a", + "path": "m/44'/60'/0'/0/0", + "locale": "en", + "version": "0.1" + } +} diff --git a/gre/test/fixture-projects/graph-config/.accounts/test-account.json b/gre/test/fixture-projects/graph-config/.accounts/test-account.json new file mode 100644 index 000000000..de055e684 --- /dev/null +++ b/gre/test/fixture-projects/graph-config/.accounts/test-account.json @@ -0,0 +1,28 @@ +{ + "address": "c108fda1b5b2903751594298769efd4904b146bd", + "id": "37ec99f7-8244-4982-b2d4-173c244784f3", + "version": 3, + "Crypto": { + "cipher": "aes-128-ctr", + "cipherparams": { "iv": "1eb9d55c0882a50e7988a09e674c2402" }, + "ciphertext": "822fd907f44e48d15d500433200ac244b70487813982936a88c0830fa9cd66b6", + "kdf": "scrypt", + "kdfparams": { + "salt": "f6d158afdf9a11d3353fbe736cbb769626c8428015603c6449ca1fa0b42e3c2e", + "n": 131072, + "dklen": 32, + "p": 1, + "r": 8 + }, + "mac": "1af4526f4e62b6722226ee1c3a18d7f5dfff0d5b7862ca123989e7a464153f28" + }, + "x-ethers": { + "client": "ethers.js", + "gethFilename": "UTC--2022-08-24T12-27-39.0Z--c108fda1b5b2903751594298769efd4904b146bd", + "mnemonicCounter": "3bd3b82c7148351fe0cdc005a631d445", + "mnemonicCiphertext": "f2bc1c5598c60fe265bf7908344fde6d", + "path": "m/44'/60'/0'/0/0", + "locale": "en", + "version": "0.1" + } +} diff --git a/gre/test/fixture-projects/graph-config/hardhat.config.ts b/gre/test/fixture-projects/graph-config/hardhat.config.ts index 1def7b415..bbd6c079c 100644 --- a/gre/test/fixture-projects/graph-config/hardhat.config.ts +++ b/gre/test/fixture-projects/graph-config/hardhat.config.ts @@ -3,6 +3,7 @@ import '../../../gre' module.exports = { paths: { graph: '../../files', + accounts: '.accounts', }, solidity: '0.8.9', defaultNetwork: 'hardhat', diff --git a/gre/type-extensions.d.ts b/gre/type-extensions.d.ts index c20277927..8d5007bad 100644 --- a/gre/type-extensions.d.ts +++ b/gre/type-extensions.d.ts @@ -10,6 +10,14 @@ export interface GraphRuntimeEnvironmentOptions { l1GraphConfig?: string l2GraphConfig?: string graphConfig?: string + enableTxLogging?: boolean + disableSecureAccounts?: boolean + + // These are mostly for testing purposes + l1AccountName?: string + l2AccountName?: string + l1AccountPassword?: string + l2AccountPassword?: string } export type AccountNames = diff --git a/hardhat.config.ts b/hardhat.config.ts index aa89f8715..3b989dcd0 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -29,7 +29,7 @@ const SKIP_LOAD = process.env.SKIP_LOAD === 'true' function loadTasks() { require('./gre/gre') - ;['contracts', 'misc', 'deployment', 'actions', 'verify', 'e2e'].forEach((folder) => { + ;['contracts', 'bridge', 'deployment', 'actions', 'verify', 'e2e'].forEach((folder) => { const tasksPath = path.join(__dirname, 'tasks', folder) fs.readdirSync(tasksPath) .filter((pth) => pth.includes('.ts')) @@ -59,15 +59,21 @@ interface NetworkConfig { const networkConfigs: NetworkConfig[] = [ { network: 'mainnet', chainId: 1, graphConfig: 'config/graph.mainnet.yml' }, - { network: 'rinkeby', chainId: 4 }, - { network: 'goerli', chainId: 5 }, + { network: 'rinkeby', chainId: 4, graphConfig: 'config/graph.rinkeby.yml' }, + { network: 'goerli', chainId: 5, graphConfig: 'config/graph.goerli.yml' }, { network: 'kovan', chainId: 42 }, { network: 'arbitrum-rinkeby', chainId: 421611, url: 'https://rinkeby.arbitrum.io/rpc' }, - { network: 'arbitrum-one', chainId: 42161, url: 'https://arb1.arbitrum.io/rpc' }, + { + network: 'arbitrum-one', + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + graphConfig: 'config/graph.arbitrum-one.yml', + }, { network: 'arbitrum-goerli', chainId: 421613, url: 'https://goerli-rollup.arbitrum.io/rpc', + graphConfig: 'config/graph.arbitrum-goerli.yml', }, ] @@ -101,6 +107,9 @@ function setupNetworkProviders(hardhatConfig) { const DEFAULT_TEST_MNEMONIC = 'myth like bonus scare over problem client lizard pioneer submit female collect' +const DEFAULT_L2_TEST_MNEMONIC = + 'urge never interest human any economy gentle canvas anxiety pave unlock find' + const config: HardhatUserConfig = { paths: { sources: './contracts', @@ -145,28 +154,32 @@ const config: HardhatUserConfig = { interval: 13000, }, hardfork: 'london', + graphConfig: 'config/graph.localhost.yml', }, localhost: { chainId: 1337, url: 'http://localhost:8545', accounts: process.env.FORK === 'true' ? getAccountsKeys() : { mnemonic: DEFAULT_TEST_MNEMONIC }, + graphConfig: 'config/graph.localhost.yml', }, localnitrol1: { chainId: 1337, url: 'http://localhost:8545', accounts: { mnemonic: DEFAULT_TEST_MNEMONIC }, + graphConfig: 'config/graph.localhost.yml', }, localnitrol2: { chainId: 412346, url: 'http://localhost:8547', - accounts: { mnemonic: DEFAULT_TEST_MNEMONIC }, + accounts: { mnemonic: DEFAULT_L2_TEST_MNEMONIC }, + graphConfig: 'config/graph.arbitrum-localhost.yml', }, }, graph: { addressBook: process.env.ADDRESS_BOOK ?? 'addresses.json', - l1GraphConfig: process.env.GRAPH_CONFIG ?? 'config/graph.localhost.yml', - l2GraphConfig: process.env.L2_GRAPH_CONFIG, + l1GraphConfig: process.env.L1_GRAPH_CONFIG ?? 'config/graph.localhost.yml', + l2GraphConfig: process.env.L2_GRAPH_CONFIG ?? 'config/graph.arbitrum-localhost.yml', }, etherscan: { apiKey: process.env.ETHERSCAN_API_KEY, diff --git a/package.json b/package.json index 63fab4bd3..ef123981e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@graphprotocol/contracts", - "version": "1.16.0", + "version": "2.0.0", "description": "Contracts for the Graph Protocol", "directories": { "test": "test" @@ -15,7 +15,7 @@ "ethers": "^5.6.0" }, "devDependencies": { - "@arbitrum/sdk": "^3.0.0-beta.6", + "@arbitrum/sdk": "^3.0.0", "@commitlint/cli": "^13.2.1", "@commitlint/config-conventional": "^13.2.0", "@defi-wonderland/smock": "^2.0.7", @@ -61,6 +61,7 @@ "hardhat-abi-exporter": "^2.2.0", "hardhat-contract-sizer": "^2.0.3", "hardhat-gas-reporter": "^1.0.4", + "hardhat-secure-accounts": "0.0.5", "hardhat-storage-layout": "0.1.6", "hardhat-tracer": "^1.0.0-alpha.6", "husky": "^7.0.4", @@ -89,7 +90,6 @@ "compile": "hardhat compile", "deploy": "yarn build && yarn predeploy && hardhat migrate", "deploy-localhost": "yarn deploy --force --network localhost --graph-config config/graph.localhost.yml", - "deploy-rinkeby": "yarn deploy --force --network rinkeby --graph-config config/graph.rinkeby.yml", "deploy-goerli": "yarn deploy --force --network goerli --graph-config config/graph.goerli.yml", "deploy-arbitrum-goerli": "yarn deploy --force --network arbitrum-goerli --graph-config config/graph.arbitrum-goerli.yml", "predeploy": "scripts/predeploy", diff --git a/scripts/e2e b/scripts/e2e index c5008f25e..70aae7e63 100755 --- a/scripts/e2e +++ b/scripts/e2e @@ -3,22 +3,168 @@ set -eo pipefail source $(pwd)/scripts/evm +### > SCRIPT CONFIG < # Allow overriding config -GRAPH_CONFIG=${GRAPH_CONFIG:-"config/graph.localhost.yml"} -ADDRESS_BOOK=${ADDRESS_BOOK:-"addresses.json"} -NETWORK=${NETWORK:-"localhost"} +ADDRESS_BOOK=${ADDRESS_BOOK:-"addresses-local.json"} +ARBITRUM_ADDRESS_BOOK=${ARBITRUM_ADDRESS_BOOK:-"arbitrum-addresses-local.json"} +ARBITRUM_DEPLOYMENT_FILE=${ARBITRUM_DEPLOYMENT_FILE:-"localNetwork.json"} + +L1_NETWORK=${L1_NETWORK} +L2_NETWORK=${L2_NETWORK} + +L1_GRAPH_CONFIG=${L1_GRAPH_CONFIG:-"config/graph.localhost.yml"} +L2_GRAPH_CONFIG=${L2_GRAPH_CONFIG:-"config/graph.arbitrum-localhost.yml"} echo "Running e2e tests" -echo "- Using config: $GRAPH_CONFIG" echo "- Using address book: $ADDRESS_BOOK" -echo "- Using network: $NETWORK" -### Setup +if [[ -n "$L1_NETWORK" ]]; then + echo "- Using L1 network: $L1_NETWORK" + echo "- Using L1 config: $L1_GRAPH_CONFIG" +else + echo "- No L1_NETWORK provided, skipping L1 tests" +fi + +if [[ -n "$L2_NETWORK" ]]; then + echo "- Using L2 network: $L2_NETWORK" + echo "- Using L2 config: $L2_GRAPH_CONFIG" + echo "- Using arbitrum address book: $ARBITRUM_ADDRESS_BOOK" + echo "- Using arbitrum deployment file: $ARBITRUM_DEPLOYMENT_FILE" +else + echo "- No L2_NETWORK provided, skipping L2 tests" +fi + +if [[ -z "$L1_NETWORK" ]] && [[ -z "$L2_NETWORK" ]]; then + echo "Must specify one of L1_NETWORK or L2_NETWORK!" + exit 0 +fi + +if [[ "$L1_NETWORK" == "$L2_NETWORK" ]]; then + echo "L1_NETWORK and L2_NETWORK must be different networks!" + exit 0 +fi + +### > SCRIPT AUX FUNCTIONS < +function pre_deploy() { + local NETWORK=$1 + local GRAPH_CONFIG=$2 + + # Create named accounts + npx hardhat migrate:accounts --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --disable-secure-accounts + + # Fund accounts if using nitro test nodes + if [[ "$NETWORK" == *"localnitro"* ]]; then + npx hardhat nitro:fund-accounts --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --disable-secure-accounts + fi +} + +function deploy() { + local NETWORK=$1 + local GRAPH_CONFIG=$2 + local ADDRESS_BOOK=$3 + + # Deploy protocol + npx hardhat migrate \ + --network "$NETWORK" \ + --skip-confirmation \ + --auto-mine \ + --force \ + --graph-config "$GRAPH_CONFIG" \ + --address-book "$ADDRESS_BOOK" +} + +function post_deploy () { + local NETWORK=$1 + local GRAPH_CONFIG=$2 + local ADDRESS_BOOK=$3 + + # Governor to accept contracts ownership + npx hardhat migrate:ownership --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" --disable-secure-accounts + + # Unpause the protocol + npx hardhat migrate:unpause:protocol --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" --disable-secure-accounts +} + +function configure_bridge () { + local L1_NETWORK=$1 + local L1_GRAPH_CONFIG=$2 + local L2_NETWORK=$3 + local L2_GRAPH_CONFIG=$4 + local ADDRESS_BOOK=$5 + local ARBITRUM_ADDRESS_BOOK=$6 + local ARBITRUM_DEPLOYMENT_FILE=$7 + + # These settings are only used for CLI bridge commands + # so we keep them here to avoid confusion with hardhat based tasks + local L1_CHAIN_ID=${L1_CHAIN_ID:-"1337"} + local L2_CHAIN_ID=${L2_CHAIN_ID:-"412346"} + + local L1_RPC=${L1_RPC:-"http://localhost:8545"} + local L2_RPC=${L2_RPC:-"http://localhost:8547"} + + local L1_MNEMONIC=${L1_MNEMONIC:-"myth like bonus scare over problem client lizard pioneer submit female collect"} + local L2_MNEMONIC=${L2_MNEMONIC:-"urge never interest human any economy gentle canvas anxiety pave unlock find"} + + # Copy required arbitrum contract addresses to the local arbitrum address book + if [[ "$L1_NETWORK" == *"localnitro"* ]]; then + npx hardhat nitro:address-book-setup --deployment-file "$ARBITRUM_DEPLOYMENT_FILE" --arbitrum-address-book "$ARBITRUM_ADDRESS_BOOK" + fi + + # Configure the bridge + ./cli/cli.ts -a "$ADDRESS_BOOK" -p "$L2_RPC" -m "$L2_MNEMONIC" -n 2 -r "$ARBITRUM_ADDRESS_BOOK" protocol configure-l2-bridge "$L1_CHAIN_ID" + ./cli/cli.ts -a "$ADDRESS_BOOK" -p "$L1_RPC" -m "$L1_MNEMONIC" -n 2 -r "$ARBITRUM_ADDRESS_BOOK" protocol configure-l1-bridge "$L2_CHAIN_ID" + + # Unpause the bridge + npx hardhat migrate:unpause:bridge --network "$L2_NETWORK" --graph-config "$L2_GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" --disable-secure-accounts + npx hardhat migrate:unpause:bridge --network "$L1_NETWORK" --graph-config "$L1_GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" --disable-secure-accounts +} + +function test_e2e () { + local NETWORK=$1 + local L1_GRAPH_CONFIG=$2 + local L2_GRAPH_CONFIG=$3 + local ADDRESS_BOOK=$4 + local SKIP_BRIDGE_TESTS=$5 + + if [[ -z "$SKIP_BRIDGE_TESTS" ]]; then + npx hardhat e2e --network "$NETWORK" --l1-graph-config "$L1_GRAPH_CONFIG" --l2-graph-config "$L2_GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" + else + npx hardhat e2e --network "$NETWORK" --l1-graph-config "$L1_GRAPH_CONFIG" --l2-graph-config "$L2_GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" --skip-bridge + fi +} + +function test_e2e_scenarios () { + local NETWORK=$1 + local L1_GRAPH_CONFIG=$2 + local L2_GRAPH_CONFIG=$3 + local ADDRESS_BOOK=$4 + + npx hardhat e2e:scenario create-subgraphs --network "$NETWORK" --l1-graph-config "$L1_GRAPH_CONFIG" --l2-graph-config "$L2_GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" --disable-secure-accounts + npx hardhat e2e:scenario open-allocations --network "$NETWORK" --l1-graph-config "$L1_GRAPH_CONFIG" --l2-graph-config "$L2_GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" --disable-secure-accounts + + # skip close-allocations for arbitrum testnodes as we can't advance epoch + if [[ "$NETWORK" != *"localnitro"* ]]; then + npx hardhat e2e:scenario close-allocations --network "$NETWORK" --l1-graph-config "$L1_GRAPH_CONFIG" --l2-graph-config "$L2_GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" --disable-secure-accounts + fi +} + +function test_e2e_scenarios_bridge () { + local NETWORK=$1 + local L1_GRAPH_CONFIG=$2 + local L2_GRAPH_CONFIG=$3 + local ADDRESS_BOOK=$4 + + npx hardhat e2e:scenario send-grt-to-l2 --network "$NETWORK" --l1-graph-config "$L1_GRAPH_CONFIG" --l2-graph-config "$L2_GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" --disable-secure-accounts +} + + +### > SCRIPT START < ### +## SETUP # Compile contracts yarn build # Start evm -if [[ "$NETWORK" == "localhost" ]]; then +if [[ "$L1_NETWORK" == "localhost" || "$L2_NETWORK" == "localhost" ]]; then evm_kill evm_start fi @@ -28,42 +174,58 @@ if [[ ! -f "$ADDRESS_BOOK" ]]; then echo '{}' > "$ADDRESS_BOOK" fi -# Pre-deploy actions -npx hardhat migrate:accounts --network "$NETWORK" --graph-config "$GRAPH_CONFIG" -if [[ "$NETWORK" == *"localnitro"* ]]; then - npx hardhat migrate:accounts:nitro --network "$NETWORK" --graph-config "$GRAPH_CONFIG" +# Reset arbitrum address book (just in case the deployment changed) +if [[ -f "$ARBITRUM_ADDRESS_BOOK" ]]; then + rm "$ARBITRUM_ADDRESS_BOOK" +fi +echo '{}' > "$ARBITRUM_ADDRESS_BOOK" + +# Reset arbitrum address book (just in case the deployment changed) +if [[ -f "$ARBITRUM_DEPLOYMENT_FILE" ]]; then + rm "$ARBITRUM_DEPLOYMENT_FILE" +fi + +## DEPLOY +# Deploy L1 +if [[ -n "$L1_NETWORK" ]]; then + echo "Deploying L1 protocol" + pre_deploy "$L1_NETWORK" "$L1_GRAPH_CONFIG" + deploy "$L1_NETWORK" "$L1_GRAPH_CONFIG" "$ADDRESS_BOOK" + post_deploy "$L1_NETWORK" "$L1_GRAPH_CONFIG" "$ADDRESS_BOOK" +fi + +# Deploy L2 +if [[ -n "$L2_NETWORK" ]]; then + echo "Deploying L2 protocol" + pre_deploy "$L2_NETWORK" "$L2_GRAPH_CONFIG" + deploy "$L2_NETWORK" "$L2_GRAPH_CONFIG" "$ADDRESS_BOOK" + post_deploy "$L2_NETWORK" "$L2_GRAPH_CONFIG" "$ADDRESS_BOOK" +fi + +# Configure bridge +if [[ -n "$L1_NETWORK" ]] && [[ -n "$L2_NETWORK" ]]; then + configure_bridge "$L1_NETWORK" "$L1_GRAPH_CONFIG" "$L2_NETWORK" "$L2_GRAPH_CONFIG" "$ADDRESS_BOOK" "$ARBITRUM_ADDRESS_BOOK" "$ARBITRUM_DEPLOYMENT_FILE" fi -# Deploy protocol -npx hardhat migrate \ - --network "$NETWORK" \ - --skip-confirmation \ - --auto-mine \ - --graph-config "$GRAPH_CONFIG" \ - --address-book "$ADDRESS_BOOK" - -# Post deploy actions -npx hardhat migrate:ownership --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" -npx hardhat migrate:unpause --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" - -### Test -# Run tests -npx hardhat e2e --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" - -# Skip GRT scenarios in L2 as we don't have bridged GRT yet -if [[ "$NETWORK" != "localnitrol2" ]]; then - npx hardhat e2e:scenario create-subgraphs --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" - npx hardhat e2e:scenario open-allocations --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" +## TEST +# Run e2e tests +if [[ -z "$L2_NETWORK" ]]; then + test_e2e "$L1_NETWORK" "$L1_GRAPH_CONFIG" "$L2_GRAPH_CONFIG" "$ADDRESS_BOOK" true +else + test_e2e "$L1_NETWORK" "$L1_GRAPH_CONFIG" "$L2_GRAPH_CONFIG" "$ADDRESS_BOOK" + test_e2e "$L2_NETWORK" "$L1_GRAPH_CONFIG" "$L2_GRAPH_CONFIG" "$ADDRESS_BOOK" fi -# skip close-allocations for arbitrum testnodes as we can't advance epoch -if [[ "$NETWORK" != *"localnitro"* ]]; then - npx hardhat e2e:scenario close-allocations --network "$NETWORK" --graph-config "$GRAPH_CONFIG" --address-book "$ADDRESS_BOOK" +# Run scenario tests +test_e2e_scenarios "$L1_NETWORK" "$L1_GRAPH_CONFIG" "$L2_GRAPH_CONFIG" "$ADDRESS_BOOK" +if [[ -n "$L2_NETWORK" ]]; then + test_e2e_scenarios_bridge "$L1_NETWORK" "$L1_GRAPH_CONFIG" "$L2_GRAPH_CONFIG" "$ADDRESS_BOOK" + test_e2e_scenarios "$L2_NETWORK" "$L1_GRAPH_CONFIG" "$L2_GRAPH_CONFIG" "$ADDRESS_BOOK" fi -### Cleanup +## Cleanup # Exit error mode so the evm instance always gets killed -if [[ "$NETWORK" == "localhost" ]]; then +if [[ "$L1_NETWORK" == "localhost" || "$L2_NETWORK" == "localhost" ]]; then set +e result=0 diff --git a/scripts/evm b/scripts/evm index e63e2e9dc..62bab58fb 100644 --- a/scripts/evm +++ b/scripts/evm @@ -4,7 +4,7 @@ TESTRPC_PORT=${TESTRPC_PORT:-8545} MAX_RETRIES=120 evm_running() { - nc -z localhost "$TESTRPC_PORT" > /dev/null + lsof -i:$TESTRPC_PORT -t > /dev/null } evm_ping() { diff --git a/scripts/test b/scripts/test index 42dbf7366..c8cc60c4a 100755 --- a/scripts/test +++ b/scripts/test @@ -9,6 +9,14 @@ source $(pwd)/scripts/evm yarn build +### Cleanup +function cleanup() { + if [ "$RUN_EVM" = true ]; then + evm_kill + fi +} +trap cleanup EXIT + # Gas reporter needs to run in its own evm instance if [ "$RUN_EVM" = true ]; then evm_kill @@ -23,14 +31,7 @@ mkdir -p reports # Run using the standalone evm instance npx hardhat test --network hardhat $@ -### Cleanup - -# Exit error mode so the evm instance always gets killed -set +e -result=0 - -if [ "$RUN_EVM" = true ]; then - evm_kill +if [ "$REPORT_GAS" = true ]; then + cat reports/gas-report.log + echo "" # Gas report doesn't have a newline at the end fi - -exit $result diff --git a/tasks/bridge/to-l2.ts b/tasks/bridge/to-l2.ts new file mode 100644 index 000000000..ce12d1a57 --- /dev/null +++ b/tasks/bridge/to-l2.ts @@ -0,0 +1,61 @@ +import { task } from 'hardhat/config' +import { cliOpts } from '../../cli/defaults' +import { sendToL2 } from '../../cli/commands/bridge/to-l2' +import { loadEnv } from '../../cli/env' +import { TASK_NITRO_SETUP_SDK } from '../deployment/nitro' +import { BigNumber } from 'ethers' + +export const TASK_BRIDGE_TO_L2 = 'bridge:send-to-l2' + +task(TASK_BRIDGE_TO_L2, 'Bridge GRT tokens from L1 to L2') + .addParam('amount', 'Amount of tokens to bridge') + .addOptionalParam('sender', 'Address of the sender. L1 deployer if empty.') + .addOptionalParam('recipient', 'Receiving address in L2. Same to L1 address if empty.') + .addOptionalParam('addressBook', cliOpts.addressBook.description) + .addOptionalParam( + 'arbitrumAddressBook', + cliOpts.arbitrumAddressBook.description, + cliOpts.arbitrumAddressBook.default, + ) + .addOptionalParam('l1GraphConfig', cliOpts.graphConfig.description) + .addOptionalParam('l2GraphConfig', cliOpts.graphConfig.description) + .addOptionalParam( + 'deploymentFile', + 'Nitro testnode deployment file. Must specify if using nitro test nodes.', + ) + .setAction(async (taskArgs, hre) => { + console.log('> Sending GRT to L2') + const graph = hre.graph(taskArgs) + + // If local, add nitro test node networks to sdk + if (taskArgs.deploymentFile) { + console.log('> Adding nitro test node network to sdk') + await hre.run(TASK_NITRO_SETUP_SDK, { deploymentFile: taskArgs.deploymentFile }) + } + + // Get the sender, use L1 deployer if not provided + const l1Deployer = await graph.l1.getDeployer() + const sender: string = taskArgs.sender ?? l1Deployer.address + + let wallet = await graph.l1.getWallet(sender) + + if (!wallet) { + throw new Error(`No wallet found for address ${sender}`) + } else { + console.log(`> Using wallet ${wallet.address}`) + wallet = wallet.connect(graph.l1.provider) + } + + // Patch sendToL2 opts + taskArgs.l2Provider = graph.l2.provider + taskArgs.amount = hre.ethers.utils.formatEther(taskArgs.amount) // sendToL2 expects amount in GRT + + // L2 provider gas limit estimation has been hit or miss in CI, 400k should be more than enough + if (process.env.CI) { + taskArgs.maxGas = BigNumber.from('400000') + } + + await sendToL2(await loadEnv(taskArgs, wallet), taskArgs) + + console.log('Done!') + }) diff --git a/tasks/deployment/accounts.ts b/tasks/deployment/accounts.ts index c09ff2a2f..747ae22eb 100644 --- a/tasks/deployment/accounts.ts +++ b/tasks/deployment/accounts.ts @@ -2,9 +2,9 @@ import { task } from 'hardhat/config' import { cliOpts } from '../../cli/defaults' import { updateItemValue, writeConfig } from '../../cli/config' -import { BigNumber, ContractTransaction } from 'ethers' task('migrate:accounts', 'Creates protocol accounts and saves them in graph config') + .addFlag('disableSecureAccounts', 'Disable secure accounts on GRE') .addOptionalParam('graphConfig', cliOpts.graphConfig.description) .setAction(async (taskArgs, hre) => { const { graphConfig, getDeployer } = hre.graph(taskArgs) @@ -39,69 +39,3 @@ task('migrate:accounts', 'Creates protocol accounts and saves them in graph conf writeConfig(taskArgs.graphConfig, graphConfig.toString()) }) - -task('migrate:accounts:nitro', 'Funds protocol accounts on Arbitrum Nitro testnodes') - .addOptionalParam('graphConfig', cliOpts.graphConfig.description) - .addOptionalParam('privateKey', 'The private key for Arbitrum testnode genesis account') - .addOptionalParam('amount', 'The amount to fund each account with') - .setAction(async (taskArgs, hre) => { - // Arbitrum Nitro testnodes have a pre-funded genesis account whose private key is hardcoded here: - // - L1 > https://github.com/OffchainLabs/nitro/blob/01c558c06ad9cbaa083bebe3e51960e195c3fd6b/test-node.bash#L136 - // - L2 > https://github.com/OffchainLabs/nitro/blob/01c558c06ad9cbaa083bebe3e51960e195c3fd6b/testnode-scripts/config.ts#L22 - const genesisAccountPrivateKey = - taskArgs.privateKey ?? 'e887f7d17d07cc7b8004053fb8826f6657084e88904bb61590e498ca04704cf2' - const genesisAccount = new hre.ethers.Wallet(genesisAccountPrivateKey) - - // Get protocol accounts - const { getDeployer, getNamedAccounts, getTestAccounts, provider } = hre.graph(taskArgs) - const deployer = await getDeployer() - const testAccounts = await getTestAccounts() - const namedAccounts = await getNamedAccounts() - const accounts = [ - deployer, - ...testAccounts, - ...Object.keys(namedAccounts).map((k) => namedAccounts[k]), - ] - - // Amount to fund - // - If amount is specified, use that - // - Otherwise, use 95% of genesis account balance with a maximum of 100 Eth - let amount: BigNumber - const maxAmount = hre.ethers.utils.parseEther('100') - const genesisAccountBalance = await provider.getBalance(genesisAccount.address) - - if (taskArgs.amount) { - amount = hre.ethers.BigNumber.from(taskArgs.amount) - } else { - const splitGenesisBalance = genesisAccountBalance.mul(95).div(100).div(accounts.length) - if (splitGenesisBalance.gt(maxAmount)) { - amount = maxAmount - } else { - amount = splitGenesisBalance - } - } - - // Check genesis account balance - const requiredFunds = amount.mul(accounts.length) - if (genesisAccountBalance.lt(requiredFunds)) { - throw new Error('Insufficient funds in genesis account') - } - - // Fund accounts - console.log('> Funding protocol addresses') - console.log(`Genesis account: ${genesisAccount.address}`) - console.log(`Total accounts: ${accounts.length}`) - console.log(`Amount per account: ${hre.ethers.utils.formatEther(amount)}`) - console.log(`Required funds: ${hre.ethers.utils.formatEther(requiredFunds)}`) - - const txs: ContractTransaction[] = [] - for (const account of accounts) { - const tx = await genesisAccount.connect(provider).sendTransaction({ - value: amount, - to: account.address, - }) - txs.push(tx) - } - await Promise.all(txs.map((tx) => tx.wait())) - console.log('Done!') - }) diff --git a/tasks/deployment/nitro.ts b/tasks/deployment/nitro.ts new file mode 100644 index 000000000..8cd3feb0c --- /dev/null +++ b/tasks/deployment/nitro.ts @@ -0,0 +1,141 @@ +import { BigNumber, ContractTransaction } from 'ethers' +import { subtask, task } from 'hardhat/config' +import { cliOpts } from '../../cli/defaults' +import { addCustomNetwork } from '@arbitrum/sdk/dist/lib/dataEntities/networks' +import fs from 'fs' +import { execSync } from 'child_process' + +export const TASK_NITRO_FUND_ACCOUNTS = 'nitro:fund-accounts' +export const TASK_NITRO_SETUP_SDK = 'nitro:sdk-setup' +export const TASK_NITRO_SETUP_ADDRESS_BOOK = 'nitro:address-book-setup' +export const TASK_NITRO_FETCH_DEPLOYMENT_FILE = 'nitro:fetch-deployment-file' + +task(TASK_NITRO_FUND_ACCOUNTS, 'Funds protocol accounts on Arbitrum Nitro testnodes') + .addFlag('disableSecureAccounts', 'Disable secure accounts on GRE') + .addOptionalParam('graphConfig', cliOpts.graphConfig.description) + .addOptionalParam('privateKey', 'The private key for Arbitrum testnode genesis account') + .addOptionalParam('amount', 'The amount to fund each account with') + .setAction(async (taskArgs, hre) => { + // Arbitrum Nitro testnodes have a pre-funded genesis account whose private key is hardcoded here: + // - L1 > https://github.com/OffchainLabs/nitro/blob/01c558c06ad9cbaa083bebe3e51960e195c3fd6b/test-node.bash#L136 + // - L2 > https://github.com/OffchainLabs/nitro/blob/01c558c06ad9cbaa083bebe3e51960e195c3fd6b/testnode-scripts/config.ts#L22 + const genesisAccountPrivateKey = + taskArgs.privateKey ?? 'e887f7d17d07cc7b8004053fb8826f6657084e88904bb61590e498ca04704cf2' + const genesisAccount = new hre.ethers.Wallet(genesisAccountPrivateKey) + + // Get protocol accounts + const { getDeployer, getNamedAccounts, getTestAccounts, provider } = hre.graph(taskArgs) + const deployer = await getDeployer() + const testAccounts = await getTestAccounts() + const namedAccounts = await getNamedAccounts() + const accounts = [ + deployer, + ...testAccounts, + ...Object.keys(namedAccounts).map((k) => namedAccounts[k]), + ] + + // Amount to fund + // - If amount is specified, use that + // - Otherwise, use 95% of genesis account balance with a maximum of 100 Eth + let amount: BigNumber + const maxAmount = hre.ethers.utils.parseEther('100') + const genesisAccountBalance = await provider.getBalance(genesisAccount.address) + + if (taskArgs.amount) { + amount = hre.ethers.BigNumber.from(taskArgs.amount) + } else { + const splitGenesisBalance = genesisAccountBalance.mul(95).div(100).div(accounts.length) + if (splitGenesisBalance.gt(maxAmount)) { + amount = maxAmount + } else { + amount = splitGenesisBalance + } + } + + // Check genesis account balance + const requiredFunds = amount.mul(accounts.length) + if (genesisAccountBalance.lt(requiredFunds)) { + throw new Error('Insufficient funds in genesis account') + } + + // Fund accounts + console.log('> Funding protocol addresses') + console.log(`Genesis account: ${genesisAccount.address}`) + console.log(`Total accounts: ${accounts.length}`) + console.log(`Amount per account: ${hre.ethers.utils.formatEther(amount)}`) + console.log(`Required funds: ${hre.ethers.utils.formatEther(requiredFunds)}`) + + const txs: ContractTransaction[] = [] + for (const account of accounts) { + const tx = await genesisAccount.connect(provider).sendTransaction({ + value: amount, + to: account.address, + }) + txs.push(tx) + } + await Promise.all(txs.map((tx) => tx.wait())) + console.log('Done!') + }) + +// Arbitrum SDK does not support Nitro testnodes out of the box +// This adds the testnodes to the SDK configuration +subtask(TASK_NITRO_SETUP_SDK, 'Adds nitro testnodes to SDK config') + .addParam('deploymentFile', 'The testnode deployment file to use', 'localNetwork.json') + .setAction(async (taskArgs) => { + if (!fs.existsSync(taskArgs.deploymentFile)) { + throw new Error(`Deployment file not found: ${taskArgs.deploymentFile}`) + } + const deployment = JSON.parse(fs.readFileSync(taskArgs.deploymentFile, 'utf-8')) + addCustomNetwork({ + customL1Network: deployment.l1Network, + customL2Network: deployment.l2Network, + }) + }) + +subtask(TASK_NITRO_FETCH_DEPLOYMENT_FILE, 'Fetches nitro deployment file from a local testnode') + .addParam( + 'deploymentFile', + 'Path to the file where to deployment file will be saved', + 'localNetwork.json', + ) + .setAction(async (taskArgs) => { + console.log(`Attempting to fetch deployment file from testnode...`) + + const command = `docker exec $(docker ps -qf "name=sequencer") cat /workspace/localNetwork.json > ${taskArgs.deploymentFile}` + const stdOut = execSync(command) + console.log(stdOut.toString()) + + if (!fs.existsSync(taskArgs.deploymentFile)) { + throw new Error(`Unable to fetch deployment file: ${taskArgs.deploymentFile}`) + } + console.log(`Deployment file saved to ${taskArgs.deploymentFile}`) + }) + +// Read arbitrum contract addresses from deployment file and write them to the address book +task(TASK_NITRO_SETUP_ADDRESS_BOOK, 'Write arbitrum addresses to address book') + .addParam('deploymentFile', 'The testnode deployment file to use') + .addParam('arbitrumAddressBook', 'Arbitrum address book file') + .setAction(async (taskArgs, hre) => { + if (!fs.existsSync(taskArgs.deploymentFile)) { + await hre.run(TASK_NITRO_FETCH_DEPLOYMENT_FILE, taskArgs) + } + const deployment = JSON.parse(fs.readFileSync(taskArgs.deploymentFile, 'utf-8')) + + const addressBook = { + '1337': { + L1GatewayRouter: { + address: deployment.l2Network.tokenBridge.l1GatewayRouter, + }, + IInbox: { + address: deployment.l2Network.ethBridge.inbox, + }, + }, + '412346': { + L2GatewayRouter: { + address: deployment.l2Network.tokenBridge.l2GatewayRouter, + }, + }, + } + + fs.writeFileSync(taskArgs.arbitrumAddressBook, JSON.stringify(addressBook)) + }) diff --git a/tasks/deployment/ownership.ts b/tasks/deployment/ownership.ts index 86591c974..9f8e318fc 100644 --- a/tasks/deployment/ownership.ts +++ b/tasks/deployment/ownership.ts @@ -1,8 +1,10 @@ -import { ContractTransaction } from 'ethers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { Contract, ContractTransaction, ethers } from 'ethers' import { task } from 'hardhat/config' import { cliOpts } from '../../cli/defaults' task('migrate:ownership', 'Accepts ownership of protocol contracts on behalf of governor') + .addFlag('disableSecureAccounts', 'Disable secure accounts on GRE') .addOptionalParam('addressBook', cliOpts.addressBook.description) .addOptionalParam('graphConfig', cliOpts.graphConfig.description) .setAction(async (taskArgs, hre) => { @@ -13,12 +15,36 @@ task('migrate:ownership', 'Accepts ownership of protocol contracts on behalf of console.log('> Accepting ownership of contracts') console.log(`- Governor: ${governor.address}`) + const governedContracts = [GraphToken, Controller, GraphProxyAdmin, SubgraphNFT] const txs: ContractTransaction[] = [] - txs.push(await GraphToken.connect(governor).acceptOwnership()) - txs.push(await Controller.connect(governor).acceptOwnership()) - txs.push(await GraphProxyAdmin.connect(governor).acceptOwnership()) - txs.push(await SubgraphNFT.connect(governor).acceptOwnership()) + for (const contract of governedContracts) { + const tx = await acceptOwnershipIfPending(contract, governor) + if (tx) { + txs.push() + } + } await Promise.all(txs.map((tx) => tx.wait())) console.log('Done!') }) + +async function acceptOwnershipIfPending( + contract: Contract, + signer: SignerWithAddress, +): Promise { + const pendingGovernor = await contract.connect(signer).pendingGovernor() + + if (pendingGovernor === ethers.constants.AddressZero) { + console.log(`No pending governor for ${contract.address}`) + return + } + + if (pendingGovernor === signer.address) { + console.log(`Accepting ownership of ${contract.address}`) + return contract.connect(signer).acceptOwnership() + } else { + console.log( + `Signer ${signer.address} is not the pending governor of ${contract.address}, it is ${pendingGovernor}`, + ) + } +} diff --git a/tasks/deployment/unpause.ts b/tasks/deployment/unpause.ts index fc10c766f..b7bc29b5d 100644 --- a/tasks/deployment/unpause.ts +++ b/tasks/deployment/unpause.ts @@ -1,25 +1,38 @@ import { task } from 'hardhat/config' import { cliOpts } from '../../cli/defaults' -import GraphChain from '../../gre/helpers/network' +import GraphChain from '../../gre/helpers/chain' -task('migrate:unpause', 'Unpause protocol and bridge') +task('migrate:unpause:protocol', 'Unpause protocol (except bridge)') + .addFlag('disableSecureAccounts', 'Disable secure accounts on GRE') .addOptionalParam('addressBook', cliOpts.addressBook.description) .addOptionalParam('graphConfig', cliOpts.graphConfig.description) .setAction(async (taskArgs, hre) => { const graph = hre.graph(taskArgs) const { governor } = await graph.getNamedAccounts() - const { Controller, L1GraphTokenGateway, L2GraphTokenGateway } = graph.contracts + const { Controller } = graph.contracts console.log('> Unpausing protocol') const tx = await Controller.connect(governor).setPaused(false) await tx.wait() + console.log('Done!') + }) + +task('migrate:unpause:bridge', 'Unpause bridge') + .addFlag('disableSecureAccounts', 'Disable secure accounts on GRE') + .addOptionalParam('addressBook', cliOpts.addressBook.description) + .addOptionalParam('graphConfig', cliOpts.graphConfig.description) + .setAction(async (taskArgs, hre) => { + const graph = hre.graph(taskArgs) + const { governor } = await graph.getNamedAccounts() + const { L1GraphTokenGateway, L2GraphTokenGateway } = graph.contracts + console.log('> Unpausing bridge') const GraphTokenGateway = GraphChain.isL2(graph.chainId) ? L2GraphTokenGateway : L1GraphTokenGateway - const tx2 = await GraphTokenGateway.connect(governor).setPaused(false) - await tx2.wait() + const tx = await GraphTokenGateway.connect(governor).setPaused(false) + await tx.wait() console.log('Done!') }) diff --git a/tasks/e2e/e2e.ts b/tasks/e2e/e2e.ts index d2ab8f96e..aa712380c 100644 --- a/tasks/e2e/e2e.ts +++ b/tasks/e2e/e2e.ts @@ -4,7 +4,7 @@ import { TASK_TEST } from 'hardhat/builtin-tasks/task-names' import glob from 'glob' import { cliOpts } from '../../cli/defaults' import fs from 'fs' -import { isL1 } from '../../gre/helpers/network' +import { isL1 } from '../../gre/helpers/chain' import { runScriptWithHardhat } from 'hardhat/internal/util/scripts-runner' const CONFIG_TESTS = 'e2e/deployment/config/**/*.test.ts' @@ -13,7 +13,13 @@ const INIT_TESTS = 'e2e/deployment/init/**/*.test.ts' // Built-in test & run tasks don't support GRE arguments // so we pass them by overriding GRE config object const setGraphConfig = async (args: TaskArguments, hre: HardhatRuntimeEnvironment) => { - const greArgs = ['graphConfig', 'l1GraphConfig', 'l2GraphConfig', 'addressBook'] + const greArgs = [ + 'graphConfig', + 'l1GraphConfig', + 'l2GraphConfig', + 'addressBook', + 'disableSecureAccounts', + ] for (const arg of greArgs) { if (args[arg]) { @@ -29,13 +35,23 @@ const setGraphConfig = async (args: TaskArguments, hre: HardhatRuntimeEnvironmen task('e2e', 'Run all e2e tests') .addOptionalParam('graphConfig', cliOpts.graphConfig.description) + .addOptionalParam('l1GraphConfig', cliOpts.graphConfig.description) + .addOptionalParam('l2GraphConfig', cliOpts.graphConfig.description) .addOptionalParam('addressBook', cliOpts.addressBook.description) + .addFlag('skipBridge', 'Skip bridge tests') .setAction(async (args, hre: HardhatRuntimeEnvironment) => { - const testFiles = [ + let testFiles = [ ...new glob.GlobSync(CONFIG_TESTS).found, ...new glob.GlobSync(INIT_TESTS).found, ] + if (args.skipBridge) { + testFiles = testFiles.filter((file) => !['l1', 'l2'].includes(file.split('/')[3])) + } + + // Disable secure accounts, we don't need them for this task + hre.config.graph.disableSecureAccounts = true + setGraphConfig(args, hre) await hre.run(TASK_TEST, { testFiles: testFiles, @@ -44,9 +60,15 @@ task('e2e', 'Run all e2e tests') task('e2e:config', 'Run deployment configuration e2e tests') .addOptionalParam('graphConfig', cliOpts.graphConfig.description) + .addOptionalParam('l1GraphConfig', cliOpts.graphConfig.description) + .addOptionalParam('l2GraphConfig', cliOpts.graphConfig.description) .addOptionalParam('addressBook', cliOpts.addressBook.description) .setAction(async (args, hre: HardhatRuntimeEnvironment) => { const files = new glob.GlobSync(CONFIG_TESTS).found + + // Disable secure accounts, we don't need them for this task + hre.config.graph.disableSecureAccounts = true + setGraphConfig(args, hre) await hre.run(TASK_TEST, { testFiles: files, @@ -55,9 +77,15 @@ task('e2e:config', 'Run deployment configuration e2e tests') task('e2e:init', 'Run deployment initialization e2e tests') .addOptionalParam('graphConfig', cliOpts.graphConfig.description) + .addOptionalParam('l1GraphConfig', cliOpts.graphConfig.description) + .addOptionalParam('l2GraphConfig', cliOpts.graphConfig.description) .addOptionalParam('addressBook', cliOpts.addressBook.description) .setAction(async (args, hre: HardhatRuntimeEnvironment) => { const files = new glob.GlobSync(INIT_TESTS).found + + // Disable secure accounts, we don't need them for this task + hre.config.graph.disableSecureAccounts = true + setGraphConfig(args, hre) await hre.run(TASK_TEST, { testFiles: files, @@ -66,8 +94,11 @@ task('e2e:init', 'Run deployment initialization e2e tests') task('e2e:scenario', 'Run scenario scripts and e2e tests') .addPositionalParam('scenario', 'Name of the scenario to run') - .addOptionalParam('graphConfig', cliOpts.graphConfig.description) + .addFlag('disableSecureAccounts', 'Disable secure accounts on GRE') .addOptionalParam('addressBook', cliOpts.addressBook.description) + .addOptionalParam('graphConfig', cliOpts.graphConfig.description) + .addOptionalParam('l1GraphConfig', cliOpts.graphConfig.description) + .addOptionalParam('l2GraphConfig', cliOpts.graphConfig.description) .addFlag('skipScript', "Don't run scenario script") .setAction(async (args, hre: HardhatRuntimeEnvironment) => { setGraphConfig(args, hre) @@ -82,8 +113,11 @@ task('e2e:scenario', 'Run scenario scripts and e2e tests') if (!args.skipScript) { if (fs.existsSync(script)) { await runScriptWithHardhat(hre.hardhatArguments, script, [ - args.graphConfig, args.addressBook, + args.graphConfig, + args.l1GraphConfig, + args.l2GraphConfig, + args.disableSecureAccounts, ]) } else { console.log(`No script found for scenario ${args.scenario}`) diff --git a/tasks/misc/accounts.ts b/tasks/misc/accounts.ts deleted file mode 100644 index 5bb6512df..000000000 --- a/tasks/misc/accounts.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { task } from 'hardhat/config' -import { HardhatRuntimeEnvironment } from 'hardhat/types' -import '@nomiclabs/hardhat-ethers' - -task('accounts', 'Prints the list of accounts', async (_, hre: HardhatRuntimeEnvironment) => { - const accounts = await hre.ethers.getSigners() - for (const account of accounts) { - console.log(await account.getAddress()) - } -}) diff --git a/test/curation/configuration.test.ts b/test/curation/configuration.test.ts index b2424c784..f475d8f30 100644 --- a/test/curation/configuration.test.ts +++ b/test/curation/configuration.test.ts @@ -55,7 +55,7 @@ describe('Curation:Config', () => { it('reject set `defaultReserveRatio` if not allowed', async function () { const tx = curation.connect(me.signer).setDefaultReserveRatio(defaults.curation.reserveRatio) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -79,7 +79,7 @@ describe('Curation:Config', () => { const tx = curation .connect(me.signer) .setMinimumCurationDeposit(defaults.curation.minimumCurationDeposit) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -99,7 +99,7 @@ describe('Curation:Config', () => { it('reject set `curationTaxPercentage` if not allowed', async function () { const tx = curation.connect(me.signer).setCurationTaxPercentage(0) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -124,7 +124,7 @@ describe('Curation:Config', () => { it('reject set `curationTokenMaster` if not allowed', async function () { const newCurationTokenMaster = curation.address const tx = curation.connect(me.signer).setCurationTokenMaster(newCurationTokenMaster) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) }) diff --git a/test/disputes/configuration.test.ts b/test/disputes/configuration.test.ts index 28bc92ed5..c663c31d7 100644 --- a/test/disputes/configuration.test.ts +++ b/test/disputes/configuration.test.ts @@ -49,7 +49,7 @@ describe('DisputeManager:Config', () => { it('reject set `arbitrator` if not allowed', async function () { const tx = disputeManager.connect(me.signer).setArbitrator(arbitrator.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('reject set `arbitrator` to address zero', async function () { @@ -74,7 +74,7 @@ describe('DisputeManager:Config', () => { it('reject set `minimumDeposit` if not allowed', async function () { const newValue = toBN('1') const tx = disputeManager.connect(me.signer).setMinimumDeposit(newValue) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -97,7 +97,7 @@ describe('DisputeManager:Config', () => { it('reject set `fishermanRewardPercentage` if not allowed', async function () { const tx = disputeManager.connect(me.signer).setFishermanRewardPercentage(50) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -127,7 +127,7 @@ describe('DisputeManager:Config', () => { it('reject set `slashingPercentage` if not allowed', async function () { const tx = disputeManager.connect(me.signer).setSlashingPercentage(50, 50) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) }) diff --git a/test/gateway/bridgeEscrow.test.ts b/test/gateway/bridgeEscrow.test.ts index aea195159..fabe9de27 100644 --- a/test/gateway/bridgeEscrow.test.ts +++ b/test/gateway/bridgeEscrow.test.ts @@ -41,17 +41,17 @@ describe('BridgeEscrow', () => { describe('approveAll', function () { it('cannot be called by someone other than the governor', async function () { const tx = bridgeEscrow.connect(tokenReceiver.signer).approveAll(spender.address) - expect(tx).to.be.revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('allows a spender to transfer GRT held by the contract', async function () { expect(await grt.allowance(bridgeEscrow.address, spender.address)).eq(0) const tx = grt .connect(spender.signer) .transferFrom(bridgeEscrow.address, tokenReceiver.address, nTokens) - expect(tx).to.be.revertedWith('ERC20: transfer amount exceeds allowance') + await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') await bridgeEscrow.connect(governor.signer).approveAll(spender.address) - expect( - await grt + await expect( + grt .connect(spender.signer) .transferFrom(bridgeEscrow.address, tokenReceiver.address, nTokens), ).to.emit(grt, 'Transfer') @@ -62,7 +62,7 @@ describe('BridgeEscrow', () => { describe('revokeAll', function () { it('cannot be called by someone other than the governor', async function () { const tx = bridgeEscrow.connect(tokenReceiver.signer).revokeAll(spender.address) - expect(tx).to.be.revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it("revokes a spender's permission to transfer GRT held by the contract", async function () { await bridgeEscrow.connect(governor.signer).approveAll(spender.address) @@ -71,7 +71,7 @@ describe('BridgeEscrow', () => { const tx = grt .connect(spender.signer) .transferFrom(bridgeEscrow.address, tokenReceiver.address, BigNumber.from('1')) - expect(tx).to.be.revertedWith('ERC20: transfer amount exceeds allowance') + await expect(tx).revertedWith('ERC20: transfer amount exceeds allowance') }) }) }) diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts index 1578d83b7..1959cba2f 100644 --- a/test/gateway/l1GraphTokenGateway.test.ts +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -17,6 +17,7 @@ import { Account, applyL1ToL2Alias, advanceBlocks, + provider, } from '../lib/testHelpers' import { BridgeEscrow } from '../../build/types/BridgeEscrow' @@ -52,6 +53,10 @@ describe('L1GraphTokenGateway', () => { ['uint256', 'bytes'], [maxSubmissionCost, emptyCallHookData], ) + const defaultDataNoSubmissionCost = utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [toBN(0), emptyCallHookData], + ) const notEmptyCallHookData = '0x12' const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( ['uint256', 'bytes'], @@ -62,6 +67,8 @@ describe('L1GraphTokenGateway', () => { ;[governor, tokenSender, l2Receiver, mockRouter, mockL2GRT, mockL2Gateway, pauseGuardian] = await getAccounts() + // Dummy code on the mock router so that it appears as a contract + await provider().send('hardhat_setCode', [mockRouter.address, '0x1234']) fixture = new NetworkFixture() fixtureContracts = await fixture.load(governor.signer) ;({ grt, l1GraphTokenGateway, bridgeEscrow } = fixtureContracts) @@ -127,7 +134,17 @@ describe('L1GraphTokenGateway', () => { const tx = l1GraphTokenGateway .connect(tokenSender.signer) .setArbitrumAddresses(inboxMock.address, mockRouter.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') + }) + it('rejects setting an EOA as router or inbox', async function () { + let tx = l1GraphTokenGateway + .connect(governor.signer) + .setArbitrumAddresses(tokenSender.address, mockRouter.address) + await expect(tx).revertedWith('INBOX_MUST_BE_CONTRACT') + tx = l1GraphTokenGateway + .connect(governor.signer) + .setArbitrumAddresses(inboxMock.address, tokenSender.address) + await expect(tx).revertedWith('ROUTER_MUST_BE_CONTRACT') }) it('sets inbox and router address', async function () { const tx = l1GraphTokenGateway @@ -146,7 +163,7 @@ describe('L1GraphTokenGateway', () => { const tx = l1GraphTokenGateway .connect(tokenSender.signer) .setL2TokenAddress(mockL2GRT.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('sets l2GRT', async function () { const tx = l1GraphTokenGateway.connect(governor.signer).setL2TokenAddress(mockL2GRT.address) @@ -160,9 +177,9 @@ describe('L1GraphTokenGateway', () => { const tx = l1GraphTokenGateway .connect(tokenSender.signer) .setL2CounterpartAddress(mockL2Gateway.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) - it('sets L2Counterpart', async function () { + it('sets l2Counterpart which can be queried with counterpartGateway()', async function () { const tx = l1GraphTokenGateway .connect(governor.signer) .setL2CounterpartAddress(mockL2Gateway.address) @@ -170,6 +187,7 @@ describe('L1GraphTokenGateway', () => { .emit(l1GraphTokenGateway, 'L2CounterpartAddressSet') .withArgs(mockL2Gateway.address) expect(await l1GraphTokenGateway.l2Counterpart()).eq(mockL2Gateway.address) + expect(await l1GraphTokenGateway.counterpartGateway()).eq(mockL2Gateway.address) }) }) describe('setEscrowAddress', function () { @@ -177,7 +195,7 @@ describe('L1GraphTokenGateway', () => { const tx = l1GraphTokenGateway .connect(tokenSender.signer) .setEscrowAddress(bridgeEscrow.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('sets escrow', async function () { const tx = l1GraphTokenGateway @@ -189,69 +207,105 @@ describe('L1GraphTokenGateway', () => { expect(await l1GraphTokenGateway.escrow()).eq(bridgeEscrow.address) }) }) - describe('addToCallhookWhitelist', function () { + describe('addToCallhookAllowlist', function () { it('is not callable by addreses that are not the governor', async function () { const tx = l1GraphTokenGateway .connect(tokenSender.signer) - .addToCallhookWhitelist(tokenSender.address) - await expect(tx).revertedWith('Caller must be Controller governor') - expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false) + .addToCallhookAllowlist(fixtureContracts.rewardsManager.address) + await expect(tx).revertedWith('Only Controller governor') + expect( + await l1GraphTokenGateway.callhookAllowlist(fixtureContracts.rewardsManager.address), + ).eq(false) + }) + it('rejects adding an EOA to the callhook allowlist', async function () { + const tx = l1GraphTokenGateway + .connect(governor.signer) + .addToCallhookAllowlist(tokenSender.address) + await expect(tx).revertedWith('MUST_BE_CONTRACT') }) - it('adds an address to the callhook whitelist', async function () { + it('adds an address to the callhook allowlist', async function () { const tx = l1GraphTokenGateway .connect(governor.signer) - .addToCallhookWhitelist(tokenSender.address) + .addToCallhookAllowlist(fixtureContracts.rewardsManager.address) await expect(tx) - .emit(l1GraphTokenGateway, 'AddedToCallhookWhitelist') - .withArgs(tokenSender.address) - expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true) + .emit(l1GraphTokenGateway, 'AddedToCallhookAllowlist') + .withArgs(fixtureContracts.rewardsManager.address) + expect( + await l1GraphTokenGateway.callhookAllowlist(fixtureContracts.rewardsManager.address), + ).eq(true) }) }) - describe('removeFromCallhookWhitelist', function () { + describe('removeFromCallhookAllowlist', function () { it('is not callable by addreses that are not the governor', async function () { await l1GraphTokenGateway .connect(governor.signer) - .addToCallhookWhitelist(tokenSender.address) + .addToCallhookAllowlist(fixtureContracts.rewardsManager.address) const tx = l1GraphTokenGateway .connect(tokenSender.signer) - .removeFromCallhookWhitelist(tokenSender.address) - await expect(tx).revertedWith('Caller must be Controller governor') - expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(true) + .removeFromCallhookAllowlist(fixtureContracts.rewardsManager.address) + await expect(tx).revertedWith('Only Controller governor') + expect( + await l1GraphTokenGateway.callhookAllowlist(fixtureContracts.rewardsManager.address), + ).eq(true) }) - it('removes an address from the callhook whitelist', async function () { + it('removes an address from the callhook allowlist', async function () { await l1GraphTokenGateway .connect(governor.signer) - .addToCallhookWhitelist(tokenSender.address) + .addToCallhookAllowlist(fixtureContracts.rewardsManager.address) const tx = l1GraphTokenGateway .connect(governor.signer) - .removeFromCallhookWhitelist(tokenSender.address) + .removeFromCallhookAllowlist(fixtureContracts.rewardsManager.address) await expect(tx) - .emit(l1GraphTokenGateway, 'RemovedFromCallhookWhitelist') - .withArgs(tokenSender.address) - expect(await l1GraphTokenGateway.callhookWhitelist(tokenSender.address)).eq(false) + .emit(l1GraphTokenGateway, 'RemovedFromCallhookAllowlist') + .withArgs(fixtureContracts.rewardsManager.address) + expect( + await l1GraphTokenGateway.callhookAllowlist(fixtureContracts.rewardsManager.address), + ).eq(false) }) }) describe('Pausable behavior', () => { it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { let tx = l1GraphTokenGateway.connect(tokenSender.signer).setPaused(false) - await expect(tx).revertedWith('Only Governor or Guardian can call') + await expect(tx).revertedWith('Only Governor or Guardian') tx = l1GraphTokenGateway.connect(tokenSender.signer).setPaused(true) - await expect(tx).revertedWith('Only Governor or Guardian can call') + await expect(tx).revertedWith('Only Governor or Guardian') }) - it('can be paused and unpaused by the governor', async function () { + it('cannot be unpaused if some state variables are not set', async function () { let tx = l1GraphTokenGateway.connect(governor.signer).setPaused(false) - await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(false) - expect(await l1GraphTokenGateway.paused()).eq(false) - tx = l1GraphTokenGateway.connect(governor.signer).setPaused(true) + await expect(tx).revertedWith('INBOX_NOT_SET') + await l1GraphTokenGateway + .connect(governor.signer) + .setArbitrumAddresses(arbitrumMocks.inboxMock.address, mockRouter.address) + tx = l1GraphTokenGateway.connect(governor.signer).setPaused(false) + await expect(tx).revertedWith('L2_COUNTERPART_NOT_SET') + await l1GraphTokenGateway + .connect(governor.signer) + .setL2CounterpartAddress(mockL2Gateway.address) + tx = l1GraphTokenGateway.connect(governor.signer).setPaused(false) + await expect(tx).revertedWith('ESCROW_NOT_SET') + }) + it('can be paused and unpaused by the governor', async function () { + await fixture.configureL1Bridge( + governor.signer, + arbitrumMocks, + fixtureContracts, + mockRouter.address, + mockL2GRT.address, + mockL2Gateway.address, + ) + let tx = l1GraphTokenGateway.connect(governor.signer).setPaused(true) await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(true) - expect(await l1GraphTokenGateway.paused()).eq(true) + await expect(await l1GraphTokenGateway.paused()).eq(true) + tx = l1GraphTokenGateway.connect(governor.signer).setPaused(false) + await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l1GraphTokenGateway.paused()).eq(false) }) describe('setPauseGuardian', function () { it('cannot be called by someone other than governor', async function () { const tx = l1GraphTokenGateway .connect(tokenSender.signer) .setPauseGuardian(pauseGuardian.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('sets a new pause guardian', async function () { const tx = l1GraphTokenGateway @@ -262,13 +316,21 @@ describe('L1GraphTokenGateway', () => { .withArgs(AddressZero, pauseGuardian.address) }) it('allows a pause guardian to pause and unpause', async function () { + await fixture.configureL1Bridge( + governor.signer, + arbitrumMocks, + fixtureContracts, + mockRouter.address, + mockL2GRT.address, + mockL2Gateway.address, + ) await l1GraphTokenGateway.connect(governor.signer).setPauseGuardian(pauseGuardian.address) - let tx = l1GraphTokenGateway.connect(pauseGuardian.signer).setPaused(false) - await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(false) - expect(await l1GraphTokenGateway.paused()).eq(false) - tx = l1GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) + let tx = l1GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(true) - expect(await l1GraphTokenGateway.paused()).eq(true) + await expect(await l1GraphTokenGateway.paused()).eq(true) + tx = l1GraphTokenGateway.connect(pauseGuardian.signer).setPaused(false) + await expect(tx).emit(l1GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l1GraphTokenGateway.paused()).eq(false) }) }) }) @@ -279,13 +341,7 @@ describe('L1GraphTokenGateway', () => { const selector = l1GraphTokenGateway.interface.getSighash('finalizeInboundTransfer') const params = utils.defaultAbiCoder.encode( ['address', 'address', 'address', 'uint256', 'bytes'], - [ - grt.address, - tokenSender.address, - l2Receiver.address, - toGRT('10'), - utils.defaultAbiCoder.encode(['bytes', 'bytes'], [emptyCallHookData, callHookData]), - ], + [grt.address, tokenSender.address, l2Receiver.address, toGRT('10'), callHookData], ) const outboundData = utils.hexlify(utils.concat([selector, params])) @@ -380,7 +436,7 @@ describe('L1GraphTokenGateway', () => { const tx = l1GraphTokenGateway .connect(pauseGuardian.address) .updateL2MintAllowance(toGRT('1'), await latestBlock()) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('does not allow using a future or current block number', async function () { const issuancePerBlock = toGRT('120') @@ -477,7 +533,7 @@ describe('L1GraphTokenGateway', () => { const tx = l1GraphTokenGateway .connect(pauseGuardian.address) .setL2MintAllowanceParametersManual(toGRT('0'), toGRT('1'), await latestBlock()) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('does not allow using a future or current block number', async function () { const issuancePerBlock = toGRT('120') @@ -592,7 +648,7 @@ describe('L1GraphTokenGateway', () => { ) await testValidOutboundTransfer(mockRouter.signer, routerEncodedData, emptyCallHookData) }) - it('reverts when called with the wrong value', async function () { + it('reverts when called with no submission cost', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) const tx = l1GraphTokenGateway .connect(tokenSender.signer) @@ -602,14 +658,14 @@ describe('L1GraphTokenGateway', () => { toGRT('10'), maxGas, gasPriceBid, - defaultData, + defaultDataNoSubmissionCost, { - value: defaultEthValue.sub(1), + value: defaultEthValue, }, ) - await expect(tx).revertedWith('WRONG_ETH_VALUE') + await expect(tx).revertedWith('NO_SUBMISSION_COST') }) - it('reverts when called with nonempty calldata, if the sender is not whitelisted', async function () { + it('reverts when called with nonempty calldata, if the sender is not allowlisted', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) const tx = l1GraphTokenGateway .connect(tokenSender.signer) @@ -626,10 +682,12 @@ describe('L1GraphTokenGateway', () => { ) await expect(tx).revertedWith('CALL_HOOK_DATA_NOT_ALLOWED') }) - it('allows sending nonempty calldata, if the sender is whitelisted', async function () { + it('allows sending nonempty calldata, if the sender is allowlisted', async function () { + // Make the sender a contract so that it can be allowed to send callhooks + await provider().send('hardhat_setCode', [tokenSender.address, '0x1234']) await l1GraphTokenGateway .connect(governor.signer) - .addToCallhookWhitelist(tokenSender.address) + .addToCallhookAllowlist(tokenSender.address) await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) await testValidOutboundTransfer( tokenSender.signer, diff --git a/test/gns.test.ts b/test/gns.test.ts index 298d95ab0..1ee46002a 100644 --- a/test/gns.test.ts +++ b/test/gns.test.ts @@ -546,7 +546,7 @@ describe('GNS', () => { it('reject set `ownerTaxPercentage` if not allowed', async function () { const tx = gns.connect(me.signer).setOwnerTaxPercentage(newValue) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -1034,7 +1034,7 @@ describe('GNS', () => { // Batch send transaction const tx = gns.connect(me.signer).multicall([tx1.data, tx2.data]) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('should revert if batching a call to initialize', async function () { @@ -1050,7 +1050,7 @@ describe('GNS', () => { // Batch send transaction const tx = gns.connect(me.signer).multicall([tx1.data, tx2.data]) - await expect(tx).revertedWith('Caller must be the implementation') + await expect(tx).revertedWith('Only implementation') }) it('should revert if trying to call a private function', async function () { diff --git a/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts index f283aabe1..236817afd 100644 --- a/test/l2/l2GraphTokenGateway.test.ts +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -39,14 +39,10 @@ describe('L2GraphTokenGateway', () => { const senderTokens = toGRT('1000') const defaultData = '0x' - const notEmptyCallHookData = utils.defaultAbiCoder.encode( + const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( ['uint256', 'uint256'], [toBN('1337'), toBN('42')], ) - const defaultDataWithNotEmptyCallHookData = utils.defaultAbiCoder.encode( - ['bytes', 'bytes'], - ['0x', notEmptyCallHookData], - ) before(async function () { ;[ @@ -126,7 +122,7 @@ describe('L2GraphTokenGateway', () => { describe('setL2Router', function () { it('is not callable by addreses that are not the governor', async function () { const tx = l2GraphTokenGateway.connect(tokenSender.signer).setL2Router(mockRouter.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('sets router address', async function () { const tx = l2GraphTokenGateway.connect(governor.signer).setL2Router(mockRouter.address) @@ -140,7 +136,7 @@ describe('L2GraphTokenGateway', () => { const tx = l2GraphTokenGateway .connect(tokenSender.signer) .setL1TokenAddress(mockL1GRT.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('sets l2GRT', async function () { const tx = l2GraphTokenGateway.connect(governor.signer).setL1TokenAddress(mockL1GRT.address) @@ -154,7 +150,7 @@ describe('L2GraphTokenGateway', () => { const tx = l2GraphTokenGateway .connect(tokenSender.signer) .setL1CounterpartAddress(mockL1Gateway.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('sets L1Counterpart', async function () { const tx = l2GraphTokenGateway @@ -169,24 +165,43 @@ describe('L2GraphTokenGateway', () => { describe('Pausable behavior', () => { it('cannot be paused or unpaused by someone other than governor or pauseGuardian', async () => { let tx = l2GraphTokenGateway.connect(tokenSender.signer).setPaused(false) - await expect(tx).revertedWith('Only Governor or Guardian can call') + await expect(tx).revertedWith('Only Governor or Guardian') tx = l2GraphTokenGateway.connect(tokenSender.signer).setPaused(true) - await expect(tx).revertedWith('Only Governor or Guardian can call') + await expect(tx).revertedWith('Only Governor or Guardian') }) - it('can be paused and unpaused by the governor', async function () { + it('cannot be paused if some state variables are not set', async function () { let tx = l2GraphTokenGateway.connect(governor.signer).setPaused(false) - await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(false) - await expect(await l2GraphTokenGateway.paused()).eq(false) - tx = l2GraphTokenGateway.connect(governor.signer).setPaused(true) + await expect(tx).revertedWith('L2_ROUTER_NOT_SET') + await l2GraphTokenGateway.connect(governor.signer).setL2Router(mockRouter.address) + tx = l2GraphTokenGateway.connect(governor.signer).setPaused(false) + await expect(tx).revertedWith('L1_COUNTERPART_NOT_SET') + await l2GraphTokenGateway + .connect(governor.signer) + .setL1CounterpartAddress(mockL1Gateway.address) + tx = l2GraphTokenGateway.connect(governor.signer).setPaused(false) + await expect(tx).revertedWith('L1_GRT_NOT_SET') + }) + it('can be paused and unpaused by the governor', async function () { + await fixture.configureL2Bridge( + governor.signer, + fixtureContracts, + mockRouter.address, + mockL1GRT.address, + mockL1Gateway.address, + ) + let tx = l2GraphTokenGateway.connect(governor.signer).setPaused(true) await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(true) await expect(await l2GraphTokenGateway.paused()).eq(true) + tx = l2GraphTokenGateway.connect(governor.signer).setPaused(false) + await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l2GraphTokenGateway.paused()).eq(false) }) describe('setPauseGuardian', function () { it('cannot be called by someone other than governor', async function () { const tx = l2GraphTokenGateway .connect(tokenSender.signer) .setPauseGuardian(pauseGuardian.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('sets a new pause guardian', async function () { const tx = l2GraphTokenGateway @@ -197,13 +212,20 @@ describe('L2GraphTokenGateway', () => { .withArgs(AddressZero, pauseGuardian.address) }) it('allows a pause guardian to pause and unpause', async function () { + await fixture.configureL2Bridge( + governor.signer, + fixtureContracts, + mockRouter.address, + mockL1GRT.address, + mockL1Gateway.address, + ) await l2GraphTokenGateway.connect(governor.signer).setPauseGuardian(pauseGuardian.address) - let tx = l2GraphTokenGateway.connect(pauseGuardian.signer).setPaused(false) - await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(false) - await expect(await l2GraphTokenGateway.paused()).eq(false) - tx = l2GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) + let tx = l2GraphTokenGateway.connect(pauseGuardian.signer).setPaused(true) await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(true) await expect(await l2GraphTokenGateway.paused()).eq(true) + tx = l2GraphTokenGateway.connect(pauseGuardian.signer).setPaused(false) + await expect(tx).emit(l2GraphTokenGateway, 'PauseChanged').withArgs(false) + await expect(await l2GraphTokenGateway.paused()).eq(false) }) }) }) @@ -393,7 +415,6 @@ describe('L2GraphTokenGateway', () => { ['uint256', 'uint256'], [toBN('0'), toBN('42')], ) - const data = utils.defaultAbiCoder.encode(['bytes', 'bytes'], ['0x', callHookData]) const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) await me.signer.sendTransaction({ to: await mockL1GatewayL2Alias.getAddress(), @@ -406,13 +427,12 @@ describe('L2GraphTokenGateway', () => { tokenSender.address, callhookReceiverMock.address, toGRT('10'), - data, + callHookData, ) await expect(tx).revertedWith('FOO_IS_ZERO') }) it('reverts if trying to call a callhook in a contract that does not implement onTokenTransfer', async function () { const callHookData = utils.defaultAbiCoder.encode(['uint256'], [toBN('0')]) - const data = utils.defaultAbiCoder.encode(['bytes', 'bytes'], ['0x', callHookData]) const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) await me.signer.sendTransaction({ to: await mockL1GatewayL2Alias.getAddress(), @@ -426,7 +446,7 @@ describe('L2GraphTokenGateway', () => { tokenSender.address, rewardsManager.address, toGRT('10'), - data, + callHookData, ) await expect(tx).revertedWith( "function selector was not recognized and there's no fallback function", diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 49e597745..3c72214a9 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -161,7 +161,7 @@ describe('Rewards', () => { describe('issuance per block update', function () { it('reject set issuance per block if unauthorized', async function () { const tx = rewardsManager.connect(indexer1.signer).setIssuancePerBlock(toGRT('1.025')) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('should set issuance rate to minimum allowed (0)', async function () { @@ -183,7 +183,7 @@ describe('Rewards', () => { const tx = rewardsManager .connect(indexer1.signer) .setSubgraphAvailabilityOracle(oracle.address) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('should set subgraph oracle if governor', async function () { diff --git a/test/staking/configuration.test.ts b/test/staking/configuration.test.ts index 39ed30502..52aeaa843 100644 --- a/test/staking/configuration.test.ts +++ b/test/staking/configuration.test.ts @@ -52,7 +52,7 @@ describe('Staking:Config', () => { it('reject set `minimumIndexerStake` if not allowed', async function () { const newValue = toGRT('100') const tx = staking.connect(me.signer).setMinimumIndexerStake(newValue) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('reject set `minimumIndexerStake` to zero', async function () { @@ -74,7 +74,7 @@ describe('Staking:Config', () => { it('reject set `slasher` if not allowed', async function () { const tx = staking.connect(other.signer).setSlasher(me.address, true) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('reject set `slasher` for zero', async function () { @@ -103,7 +103,7 @@ describe('Staking:Config', () => { it('reject set `assetHolder` if not allowed', async function () { const tx = staking.connect(other.signer).setAssetHolder(me.address, true) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('reject set `assetHolder` to address zero', async function () { @@ -122,7 +122,7 @@ describe('Staking:Config', () => { it('reject set `channelDisputeEpochs` if not allowed', async function () { const newValue = toBN('5') const tx = staking.connect(other.signer).setChannelDisputeEpochs(newValue) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('reject set `channelDisputeEpochs` to zero', async function () { @@ -146,7 +146,7 @@ describe('Staking:Config', () => { it('reject set `curationPercentage` if not allowed', async function () { const tx = staking.connect(other.signer).setCurationPercentage(50) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -166,7 +166,7 @@ describe('Staking:Config', () => { it('reject set `protocolPercentage` if not allowed', async function () { const tx = staking.connect(other.signer).setProtocolPercentage(50) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -180,7 +180,7 @@ describe('Staking:Config', () => { it('reject set `maxAllocationEpochs` if not allowed', async function () { const newValue = toBN('5') const tx = staking.connect(other.signer).setMaxAllocationEpochs(newValue) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -194,7 +194,7 @@ describe('Staking:Config', () => { it('reject set `thawingPeriod` if not allowed', async function () { const newValue = toBN('5') const tx = staking.connect(other.signer).setThawingPeriod(newValue) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) it('reject set `thawingPeriod` to zero', async function () { @@ -225,7 +225,7 @@ describe('Staking:Config', () => { it('reject set `rebateRatio` if not allowed', async function () { const tx = staking.connect(other.signer).setRebateRatio(1, 1) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) }) diff --git a/test/staking/delegation.test.ts b/test/staking/delegation.test.ts index cad46a063..e097dfd9f 100644 --- a/test/staking/delegation.test.ts +++ b/test/staking/delegation.test.ts @@ -217,7 +217,7 @@ describe('Staking::Delegation', () => { it('reject set `delegationRatio` if not allowed', async function () { const tx = staking.connect(me.signer).setDelegationRatio(delegationRatio) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -231,7 +231,7 @@ describe('Staking::Delegation', () => { it('reject set `delegationParametersCooldown` if not allowed', async function () { const tx = staking.connect(me.signer).setDelegationParametersCooldown(cooldown) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) @@ -251,7 +251,7 @@ describe('Staking::Delegation', () => { it('reject set `delegationTaxPercentage` if not allowed', async function () { const tx = staking.connect(me.signer).setDelegationTaxPercentage(50) - await expect(tx).revertedWith('Caller must be Controller governor') + await expect(tx).revertedWith('Only Controller governor') }) }) diff --git a/yarn.lock b/yarn.lock index 7b859117b..42084d9cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,21 +2,15 @@ # yarn lockfile v1 -"@arbitrum/sdk@^3.0.0-beta.6": - version "3.0.0-beta.6" - resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-3.0.0-beta.6.tgz#a36c3e39a7358396b5533f3288125107da6ae59e" - integrity sha512-kPCfgj72MeyVcIXQKoztLO29UTcpSbXFzc/S0oDgVNNcHcXp1hWUJqqkVRg0O43P2yKjZRT/I94K0Nj2nZNiiQ== +"@arbitrum/sdk@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-3.0.0.tgz#a5dc48a00cb8c6e230a2c696c0e880a7f80c637d" + integrity sha512-Mws5WAxxirp3vk8JH3vyQ5H6q1NNUIAAGEd9oEnQYDMyTBHLKU293GA3s9w4w6ZfIq/RZq8YCexhy4D1R+mQng== dependencies: "@ethersproject/address" "^5.0.8" "@ethersproject/bignumber" "^5.1.1" "@ethersproject/bytes" "^5.0.8" - "@typechain/ethers-v5" "9.0.0" - "@types/prompts" "^2.0.14" - "@types/yargs" "^17.0.9" - dotenv "^10.0.0" ethers "^5.1.0" - ts-node "^10.2.1" - typechain "7.0.0" "@babel/code-frame@7.12.11": version "7.12.11" @@ -191,18 +185,6 @@ dependencies: chalk "^4.0.0" -"@cspotcode/source-map-consumer@0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" - integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== - -"@cspotcode/source-map-support@0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" - integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== - dependencies: - "@cspotcode/source-map-consumer" "0.8.0" - "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -1252,14 +1234,6 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== -"@typechain/ethers-v5@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-9.0.0.tgz#6aa93bea7425c0463bd8a61eea3643540ef851bd" - integrity sha512-bAanuPl1L2itaUdMvor/QvwnIH+TM/CmG00q17Ilv3ZZMeJ2j8HcarhgJUZ9pBY1teBb85P8cC03dz3mSSx+tQ== - dependencies: - lodash "^4.17.15" - ts-essentials "^7.0.1" - "@typechain/ethers-v5@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-2.0.0.tgz#cd3ca1590240d587ca301f4c029b67bfccd08810" @@ -1470,13 +1444,6 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17" integrity sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA== -"@types/prompts@^2.0.14": - version "2.0.14" - resolved "https://registry.yarnpkg.com/@types/prompts/-/prompts-2.0.14.tgz#10cb8899844bb0771cabe57c1becaaaca9a3b521" - integrity sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA== - dependencies: - "@types/node" "*" - "@types/qs@^6.2.31": version "6.9.7" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" @@ -1560,13 +1527,6 @@ dependencies: "@types/yargs-parser" "*" -"@types/yargs@^17.0.9": - version "17.0.10" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.10.tgz#591522fce85d8739bca7b8bb90d048e4478d186a" - integrity sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA== - dependencies: - "@types/yargs-parser" "*" - "@typescript-eslint/eslint-plugin@^4.0.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" @@ -1988,16 +1948,6 @@ array-back@^2.0.0: dependencies: typical "^2.6.1" -array-back@^3.0.1, array-back@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" - integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== - -array-back@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" - integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -3541,26 +3491,6 @@ command-line-args@^4.0.7: find-replace "^1.0.3" typical "^2.6.1" -command-line-args@^5.1.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" - integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== - dependencies: - array-back "^3.1.0" - find-replace "^3.0.0" - lodash.camelcase "^4.3.0" - typical "^4.0.0" - -command-line-usage@^6.1.0: - version "6.1.2" - resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.2.tgz#2b7ccd48a93fb19bd71ca8fe9900feab00e557b0" - integrity sha512-I+0XN613reAhpBQ6icsPOTwu9cvhc9NtLtUcY2fGYuwm9JZiWBzFDA8w0PHqQjru7Xth7fM/y9TJ13+VKdjh7Q== - dependencies: - array-back "^4.0.1" - chalk "^2.4.2" - table-layout "^1.0.1" - typical "^5.2.0" - commander@2.18.0: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -3884,7 +3814,7 @@ debug@3.2.6: dependencies: ms "^2.1.1" -debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.3: +debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -3959,11 +3889,6 @@ deep-equal@~1.1.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" -deep-extend@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -4166,11 +4091,6 @@ dotenv@*: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411" integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q== -dotenv@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" - integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== - dotenv@^9.0.0: version "9.0.2" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" @@ -5465,13 +5385,6 @@ find-replace@^1.0.3: array-back "^1.0.4" test-value "^2.1.0" -find-replace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" - integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== - dependencies: - array-back "^3.0.1" - find-up@3.0.0, find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -6153,6 +6066,16 @@ hardhat-gas-reporter@^1.0.4: eth-gas-reporter "^0.2.24" sha1 "^1.1.1" +hardhat-secure-accounts@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/hardhat-secure-accounts/-/hardhat-secure-accounts-0.0.5.tgz#753889ad43ae1bfa2df6839ffc556ed3a25d9668" + integrity sha512-ma/UOYV8fROMucLifflUEvYdtchcK4JB2tCV6etAg8PB66OlBo7MwmofnWnN4ABMR8Qt7zGgedFBkGdmBrmxRA== + dependencies: + debug "^4.3.4" + enquirer "^2.3.6" + lodash.clonedeep "^4.5.0" + prompt-sync "^4.2.0" + hardhat-storage-layout@0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/hardhat-storage-layout/-/hardhat-storage-layout-0.1.6.tgz#b6ae33d4c00f385dbc1ff67c86d67b0198cfbd91" @@ -7892,10 +7815,10 @@ lodash.assign@^4.0.3, lodash.assign@^4.0.6: resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== lodash.get@^4: version "4.4.2" @@ -9684,6 +9607,13 @@ promise@^8.0.0: dependencies: asap "~2.0.6" +prompt-sync@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/prompt-sync/-/prompt-sync-4.2.0.tgz#0198f73c5b70e3b03e4b9033a50540a7c9a1d7f4" + integrity sha512-BuEzzc5zptP5LsgV5MZETjDaKSWfchl5U9Luiu8SKp7iZWD5tZalOxvNcZRwv+d2phNFr8xlbxmFNcRKfJOzJw== + dependencies: + strip-ansi "^5.0.0" + proper-lockfile@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" @@ -10055,11 +9985,6 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -reduce-flatten@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" - integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== - regenerate@^1.2.1: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" @@ -11122,11 +11047,6 @@ string-argv@0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== -string-format@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" - integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== - string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -11359,16 +11279,6 @@ sync-rpc@^1.2.1: dependencies: get-port "^3.1.0" -table-layout@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" - integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== - dependencies: - array-back "^4.0.1" - deep-extend "~0.6.0" - typical "^5.2.0" - wordwrapjs "^4.0.0" - table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" @@ -11622,16 +11532,6 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-2.2.1.tgz#c5bf04a5bbec3fd118be4084461b3a27c4d796bf" integrity sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q== -ts-command-line-args@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ts-command-line-args/-/ts-command-line-args-2.2.1.tgz#fd6913e542099012c0ffb2496126a8f38305c7d6" - integrity sha512-mnK68QA86FYzQYTSA/rxIjT/8EpKsvQw9QkawPic8I8t0gjAOw3Oa509NIRoaY1FmH7hdrncMp7t7o+vYoceNQ== - dependencies: - chalk "^4.1.0" - command-line-args "^5.1.1" - command-line-usage "^6.1.0" - string-format "^2.0.0" - ts-essentials@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-1.0.4.tgz#ce3b5dade5f5d97cf69889c11bf7d2da8555b15a" @@ -11662,25 +11562,6 @@ ts-generator@^0.1.1: resolve "^1.8.1" ts-essentials "^1.0.0" -ts-node@^10.2.1: - version "10.7.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" - integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== - dependencies: - "@cspotcode/source-map-support" "0.7.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.0" - yn "3.1.1" - ts-node@^10.9.1: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" @@ -11823,22 +11704,6 @@ type@^2.5.0: resolved "https://registry.yarnpkg.com/type/-/type-2.6.0.tgz#3ca6099af5981d36ca86b78442973694278a219f" integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ== -typechain@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/typechain/-/typechain-7.0.0.tgz#258ca136de1d451368bde01c318976a83062f110" - integrity sha512-ILfvBBFJ7j9aIk0crX03+N2GmzoDN1gtk32G1+XrasjuvXS0XAw2XxwQeQMMgKwlnxViJjIkG87sTMYXPkXA9g== - dependencies: - "@types/prettier" "^2.1.1" - debug "^4.1.1" - fs-extra "^7.0.0" - glob "^7.1.6" - js-sha3 "^0.8.0" - lodash "^4.17.15" - mkdirp "^1.0.4" - prettier "^2.1.2" - ts-command-line-args "^2.2.0" - ts-essentials "^7.0.1" - typechain@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/typechain/-/typechain-3.0.0.tgz#d5a47700831f238e43f7429b987b4bb54849b92e" @@ -11912,16 +11777,6 @@ typical@^2.6.0, typical@^2.6.1: resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" integrity sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg== -typical@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" - integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== - -typical@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" - integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== - uglify-js@^3.1.4: version "3.15.3" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.3.tgz#9aa82ca22419ba4c0137642ba0df800cb06e0471" @@ -12136,11 +11991,6 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -v8-compile-cache-lib@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" - integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== - v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -12864,14 +12714,6 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -wordwrapjs@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" - integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== - dependencies: - reduce-flatten "^2.0.0" - typical "^5.2.0" - workerpool@6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b"