Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(solidity-devops): gas pricing, verification and deployment utils #2439

Merged
merged 17 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/solidity-devops/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ DFK_VERIFIER=sourcify
# And the list goes on

# TESTNET CHAINS
# TODO: add testnet chains
# Ethereum Sepolia Testnet
ETH_SEPOLIA_RPC=https://rpc.sepolia.org
ETH_SEPOLIA_VERIFIER=etherscan
ETH_SEPOLIA_VERIFIER_URL=https://api-sepolia.etherscan.io/api
ETH_SEPOLIA_VERIFIER_KEY=YourEtherScanKey
171 changes: 171 additions & 0 deletions packages/solidity-devops/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,174 @@ This package is still under development and should be used with caution. It is n
## Docs

Forge docs are automatically deployed [here](https://solidity-devops.vercel.app).

## Setup

In order to use this package, it needs to be installed as a dependency in your project. It is recommended to install it as a dev dependency.

```bash
npm install --save-dev @synapsecns/solidity-devops
# Or, if you are using yarn
yarn add --dev @synapsecns/solidity-devops
```

Following files are necessary to be present in the root of your project:

- `.env`: storage for the sensitive configuration variables.
- `devops.json`: configuration file for the devops package.
- `foundry.toml`: configuration file for Foundry.
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved

### `.env`

**Note: This file must be added to `.gitignore` to avoid sharing sensitive information.**

See the [example .env](./.env.example) file. Following structure is required:

- `Wallets`: defines the deployer wallets used for running the scripts. Assuming the `chad` is the name of the wallet, the following values are required:
- `CHAD_ADDRESS`: the address of the account to use for signing
- `CHAD_TYPE`: the type of the wallet:
- `keystore`: use the encrypted keystore file.
- `CHAD_JSON`: the keystore file path
- `ledger` or `trezor`: use the hardware wallet.
- TODO: find out if ledger/trezor specific options are needed
- `pk`: use the plaintext private key. STRONGLY DISCOURAGED for production usage, meant for local devnet purposes.
- `CHAD_PK`: the private key to the wallet in 0x format.
- Any other value triggers the interactive prompt to enter the private key.
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
- `Chains`: defines the list of chains where the scripts will run. Assuming the `chain` is the name of the chain, the following values are required:
- `CHAIN_RPC`: the RPC endpoint of the chain
- `CHAIN_VERIFIER`: verifier for the smart contracts. Possible values:
- `etherscan`
- `blockscout`
- `sourcify`
- `CHAIN_VERIFIER_URL`: the Verifier API endpoint (required if verifier is not sourcify)
- **Note: Blockscout URL needs to end with "/api?" for the verification to work.**
- `CHAIN_VERIFIER_KEY`: the Verifier API key (required if verifier is not sourcify)
- **Note: Use any non-empty string for Blockscout API key: it is not required per se, but foundry will complain if it's empty.**

### `devops.json`

See the [example devops.json](./devops.json) file. Following values are required:

- `chains`: list of chain-specific options for `forge script` command.
- Check the available options by running `forge script --help`.
- Two additional options to handle the gas pricing are introduced:
- `--auto-gas-1559`: will fetch the current base fee and priority fee from the chain's RPC node, and use following values for submitting the transactions:
- `maxPriorityFee = priorityFee`
- `maxGasPrice = 2 * baseFee + priorityFee`
- `--auto-gas-legacy`: will fetch the current gas price from the chain's RPC node, and use it for submitting the transactions.
> Note: these options are introduced as foundry script hardcodes the 3 Gwei priority fee, which is not ideal for all the chains.
- `deployConfigs`: path to the directory that contains the deployment configuration files. These configuration files are expected to be either chain-specific or global:
- The chain-specific configuration files should be located in `deployConfigsDir/{chainName}/{contractName}.dc.json`.
- The global configuration files should be located in `deployConfigsDir/global/{contractName}.{globalProperty}.json` or `deployConfigsDir/global/{contractName}.json`.
- The configuration files usually include the variables required for the contract deployment or its subsequent management.
- `deployments`: path to the directory that contains the deployment artifact files. These files are automatically generated by `fsr` and alike commands.
- `forgeArtifacts`: path to the directory that `forge` uses to store the artifacts for the compiled contracts.
- **Must match the `out` value in the `foundry.toml` file.**
- `freshDeployments`: path to the directory that contains the **fresh** deployment artifact files. These files are generated during the local run of the `forge script` command.
- It is recommended to add this directory to `.gitignore` to avoid unnecessary changes in the repository.
- The artifacts in this directory are automatically moved into the `deployments` directory by `fsr` and alike commands.
> Note: we opted to use this approach, as the local run of the `forge script` is always done before the actual broadcast of the deployment transactions. Therefore, the end results might not match the local run, if there was an unexpected on-chain revert, or if transactions were not included in the block. A separate job is automatically run to check all the new artifacts and move them to the `deployments` directory, if the on-chain deployment was successful.
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved

### `foundry.toml`

See the [example foundry.toml](./foundry.toml) file. Following values are required:

- `fs_permissions`:
- `{ access = "read", path = "./" }` to allow reading files from the repository root
- `{ access = "read-write", path = "<freshDeployments>" }` to allow reading/writing fresh deployment artifacts
- `rpc_endpoints`:
- It is recommended to follow the structure defined in the example file to avoid duplication:
- `arbitrum = "${ARBITRUM_RPC}"`
- `etherscan`:
- It is recommended to follow the structure defined in the example file to avoid duplication:
- `arbitrum = { key = "${ARBITRUM_VERIFIER_KEY}", url = "${ARBITRUM_VERIFIER_URL}" }`
- You should omit values for chains that are using Sourcify for verification.

See the [Foundry Book](https://book.getfoundry.sh/config/) for more information.

## Usage

### Writing scripts

- Your scripts should inherit from the `SynapseScript` class (or `SynapseScript06` if you're using Solidity 0.6 compiler).
- This class already inherits from `forge-std`'s `Script` class, so you can use all the methods from it.
- See the [example script](./script/DeployBasicContract.s.sol) for more information.
- It is recommended to use separate scripts for initial deployments and subsequent configuration of the contracts.
- The initial deployment scripts should be named `Deploy{ContractName}.s.sol`.
- The configuration scripts should be named `Configure{ContractName}.s.sol`.

### Running scripts

Your main point of entry for running the scripts should be the `yarn fsr` [command](./js/forgeScriptRun.js).

> Note: `fsr` is a shorthand for `forge script run`.

```bash
# yarn fsr <path-to-script> <chain-name> <wallet-name> [<options>]
# To simulate the script without broadcasting, using wallet named "chad"
yarn fsr script/DeployBasicContract.s.sol eth_sepolia chad
# To broadcast the script using wallet named "chad"
# NOTE: there is no need to add --verify option, as it is automatically added for the broadcasted scripts.
yarn fsr script/DeployBasicContract.s.sol eth_sepolia chad --broadcast
```

This command is responsible for the following:

- Initializing the chain's deployment directories with the `.chainid` file, if they don't exist.
- Logging the wallet's address, balance and nonce.
- Running the script using the `forge script` command.
- This executes the `run()` method of the script.
- `--verify` option is added if the script is broadcasted.
- Collecting the receipts for the newly deployed contracts, and saving their deployment artifacts.

You can also utilize the `yarn fsr-str` [command](./js/forgeScriptRunString.js) to provide a single string argument for the running script:

- This executes the `run(string memory arg)` method of the script, passing the provided string argument.

```bash
# yarn fsr-str <path-to-script> <chain-name> <wallet-name> <string-arg> [<options>]
# To simulate the script without broadcasting, using wallet named "chad"
yarn fsr-str script/DeployBasicContract.s.sol eth_sepolia chad "AAAAAAAA"
# To broadcast the script using wallet named "chad"
# NOTE: there is no need to add --verify option, as it is automatically added for the broadcasted scripts.
yarn fsr-str script/DeployBasicContract.s.sol eth_sepolia chad "AAAAAAAA" --broadcast
```

### Managing deployments

If for whatever reason the deployment script was interrupted, you can use `yarn sd` [command](./js/saveDeployment.js) to save the deployment artifacts.

> Note: `sd` is a shorthand for `save deployment`.

```bash
# yarn sd <chain-name> <contract-alias>
yarn sd eth_sepolia BasicContract
# Or, if the contract alias is used
yarn sd eth_sepolia BasicContract.Alias
```

### Contract verification

You can verify the contracts with the saved deployment artifacts using the `yarn fvc` [command](./js/forgeVerifyContract.js).

> Note: `fvc` is a shorthand for `forge verify contract`.

```bash
# yarn fvc <chain-name> <contract-alias>
yarn fvc eth_sepolia BasicContract
# Or, if the contract alias is used
yarn fvc eth_sepolia BasicContract.Alias
```

### Proxy verification

You can verify the proxy's implementation in Etherscan-alike explorers using the `yarn vp` [command](./js/verifyProxy.js).

> Note: `vp` is a shorthand for `verify proxy`.

```bash
# yarn vp <chain-name> <contract-alias>
yarn vp eth_sepolia BasicContract
# Or, if the contract alias is used
yarn vp eth_sepolia TransparentUpgradeableProxy.BasicContract
```
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
11155111
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"address": "0x8d0643b5a82360EFFC790D097E2bAd745Cb213D2",
"constructorArgs": "0x000000000000000000000000000000000000000000000000000000000000002a",
"receipt": {
"hash": "0x88bfd2c537609bc5650f2d110f10d690cfd135aa35d7793e20d875c087befcc5",
"blockNumber": 5619902
},
"abi": [
{
"type": "constructor",
"inputs": [
{
"name": "_value",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "value",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
}
]
}
21 changes: 12 additions & 9 deletions packages/solidity-devops/devops.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
{
"chains": {
"arbitrum": "--with-gas-price 100000000",
"arbitrum": "--auto-gas-1559",
"aurora": "--legacy --slow",
"base": "--with-gas-price 100000000",
"base": "--auto-gas-1559",
"blast": "--auto-gas-1559",
"boba": "--legacy --skip-simulation --slow",
"canto": "--legacy --slow",
"cronos": "--slow",
"dfk": "--slow",
"cronos": "--auto-gas-1559 --slow",
"dfk": "--auto-gas-1559 --slow",
"dogechain": "--legacy --slow",
"harmony": "--legacy --slow",
"klaytn": "--skip-simulation",
"klaytn": "--auto-gas-1559 --skip-simulation",
"mainnet": "--auto-gas-1559",
"metis": "--legacy",
"moonbeam": "--skip-simulation --slow",
"moonriver": "--skip-simulation --slow",
"optimism": "--with-gas-price 100000000",
"polygon": "--slow"
"moonbeam": "--auto-gas-1559 --skip-simulation --slow",
"moonriver": "--auto-gas-1559 --skip-simulation --slow",
"optimism": "--auto-gas-1559",
"polygon": "--auto-gas-1559 --slow",
"eth_sepolia": "--auto-gas-1559"
},
"deployConfigs": "configs",
"deployments": "deployments",
Expand Down
5 changes: 4 additions & 1 deletion packages/solidity-devops/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ evm_version = "paris"
optimizer = false
out = "artifacts"
src = "src"
ffi = true
fs_permissions = [
{ access = "read", path = "./" },
{ access = "read-write", path = "./.deployments" }
Expand All @@ -23,9 +22,13 @@ number_underscore = 'thousands'
arbitrum = "${ARBITRUM_RPC}"
aurora = "${AURORA_RPC}"
dfk = "${DFK_RPC}"
# Testnets
eth_sepolia = "${ETH_SEPOLIA_RPC}"

# And the list goes on
[etherscan]
arbitrum = { key = "${ARBITRUM_VERIFIER_KEY}", url = "${ARBITRUM_VERIFIER_URL}" }
aurora = { key = "${AURORA_VERIFIER_KEY}", url = "${AURORA_VERIFIER_URL}" }
# DFK is using Sourcify for verification
# Testnets
eth_sepolia = { key = "${ETH_SEPOLIA_VERIFIER_KEY}", url = "${ETH_SEPOLIA_VERIFIER_URL}" }
24 changes: 4 additions & 20 deletions packages/solidity-devops/js/forgeScriptRun.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ const fs = require('fs')
const { readChainSpecificOptions, logWallet } = require('./utils/chain.js')
const {
createDeploymentDirs,
getConfirmedFreshDeployment,
getNewDeployments,
getNewDeploymentReceipts,
saveDeploymentArtifact,
saveNewDeployment,
} = require('./utils/deployments.js')
const { loadEnv } = require('./utils/env.js')
const { forgeScript } = require('./utils/forge.js')
Expand Down Expand Up @@ -56,21 +55,6 @@ if (newDeployments.length === 0) {
process.exit(0)
}
const newReceipts = getNewDeploymentReceipts(chainName, scriptFN)
newDeployments.forEach((contractAlias) => {
const artifact = getConfirmedFreshDeployment(chainName, contractAlias)
if (!artifact) {
return
}
// Find the matching receipt
const receipt = newReceipts.find((r) => r.address === artifact.address)
if (!receipt) {
logInfo(`No receipt found for ${contractAlias} at ${artifact.address}`)
return
}
// Add receipt.hash and receipt.blockNumber to the artifact, but don't add receipt.address
artifact.receipt = {
hash: receipt.hash,
blockNumber: receipt.blockNumber,
}
saveDeploymentArtifact(chainName, contractAlias, artifact)
})
newDeployments.forEach((contractAlias) =>
saveNewDeployment(chainName, contractAlias, newReceipts)
)
45 changes: 45 additions & 0 deletions packages/solidity-devops/js/forgeVerifyContract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env node
const { getChainId, readChainVerificationOptions } = require('./utils/chain.js')
const {
getSavedDeployment,
getContractName,
} = require('./utils/deployments.js')
const { loadEnv } = require('./utils/env.js')
const { forgeVerify } = require('./utils/forge.js')
const { logError } = require('./utils/logger.js')
const { addOptions, parseCommandLineArgs } = require('./utils/options.js')

loadEnv()

const { positionalArgs, options } = parseCommandLineArgs({
requiredArgsCount: 2,
usage: 'Usage: "yarn fvc <chain-name> <contract-alias> [<options>]"',
})
const [chainName, contractAlias] = positionalArgs
const contractName = getContractName(contractAlias)
const chainId = getChainId(chainName)
const deployment = getSavedDeployment(chainName, contractAlias)
if (!deployment) {
process.exit(0)
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
}
const chainVerificationOptions = readChainVerificationOptions(chainName)
if (!chainVerificationOptions) {
process.exit(1)
}
const { address, constructorArgs } = deployment
if (!address) {
logError(`Missing address in deployment file for ${contractAlias}`)
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
process.exit(1)
process.exit(0)
}
if (!constructorArgs) {
constructorArgs = '0x'
}

let forgeOptions = `${address} ${contractName}`
forgeOptions = addOptions(forgeOptions, `--chain ${chainId} --watch`)
forgeOptions = addOptions(forgeOptions, chainVerificationOptions)
forgeOptions = addOptions(forgeOptions, options)
forgeOptions = addOptions(forgeOptions, `--constructor-args ${constructorArgs}`)

forgeVerify(forgeOptions)
19 changes: 19 additions & 0 deletions packages/solidity-devops/js/saveDeployment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env node
const {
createDeploymentDirs,
getAllDeploymentReceipts,
saveNewDeployment,
} = require('./utils/deployments.js')
const { loadEnv } = require('./utils/env.js')
const { parseCommandLineArgs } = require('./utils/options.js')

loadEnv()

const { positionalArgs } = parseCommandLineArgs({
requiredArgsCount: 2,
usage: 'Usage: "yarn sd <chain-name> <contract-alias>"',
})
const [chainName, contractAlias] = positionalArgs
createDeploymentDirs(chainName)
const allReceipts = getAllDeploymentReceipts(chainName)
saveNewDeployment(chainName, contractAlias, allReceipts)
Loading
Loading