Skip to content

feat(cli): add create2-address command #146

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ hex.workspace = true
itertools = "0.14.0"
toml = "0.8.19"
dirs = "6.0.0"
rand = "0.9.1"
rayon = "1.10.0"

# Logging
log = "0.4"
Expand Down
132 changes: 125 additions & 7 deletions cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::commands::l2;
use crate::utils::{parse_contract_creation, parse_func_call, parse_hex};
use crate::utils::{parse_contract_creation, parse_func_call, parse_hex, parse_hex_string};
use crate::{
commands::autocomplete,
common::{CallArgs, DeployArgs, SendArgs, TransferArgs},
Expand All @@ -9,15 +9,19 @@ use clap::{ArgAction, Parser, Subcommand};
use ethrex_common::{Address, Bytes, H256, H520};
use keccak_hash::keccak;
use rex_sdk::calldata::{Value, decode_calldata};
use rex_sdk::create::compute_create_address;
use rex_sdk::create::{
DETERMINISTIC_DEPLOYER, brute_force_create2_rayon, compute_create_address,
compute_create2_address,
};
use rex_sdk::sign::{get_address_from_message_and_signature, sign_hash};
use rex_sdk::utils::to_checksum_address;
use rex_sdk::{
balance_in_eth,
client::{EthClient, Overrides, eth::get_address_from_secret_key},
transfer, wait_for_transaction_receipt,
};

use secp256k1::SecretKey;
use std::io::{self, Write};

pub const VERSION_STRING: &str = env!("CARGO_PKG_VERSION");

Expand Down Expand Up @@ -110,12 +114,73 @@ pub(crate) enum Command {
#[clap(about = "Compute contract address given the deployer address and nonce.")]
CreateAddress {
#[arg(help = "Deployer address.")]
address: Address,
deployer: Address,
#[arg(short = 'n', long, help = "Deployer Nonce. Latest by default.")]
nonce: Option<u64>,
#[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")]
rpc_url: String,
},
Create2Address {
#[arg(
short = 'd',
long,
help = "Deployer address. Default is Mainnet Deterministic Deployer",
default_value = DETERMINISTIC_DEPLOYER
)]
deployer: Address,
#[arg(
short = 'i',
long,
help = "Initcode of the contract to deploy.",
required_unless_present_any = ["init_code_hash"],
conflicts_with_all = ["init_code_hash"]
)]
init_code: Option<Bytes>,
#[arg(
long,
help = "Hash of the initcode (keccak256).",
required_unless_present_any = ["init_code"],
conflicts_with_all = ["init_code"]
)]
init_code_hash: Option<H256>,
#[arg(short = 's', long, help = "Salt for CREATE2 opcode")]
salt: Option<H256>,
#[arg(
long,
required_unless_present_any = ["salt", "ends", "contains"],
help = "Address must begin with this hex prefix.",
value_parser = parse_hex_string,
)]
begins: Option<String>,
#[arg(
long,
required_unless_present_any = ["salt", "begins", "contains"],
help = "Address must end with this hex suffix.",
value_parser = parse_hex_string,
)]
ends: Option<String>,
#[arg(
long,
required_unless_present_any = ["salt", "begins", "ends"],
help = "Address must contain this hex substring.",
value_parser = parse_hex_string,
)]
contains: Option<String>,
#[arg(
long,
help = "Make the address search case sensitive when using begins, ends, or contains.",
default_value_t = false,
conflicts_with_all = ["salt"],
)]
case_sensitive: bool,
#[arg(
long,
help = "Number of threads to use for brute-forcing. Defaults to the number of logical CPUs.",
default_value_t = rayon::current_num_threads(),
conflicts_with_all = ["salt"],
)]
threads: usize,
},
#[clap(about = "Deploy a contract")]
Deploy {
#[clap(flatten)]
Expand Down Expand Up @@ -234,13 +299,66 @@ impl Command {
println!("{block_number}");
}
Command::CreateAddress {
address,
deployer,
nonce,
rpc_url,
} => {
let nonce = nonce.unwrap_or(EthClient::new(&rpc_url).get_nonce(address).await?);
let nonce = match nonce {
Some(n) => n,
None => {
let nonce = EthClient::new(&rpc_url).get_nonce(deployer).await?;
println!("Latest nonce: {nonce}");
nonce
}
};

println!("Address: {:#x}", compute_create_address(deployer, nonce))
}
Command::Create2Address {
deployer,
init_code,
salt,
init_code_hash,
begins,
ends,
contains,
case_sensitive,
threads,
} => {
let init_code_hash = init_code_hash
.or_else(|| init_code.as_ref().map(keccak))
.ok_or_else(|| eyre::eyre!("init_code_hash and init_code are both None"))?;

let (salt, contract_address) = match salt {
Some(salt) => {
let contract_address =
compute_create2_address(deployer, init_code_hash, salt);
(salt, contract_address)
}
None => {
// If salt is not provided, search for a salt that matches the criteria set by the user.
println!("\nComputing Create2 Address with {threads} threads...");
io::stdout().flush().ok();

let start = std::time::Instant::now();
let (salt, contract_address) = brute_force_create2_rayon(
deployer,
init_code_hash,
begins,
ends,
contains,
case_sensitive,
);
let duration = start.elapsed();
println!("Generated in: {:.2?}.", duration);
(salt, contract_address)
}
};

let contract_address = to_checksum_address(&format!("{contract_address:x}"));

println!("0x{:x}", compute_create_address(address, nonce))
println!("\nSalt: {salt:#x}");
println!("\nAddress: 0x{contract_address}");
}
Command::Transaction { tx_hash, rpc_url } => {
let eth_client = EthClient::new(&rpc_url);
Expand Down
11 changes: 11 additions & 0 deletions cli/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ pub fn parse_hex(s: &str) -> eyre::Result<Bytes, FromHexError> {
}
}

/// Parses a hex string, stripping the "0x" prefix if present.
/// Unlike `parse_hex`, the string doesn't need to be of even length.
pub fn parse_hex_string(s: &str) -> eyre::Result<String> {
let s = s.strip_prefix("0x").unwrap_or(s);
if s.chars().all(|c| c.is_ascii_hexdigit()) {
Ok(s.to_string())
} else {
Err(eyre::eyre!("Invalid hex string"))
}
}

fn parse_call_args(args: Vec<String>) -> eyre::Result<Option<(String, Vec<Value>)>> {
let mut args_iter = args.iter();
let Some(signature) = args_iter.next() else {
Expand Down
1 change: 1 addition & 0 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ toml = "0.8.19"
dirs = "6.0.0"
envy = "0.4.2"
thiserror.workspace = true
rayon = "1.10.0"

# Logging
log = "0.4"
Expand Down
117 changes: 116 additions & 1 deletion sdk/src/create.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
use ethrex_common::Address;
use ethrex_rlp::encode::RLPEncode;
use keccak_hash::keccak;
use keccak_hash::{H256, keccak};
use rand::RngCore;
use rayon::prelude::*;
use std::iter;
use std::sync::Arc;

use crate::utils::to_checksum_address;

pub const DETERMINISTIC_DEPLOYER: &str = "0x4e59b44847b379578588920cA78FbF26c0B4956C";

/// address = keccak256(rlp([sender_address,sender_nonce]))[12:]
pub fn compute_create_address(sender_address: Address, sender_nonce: u64) -> Address {
Expand All @@ -10,6 +18,113 @@ pub fn compute_create_address(sender_address: Address, sender_nonce: u64) -> Add
Address::from_slice(&keccak_bytes[12..])
}

/// address = keccak256(0xff || deployer_address || salt || keccak256(initialization_code))[12:]
pub fn compute_create2_address(
deployer_address: Address,
init_code_hash: H256,
salt: H256,
) -> Address {
Address::from_slice(
&keccak(
[
&[0xff],
deployer_address.as_bytes(),
&salt.0,
init_code_hash.as_bytes(),
]
.concat(),
)
.as_bytes()[12..],
)
}

/// Brute-force Create2 address generation
/// This function generates random salts until it finds one that matches the specified criteria.
/// `begins`, `ends`, and `contains` are optional filters for the generated address.
/// If they are not provided, the function will not filter based on that criterion.
/// Returns the salt and the generated address.
pub fn brute_force_create2(
deployer: Address,
init_code_hash: H256,
mut begins: Option<String>,
mut ends: Option<String>,
mut contains: Option<String>,
case_sensitive: bool,
) -> (H256, Address) {
// If we don't care about case convert everything to lowercase.
if !case_sensitive {
begins = begins.map(|b| b.to_lowercase());
ends = ends.map(|e| e.to_lowercase());
contains = contains.map(|c| c.to_lowercase());
}
loop {
// Generate random salt
let mut salt_bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut salt_bytes);
let salt = H256::from(salt_bytes);

// Compute Create2 Address
let candidate_address = compute_create2_address(deployer, init_code_hash, salt);

// Address as string without 0x prefix
let addr_str = if !case_sensitive {
format!("{candidate_address:x}")
} else {
to_checksum_address(&format!("{candidate_address:x}"))
};

// Validate that address satisfies the requirements given by the user.
let matches_begins = begins.as_ref().is_none_or(|b| addr_str.starts_with(b));
let matches_ends = ends.as_ref().is_none_or(|e| addr_str.ends_with(e));
let matches_contains = contains.as_ref().is_none_or(|c| addr_str.contains(c));

if matches_begins && matches_ends && matches_contains {
return (salt, candidate_address);
}
}
}

pub fn brute_force_create2_rayon(
deployer: Address,
init_code_hash: H256,
begins: Option<String>,
ends: Option<String>,
contains: Option<String>,
case_sensitive: bool,
) -> (H256, Address) {
let begins = Arc::new(begins.map(|s| if case_sensitive { s } else { s.to_lowercase() }));
let ends = Arc::new(ends.map(|s| if case_sensitive { s } else { s.to_lowercase() }));
let contains = Arc::new(contains.map(|s| if case_sensitive { s } else { s.to_lowercase() }));

iter::repeat_with(|| {
let mut salt_bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut salt_bytes);
H256::from(salt_bytes)
})
.par_bridge() // Convert into a parallel iterator
.find_any(|salt| {
// Find a salt that satisfies the criteria set by the user.
let addr = compute_create2_address(deployer, init_code_hash, *salt);

let addr_str = if !case_sensitive {
format!("{addr:x}")
} else {
to_checksum_address(&format!("{addr:x}"))
};

let matches_begins = begins.as_deref().is_none_or(|b| addr_str.starts_with(b));
let matches_ends = ends.as_deref().is_none_or(|e| addr_str.ends_with(&e));
let matches_contains = contains.as_deref().is_none_or(|c| addr_str.contains(c));

matches_begins && matches_ends && matches_contains
})
.map(|salt| {
let addr = compute_create2_address(deployer, init_code_hash, salt);
(salt, addr)
})
.expect("should eventually find a match")
}

#[test]
fn compute_address() {
use std::str::FromStr;
Expand Down
31 changes: 31 additions & 0 deletions sdk/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use ethrex_common::H256;
use keccak_hash::keccak;
use secp256k1::SecretKey;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

Expand All @@ -17,3 +18,33 @@ where
let hex = H256::from_slice(&secret_key.secret_bytes());
hex.serialize(serializer)
}

/// EIP-55 Checksum Address.
/// This is how addresses are actually displayed on ethereum apps
/// Returns address as string without "0x" prefix
pub fn to_checksum_address(address: &str) -> String {
// Trim if necessary
let addr = address.trim_start_matches("0x").to_lowercase();

// Hash the raw address using Keccak-256
let hash = keccak(&addr);

// Convert hash to hex string
let hash_hex = hex::encode(hash);

// Apply checksum by walking each nibble
let mut checksummed = String::with_capacity(40);

for (i, c) in addr.chars().enumerate() {
let hash_char = hash_hex.chars().nth(i).unwrap();
let hash_value = hash_char.to_digit(16).unwrap();

if c.is_ascii_alphabetic() && hash_value >= 8 {
checksummed.push(c.to_ascii_uppercase());
} else {
checksummed.push(c);
}
}

checksummed
}
Loading