Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@ coins-bip39 = "0.12"
# Utils
hex.workspace = true
itertools = "0.14.0"
regex = "1"
toml = "0.8.19"
dirs = "6.0.0"
rand = "0.9.1"
rayon.workspace = true
url = "2.5.7"

# HTTP
reqwest = { version = "0.12.7", features = ["json"] }

# Serde
serde = "1.0.218"
serde_json = "1.0.139"
87 changes: 87 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,12 +425,99 @@ Options:
Remove downloaded dependencies after compilation
--salt <SALT>
Salt for deploying CREATE2 contracts. If it is provided, the contract will be deployed using CREATE2.
--optimizations <OPTIMIZATIONS>
Number of optimization runs for the Solidity compiler
--verify-contract
Verify the contract on Etherscan after deployment
--etherscan-api-key <ETHERSCAN_API_KEY>
Etherscan API key for contract verification [env: ETHERSCAN_API_KEY=]
--contract-name <CONTRACT_NAME>
Contract name (defaults to filename stem). Required when file contains multiple contracts.
--rpc-url <RPC_URL>
[env: RPC_URL=] [default: http://localhost:8545]
-h, --help
Print help
```

#### Contract verification

The `--verify-contract` flag automatically verifies the contract source code on Etherscan after deployment. This makes the contract's source code publicly readable and its ABI available on Etherscan.

**Requirements:**
- Must use `--contract-path` (not `--bytecode`)
- Requires an Etherscan API key via `--etherscan-api-key` or the `ETHERSCAN_API_KEY` environment variable
- Incompatible with `--cast` (verification needs to wait for the transaction receipt)
- Requires `solc` installed locally (the same version used to compile will be reported to Etherscan)

**Supported networks:** Ethereum Mainnet, Sepolia, and Holesky (uses [Etherscan API V2](https://docs.etherscan.io/etherscan-v2)).

**Basic example:**

```shell
rex deploy \
--contract-path MyContract.sol \
--remappings "" \
--verify-contract \
--etherscan-api-key $ETHERSCAN_API_KEY \
--private-key $PRIVATE_KEY \
--rpc-url https://ethereum-sepolia-rpc.publicnode.com
```

**With optimizer enabled:**

```shell
rex deploy \
--contract-path MyContract.sol \
--remappings "" \
--optimizations 200 \
--verify-contract \
--etherscan-api-key $ETHERSCAN_API_KEY \
--private-key $PRIVATE_KEY \
--rpc-url https://ethereum-sepolia-rpc.publicnode.com
```

**With imports and remappings:**

```shell
rex deploy \
--contract-path MyToken.sol \
--remappings "@openzeppelin/contracts=https://github.com/OpenZeppelin/openzeppelin-contracts.git" \
--verify-contract \
--etherscan-api-key $ETHERSCAN_API_KEY \
--private-key $PRIVATE_KEY \
--rpc-url https://ethereum-sepolia-rpc.publicnode.com
```

**With constructor arguments:**

```shell
rex deploy \
--contract-path MyToken.sol \
--remappings "" \
--verify-contract \
--etherscan-api-key $ETHERSCAN_API_KEY \
--private-key $PRIVATE_KEY \
--rpc-url https://ethereum-sepolia-rpc.publicnode.com \
-- "constructor(string,uint256)" "MyToken" 1000000
```

**Output:**

```
Compiler run successful. Artifact(s) can be found in directory "./solc_out".
Contract deployed in tx: 0x1234...
Contract address: 0x5678...

Verifying contract on Etherscan...
Compiler version: v0.8.31+commit.fd3a2265
Contract name: MyContract
Submitting verification request...
Verification submitted (GUID: abc123...)
[1/60] Checking verification status...
Contract verified successfully!
https://sepolia.etherscan.io/address/0x5678...#code
```

### `rex encode-calldata`

```Shell
Expand Down
106 changes: 85 additions & 21 deletions cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::common::{AuthorizeArgs, BlockOverrideArgs, StateOverrideArgs};
use crate::utils::{
encode_constructor_args, parse_contract_creation, parse_func_call, parse_hex, parse_hex_string,
};
use crate::verify::{VerifyParams, verify_contract_on_etherscan};
use crate::{
commands::autocomplete,
common::{CallArgs, DeployArgs, SendArgs, TransferArgs},
Expand Down Expand Up @@ -645,23 +646,44 @@ impl Command {

println!("{result}");
}
Command::Deploy { args, rpc_url } => {
Command::Deploy { mut args, rpc_url } => {
if args.explorer_url {
todo!("Display transaction URL in the explorer")
}

if args.verify_contract {
if args.bytecode.is_some() {
return Err(eyre::eyre!(
"--verify-contract cannot be used with --bytecode. Use --contract-path instead."
));
}
if args.etherscan_api_key.is_none() {
return Err(eyre::eyre!(
"--verify-contract requires --etherscan-api-key or ETHERSCAN_API_KEY env var."
));
}
if args.cast {
return Err(eyre::eyre!(
"--verify-contract is incompatible with --cast (verification needs to wait for the receipt)."
));
}
}

let deployer = Signer::Local(LocalSigner::new(args.private_key));
let client = EthClient::new(rpc_url)?;

let bytecode = if let Some(bytecode) = args.bytecode {
bytecode
} else {
compile_contract_from_path(args.clone()).await?
let (bytecode, compilation_info) = match args.bytecode.take() {
Some(bytecode) => (bytecode, None),
None => {
let info = compile_contract_from_path(&args)?;
let bytecode = info.bytecode.clone();
(bytecode, Some(info))
}
};
let init_args = if !args.constructor_args.is_empty() {
encode_constructor_args(&args.constructor_args)?
} else if !args._args.is_empty() {
parse_contract_creation(args._args)?
parse_contract_creation(std::mem::take(&mut args._args))?
} else {
Bytes::new()
};
Expand All @@ -676,7 +698,7 @@ impl Command {
)
.await?
} else {
let init_code = [bytecode, init_args].concat().into();
let init_code = [bytecode, init_args.clone()].concat().into();
deploy(
&client,
&deployer,
Expand Down Expand Up @@ -706,6 +728,35 @@ impl Command {
if !args.cast {
wait_for_transaction_receipt(tx_hash, &client, 100, silent).await?;
}

if args.verify_contract {
// Unwraps are safe: validation above rejects bytecode and missing API key.
let info = compilation_info
.as_ref()
.expect("compilation_info is always set when verify_contract is true");

let chain_id: u64 = client
.get_chain_id()
.await?
.try_into()
.map_err(|_| eyre::eyre!("Chain ID too large to fit in u64"))?;

verify_contract_on_etherscan(VerifyParams {
contract_address: deployed_contract_address,
contract_path: args.contract_path.unwrap(),
contract_name: args.contract_name,
constructor_args: init_args.to_vec(),
remappings: info.remappings.clone(),
optimize_runs: info.optimize_runs,
etherscan_api_key: args.etherscan_api_key.unwrap(),
chain_id,
})
.await?;
}

if let Some(info) = compilation_info.filter(|i| i.should_clean) {
cleanup_compilation_artifacts(info.cloned_dirs);
}
}
Command::ChainId { hex, rpc_url } => {
let eth_client = EthClient::new(rpc_url)?;
Expand Down Expand Up @@ -960,16 +1011,24 @@ fn print_calldata(depth: usize, data: Value) {
}
}

async fn compile_contract_from_path(args: DeployArgs) -> eyre::Result<Bytes> {
struct CompilationInfo {
bytecode: Bytes,
remappings: Vec<(String, PathBuf)>,
optimize_runs: Option<u64>,
cloned_dirs: Vec<PathBuf>,
should_clean: bool,
}

fn compile_contract_from_path(args: &DeployArgs) -> eyre::Result<CompilationInfo> {
let contract_path = args
.contract_path
.as_ref()
.ok_or_else(|| eyre::eyre!("Contract path is required when bytecode is not provided"))?;

let clean = !args.keep_deps;
let should_clean = !args.keep_deps;
let output_dir = Path::new(".");
let deps_dir = Path::new("rex_deps");
let mut solc_remappings = Vec::new();
let mut solc_remappings: Vec<(String, PathBuf)> = Vec::new();
let mut cloned_dirs = Vec::new();

std::fs::create_dir_all(deps_dir).ok();
Expand Down Expand Up @@ -1033,7 +1092,7 @@ async fn compile_contract_from_path(args: DeployArgs) -> eyre::Result<Bytes> {
false,
Some(&solc_remappings_ref),
&include_paths,
None,
args.optimizations,
)
.map_err(|e| eyre::eyre!("Failed to compile contract: {}", e))?;

Expand Down Expand Up @@ -1062,16 +1121,21 @@ async fn compile_contract_from_path(args: DeployArgs) -> eyre::Result<Bytes> {
})?
.into();

if clean {
for dir in cloned_dirs {
if dir.exists() {
std::fs::remove_dir_all(&dir)
.map_err(|e| eyre::eyre!("Failed to clean up {}: {}", dir.display(), e))?;
}
Ok(CompilationInfo {
bytecode,
remappings: solc_remappings,
optimize_runs: args.optimizations,
cloned_dirs,
should_clean,
})
}

fn cleanup_compilation_artifacts(cloned_dirs: Vec<PathBuf>) {
for dir in cloned_dirs {
if dir.exists() {
std::fs::remove_dir_all(&dir).ok();
}
std::fs::remove_dir_all("rex_deps").ok();
std::fs::remove_dir_all("solc_out").ok();
}

Ok(bytecode)
std::fs::remove_dir_all("rex_deps").ok();
std::fs::remove_dir_all("solc_out").ok();
}
26 changes: 26 additions & 0 deletions cli/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,32 @@ pub struct DeployArgs {
required = false
)]
pub salt: Option<Secret>,
#[arg(
long,
required = false,
help = "Number of optimization runs for the Solidity compiler"
)]
pub optimizations: Option<u64>,
#[arg(
long,
help = "Verify the contract on Etherscan after deployment",
default_value_t = false,
requires = "source"
)]
pub verify_contract: bool,
#[arg(
long,
env = "ETHERSCAN_API_KEY",
required = false,
help = "Etherscan API key for contract verification"
)]
pub etherscan_api_key: Option<String>,
#[arg(
long,
required = false,
help = "Contract name (defaults to filename stem). Required when file contains multiple contracts."
)]
pub contract_name: Option<String>,
#[arg(last = true, hide = true)]
pub _args: Vec<String>,
#[arg(
Expand Down
1 change: 1 addition & 0 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pub mod cli;
mod commands;
mod common;
mod utils;
mod verify;
Loading
Loading