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:
Table of contents
Open Table of contents
Configuration
Uniswap V4 will be deployed using the CREATE2 function. This function generates deterministic addresses using:
- The hash of the initcode for Uniswap v4:
0x94d114296a5af85c1fd2dc039cdaa32f1ed4b0fe0868f02d888bfc91feb645d9
- The deployer address for Uniswap v4:
0x48E516B34A1274f49457b9C6182097796D0498Cb
- Your choice of a salt*
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
- Scoring Validity: Only addresses with a first non-zero nibble of 4 are valid for scoring.
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:
- 10 points for each leading 0 nibble
let leading_zeros = addr
.chars()
.take(first_non_zero_idx)
.filter(|&c| c == '0')
.count();
score += leading_zeros * 10;
- 40 points if the address starts with four consecutive 4s
if remainder.starts_with("4444") {
score += 40;
}
- 20 points if the first nibble after the four 4s is not a 4
if remainder.chars().nth(4) != Some('4') {
score += 20;
}
- 20 points if the last four nibbles are all 4s
if addr.ends_with("4444") {
score += 20;
}
- 1 point for each 4 elsewhere in the address
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.
- 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:
- Output File: Prepares
efficient_addresses.txt
to record valid salts and their corresponding addresses and scores. - Reward System: Sets up the scoring mechanism based on thresholds.
- Statistics Variables: Initializes shared counters and a list to track found addresses and mining progress.
- 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:
- Cloning References: Shares the counters and list with the new thread safely.
- Statistics Thread: Continuously calculates and displays mining statistics without interrupting the main mining process.
- 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.