Skip to content

Uniswap V4 Address Mining

Updated:
by 22Xy

To mine the best salt for Uniswap v4, I modified create2crunch to generate the salt that meets the criteria. For details about the challenge, you can refer to the official blog. In this post, I will focus on the technical details of the modifications I made. You can also just go to 22Xy/uniswapv4-create2crunch to see the source code and run it yourself.

I don’t have a super powerful GPU, so I’m mining with my CPU. It’s also the reason why I want to share the program. I want people with powerful GPUs to have a chance to mine the best salt. The best salt I got has a score of 118 126 128:

0xbd24513ed63130e883553105c1893d540372adc689a01751d24fe0cde5290d20
=> 0x00000044449641ffa0d5e0934e683317da264884 => Score: 128
// 0xbd24513ed63130e883553105c1893d540372adc64084fbc94b7caa1acdc90490
// => 0x0000004444e3df28675c96dad0ed4c26147d7127 => Score: 126
// 0xbd24513ed63130e883553105c1893d540372adc6ce68e37bcb273a9a6d830e40
// => 0x0000044443168c40f4d68d163473fdc4a59c3fa2 => Score: 118

As of the time of writing, it can be ranked 19th on the leaderboard:

leaderboard

Table of contents

Open Table of contents

Configuration

Uniswap V4 will be deployed using the CREATE2 function. This function generates deterministic addresses using:

Therefore, we only need to generate the salt. In the .env file, we set the INITCODE_HASH and DEPLOYER_ADDRESS to the above values.

FACTORY="0x48E516B34A1274f49457b9C6182097796D0498Cb"
INIT_CODE_HASH="0x94d114296a5af85c1fd2dc039cdaa32f1ed4b0fe0868f02d888bfc91feb645d9"

*To ensure that only you can submit your salt, set the first 20 bytes of your salt to the Ethereum address executing the submission. Alternatively, you can leave the first 20 bytes as 0 bytes, but your submission could be frontrun. The last 12 bytes of the salt can be anything you choose.

We set the CALLER to your Ethereum address you use to submit the salt.

CALLER="<YOUR_ETH_ADDRESS>"

Replace GPU_DEVICE with your GPU device ID if using GPU mining. The default is 255 for CPU mining.

GPU_DEVICE="255"

We have score thresholds for the best salt. Scores below SCORE_MIN_THRESHOLD will not be logged/recorded.

SCORE_MIN_THRESHOLD="10" # > Minimum score to consider
SCORE_MAX_THRESHOLD="100000" # Maximum score cap

Challenge Rules

In reward.rs, the calculate_score function is used to calculate the score of an address. We check if the first non-zero nibble is 4. If not, we return None.

pub fn calculate_score(&self, address: &str) -> Option<usize> {
    // ...
    // Find the first non-zero nibble index
    let first_non_zero_idx = addr.find(|c: char| c != '0')?;
    if addr.chars().nth(first_non_zero_idx)? != '4' {
        // Address does not meet the first non-zero nibble requirement
        return None;
    }
    // ...
}

Scoring Criteria

In reward.rs, we calculate the score based on the following criteria:

let leading_zeros = addr
    .chars()
    .take(first_non_zero_idx)
    .filter(|&c| c == '0')
    .count();
score += leading_zeros * 10;
if remainder.starts_with("4444") {
    score += 40;
}
if remainder.chars().nth(4) != Some('4') {
    score += 20;
}
if addr.ends_with("4444") {
    score += 20;
}
let additional_fours = addr[first_non_zero_idx..]
    .chars()
    .filter(|&c| c == '4')
    .count();
score += additional_fours;

Mining

I’m currently running the program on my MacBook Pro M3 Pro and I don’t have a powerful GPU. So I haven’t fully tested the performance of the gpu method yet. Here, I will only break down the cpu method in lib.rs.

  1. Setup and Initialization
// Open or create the output file for storing found salts and addresses
let file = output_file();

// Initialize the Reward system with scoring thresholds
let rewards = Reward::new().expect("Failed to initialize Reward");

// Initialize runtime statistics variables
let start_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs_f64();
let found = Arc::new(AtomicU64::new(0));
let cumulative_nonce = Arc::new(AtomicU64::new(0));
let found_list = Arc::new(Mutex::new(Vec::new()));

What it does:

  1. Launching the Statistics Monitoring Thread
// Clone Arc references for the statistics thread
let stats_found = Arc::clone(&found);
let stats_nonce = Arc::clone(&cumulative_nonce);
let stats_list = Arc::clone(&found_list);

// Spawn a separate thread to display runtime statistics
thread::spawn(move || {
    loop {
        // Calculate and display statistics (runtime, rate, total found)
        // Show the latest found addresses
        // ...

        // Print statistics
        println!("\x1B[2J\x1B[1;1H"); // Clear screen
        println!(
            "Total runtime: {}:{:02}:{:02} ({} cycles)\n\
            Rate: {:.2} million attempts per second\n\
            Total found this run: {}\n\
            Score thresholds: min={}, max={}\n",
            total_runtime_hrs,
            total_runtime_mins,
            total_runtime_secs,
            stats_nonce.load(Ordering::Relaxed),
            rate,
            stats_found.load(Ordering::Relaxed),
            config.score_min_threshold,
            config.score_max_threshold,
        );
        // Display recently found addresses
        let found_list_guard = stats_list.lock().unwrap();
        let last_10: Vec<String> = found_list_guard.iter().rev().take(10).cloned().collect();
        drop(found_list_guard);

        // ...
        thread::sleep(Duration::from_secs(1)); // Update every second
    }
});

What it does:

  1. Main Mining Loop
loop {
    // Construct the CREATE2 header with factory, caller, and random salt segments
    let mut header = [0; 47];
    header[0] = CONTROL_CHARACTER;
    header[1..21].copy_from_slice(&config.factory_address);
    header[21..41].copy_from_slice(&config.calling_address);
    header[41..].copy_from_slice(&FixedBytes::<6>::random()[..]);

    // Initialize Keccak-256 hashing with the header
    let mut hash_header = Keccak::v256();
    hash_header.update(&header);

    // Increment the nonce for the next batch of salts
    cumulative_nonce.fetch_add(MAX_INCREMENTER, Ordering::Relaxed);

    // Process a batch of salts in parallel
    (0..MAX_INCREMENTER).into_par_iter().for_each(|salt| {
        // Generate the salt segment
        let salt_bytes = salt.to_le_bytes();
        let salt_segment = &salt_bytes[..6];

        // Clone and finalize the hash with the salt and initialization code
        let mut hash = hash_header.clone();
        hash.update(salt_segment);
        hash.update(&config.init_code_hash);
        let mut res = [0; 32];
        hash.finalize(&mut res);

        // Derive the Ethereum address from the hash result
        let address = <&Address>::try_from(&res[12..]).unwrap();
        let address_str = format!("0x{}", hex::encode(address));

        // Calculate the score based on Uniswap V4 criteria
        let score_option = rewards.calculate_score(&address_str);
        let score = match score_option {
            Some(s) => s,
            None => return, // Skip if the address doesn't meet the criteria
        };

        // Check for duplicate addresses
        {
            let mut processed = PROCESSED_ADDRESSES.lock().unwrap();
            if !processed.insert(address_str.clone()) {
                return; // Skip if already processed
            }
        }

        // Format the full salt for output
        let full_salt = format!(
            "0x{}{}",
            hex::encode(&header[42..]),
            hex::encode(salt_segment)
        );

        // Prepare the output string
        let output = format!("{full_salt} => {address_str} => Score: {score}\n");
        println!("{output}");

        // Write the result to the output file safely
        file.lock_exclusive().expect("Couldn't lock file.");
        writeln!(&file, "{output}").expect("Couldn't write to file.");
        file.unlock().expect("Couldn't unlock file.");

        // Update the found counters and list
        found.fetch_add(1, Ordering::Relaxed);
        found_list.lock().unwrap().push(output.clone());
    });
}

Not so much modification from the original code, but I logged and recorded the output in this format for better readability.

let output = format!("{full_salt} => {address_str} => Score: {score}\n");
println!("{output}");

Results

Example output when mining with CPU:

Total runtime: 0:00:20.080167055130005 (281474976710655 cycles)
Rate: 14017561.50 million attempts per second
Total found this run: 2
Score thresholds: min=90, max=100000

0xbd24513ed63130e883553105c1893d540372adc60a7fb862b875f3d3e7000000 => 0x00044446648fd9f03b834afcb4f63c0192ae152d => Score: 97

0xbd24513ed63130e883553105c1893d540372adc60a7fb862b875179514020000 => 0x00044449b22d31e1cbd3e2de90b33f0bda989014 => Score: 95

Example output when mining with GPU:

Total runtime: 0:00:7.089572906494141 (42 cycles)
Rate: 396.92 million attempts per second
Total found this run: 2
Current search space: 090836f7xxxxxxxx859388b1
Score thresholds: min=0, max=100000

0xbd24513ed63130e883553105c1893d540372adc6c0087372e0ffff03d43ec597 => 0x4d4a91e0197b9e6d0bd32ef6d76d460f40b7a5e2 => Score: 4

0xbd24513ed63130e883553105c1893d540372adc6188043c760ffff03545a2da0 => 0x4d2c88a006228ba4df9389c79b67acadf2f5eee4 => Score: 3

As you can see, the rate is much lower when mining with a GPU. You can try it yourself if you have a powerful GPU.

Resource


Previous Post
Resolving Solidity ABI Packing Errors in Go
Next Post
Stop Promoting Software Engineering Shortcut Tutorials