Ethereum with Rust Tutorial Part 2: Compile and Deploy Solidity Contract with Rust

Our previous tutorial introduced how to start Ganache from Rust, query Ethereum balance and make simple transactions in Rust.

In this tutorial, you will learn how to compile and deploy Solidity smart contracts to Ethereum from Rust.

Again the source code is available on Github.

Start Ganache

We'll start Ganache instance and get a Web3 Provider instance just the same way as in our previous tutorial. Additionally, we will also get the Chain ID which will be used later.

The Ethereum Chain ID and the Network ID are used to differentiate different Ethereum blockchain networks. For most networks, the two IDs are the same, this why sometimes we use Chain ID and Network ID interchangeably.

Compile solidity project

Next, we will compile a Solidity project in a few steps.

  • Provide a directory that holds our Solidity source code files:
    let root = PathBuf::from("examples/");
    
  • Create a ProjectBuilder and configure the builder:
    let project = Project::builder()
        .paths(paths)
        .set_auto_detect(true)
        .no_artifacts()
        .build()?;
    

A few things to note:

  1. we use set_auto_detect(true) to enable automatic solc version detection from source code. Note that to use this feature, you need to enable svm-solc feature for the ethers-solc library. If you don't enable this feature, this method will have no effect. The default solc in your system path will be selected to compile your project.
  2. use no_artifacts() to generate stop artifacts on disk.
  3. by default the build will be cached, if you suspect the caching is causing any problem, you can use .ephemeral() to disable caching.
  4. Now we can compile our project and get results:
    let output = project.compile()?;
    if output.has_compiler_errors() {
        Err(eyre!(
            "Compiling solidity project failed: {:?}",
            output.output().errors
        ))
    } else {
        Ok(output.clone())
    }
    

Inspect a compiled project

Before deploying the compiled project, we'll spend some time trying to get some interesting information from the compiled project.

We can get all the artifacts from the compiled project. An artifact contains information about the ABI, source map information about the smart contracts.

let artifacts = project.into_artifacts();

We will use the following code block to print out the contract name, constructor argument list and a list of all functions in every contract:

for (id, artifact) in artifacts {
    let name = id.name;
    let abi = artifact.abi.context("No ABI found for artificat {name}")?;

    println!("CONTRACT: {:?}", name);

    let contract = &abi.abi;
    let functions = contract.functions();
    let functions = functions.cloned();
    let constructor = contract.constructor();

    if let Some(constructor) = constructor {
        let args = &constructor.inputs;
        println!("CONSTRUCTOR args: {args:?}");
    }

    for func in functions {
        let name = &func.name;
        let params = &func.inputs;
        println!("FUNCTION {name} {params:?}");
    }
}

Deploy contract

We will locate our target contract from the compiled project and deploy it to our locally running Ganache EVM.

  • To find the compiled contract by name we simple use the find method:
    let contract = project
        .find(contract_name)
        .context("Contract not found")?;
    
  • Get the ABI and bytecode of the contract required for deployment:
    let (abi, bytecode, _) = contract.into_parts();
    
  • Create a SignerMiddleware for signing the contract deployment transaction:
    let wallet = wallet.with_chain_id(chain_id);
    let client = SignerMiddleware::new(provider.clone(), wallet).into();
    

Note that it's important to set the chain ID manually, the default value might not be consistent with the Chain ID our network is using.

  • Deploy the contract

We create a ContractFactory and deploy the contract:

let factory = ContractFactory::new(abi.clone(), bytecode, client);
let deployer = factory.deploy(constructor_args)?;
let deployed_contract = deployer.clone().legacy().send().await?;

Some EVM chains don't support the new version of transaction types. That's why we need to use legacy() here. For more information, see here.

When we deploy the contract or run some transactions, we may encounter the following error

Error: Transaction's maxFeePerGas ... is less than the block's baseFeePerGas ...

The reason is that starting from the Ethereum London fork, each block's base gas fee may increase by 12.5%.

The naive solution is to set the gas price to a higher value:

deployer.tx.set_gas::<U256>(20_000_000.into()); // 10 times of default value

However this may result in us paying a much higher gas price in our transactions. A better solution is to use the estimated gas price from the Ethereum block:

// ... 
let block = provider
    .clone()
    .get_block(BlockNumber::Latest)
    .await?
    .context("Failed to get latest block")?;

let gas_price = block
    .next_block_base_fee()
    .context("Failed to get the next block base fee")?;
deployer.tx.set_gas_price::<U256>(gas_price);
// ... 

What's next

In the next tutorial, I'll show you how to invoke contract methods from Rust.

Comment