Smart contract development is a cornerstone of decentralized application (dApp) building on Ethereum. While Solidity remains the dominant language for writing contracts, interacting with them from the backend or tooling side has evolved rapidly. Rust, known for its performance, safety, and concurrency, has emerged as a powerful choice for Ethereum interaction—especially through the ethers-rs library.
This guide walks you through developing, deploying, and interacting with Ethereum smart contracts using Rust and ethers-rs, focusing on practical implementation patterns. We’ll cover compilation, deployment, querying, writing state changes, bytecode inspection, and ERC20 token integration—all while leveraging Rust’s strong typing and developer ergonomics.
Core Keywords
- Ethereum smart contract
- Rust programming
- ethers-rs
- ABI generation
- Contract deployment
- ERC20 token interaction
These keywords reflect the core technical themes of this article and are naturally integrated throughout to support SEO without keyword stuffing.
Compiling Smart Contracts and Generating ABI with Rust
To interact with a Solidity smart contract in Rust, you first need its Application Binary Interface (ABI)—a JSON description of the contract’s functions, events, and types.
The ethers-rs library provides tools to compile .sol files directly using the Solidity compiler (solc). It expects solc to be available in your system path. Alternatively, set the SOLC_PATH environment variable to specify a custom location.
Here's an example of how to compile a contract and generate its ABI at runtime:
use ethers::{prelude::Abigen, solc::Solc};
use eyre::Result;
fn main() -> Result<()> {
let mut args = std::env::args();
args.next().unwrap(); // skip program name
let contract_name = "SimpleStorage";
let contract: String = args.next().unwrap_or_else(|| "simple_contract.sol".to_owned());
println!("Generating bindings for contract: {contract}\n");
let abi = if contract.ends_with(".sol") {
let contracts = Solc::default().compile_source(&contract)?;
let abi = contracts.get(&contract, &contract_name).unwrap().abi.clone().unwrap();
serde_json::to_string(&abi).unwrap()
} else {
contract
};
let bindings = Abigen::new(&contract_name, abi)?.generate()?;
if let Some(output_path) = args.next() {
bindings.write_to_file(output_path)?;
} else {
bindings.write(&mut std::io::stdout())?;
}
Ok(())
}⚠️ Note: You may encounter"Invalid EVM version requested"errors due to version mismatches betweensolcandethers-rs. As ofethers-rs 2.0.10, the default EVM target is Shanghai. Usesolc v0.8.23+for compatibility.
👉 Learn how to securely manage blockchain interactions using modern tools
Alternatively, generate the ABI manually via command line:
solc --abi simple_contract.sol -o .This outputs SimpleStorage.abi, a JSON file used later for type-safe Rust bindings.
Deploying a Smart Contract with ethers-rs
Deploying a contract requires:
- A running Ethereum node (e.g., Geth or Anvil)
- Wallet credentials (private key)
- Compiled bytecode and ABI
- Gas to pay for deployment
Below is a complete example deploying a SimpleStorage contract:
use ethers::{
contract::{abigen, ContractFactory},
middleware::SignerMiddleware,
providers::{Http, Provider},
signers::{Wallet, Signer},
solc::Solc,
};
use std::{convert::TryFrom, path::Path, sync::Arc};
use eyre::Result;
abigen!(
SimpleContract,
"simple_contract.json",
event_derives(serde::Deserialize, serde::Serialize)
);
const RPC_URL: &str = "http://127.0.0.1:8545";
#[tokio::main]
async fn main() -> Result<()> {
let prikey = hex::decode("0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e")?;
let wallet = Wallet::from_bytes(&prikey).unwrap();
println!("Wallet address: {:?}", wallet.address());
let source = Path::new(&env!("CARGO_MANIFEST_DIR")).join("simple_contract.sol");
let compiled = Solc::default().compile_source(source)?;
let (abi, bytecode, _) = compiled.find("SimpleStorage").unwrap().into_parts_or_default();
let provider = Provider::<Http>::try_from(RPC_URL)?;
let chain_id = provider.get_chainid().await?.as_u64();
let client = Arc::new(SignerMiddleware::new(provider, wallet.with_chain_id(chain_id)));
let factory = ContractFactory::new(abi, bytecode, client.clone());
let contract = factory.deploy("initial value".to_string())?.send().await?;
let addr = contract.address();
println!("Contract deployed at: {addr:?}");
let contract_instance = SimpleContract::new(addr, client.clone());
let _receipt = contract_instance.set_value("hi".to_owned()).send().await?.await?;
let logs = contract_instance.value_changed_filter().query().await?;
let value = contract_instance.get_value().call().await?;
println!("Value: {value}. Logs: {}", serde_json::to_string(&logs)?);
Ok(())
}🔁 Rust vs Solidity Naming:ethers-rsautomatically converts camelCase Solidity method names (likegetValue) into snake_case (get_value) for idiomatic Rust usage.
Loading and Querying an Existing Smart Contract
Once a contract is deployed, you can query its read-only functions using its address and ABI:
use ethers::prelude::*;
use std::sync::Arc;
const RPC_URL: &str = "http://127.0.0.1:8545";
const CONTRACT_ADDRESS: &str = "0x73511669fd4de447fed18bb79bafeac93ab7f31f";
#[tokio::main]
async fn main() -> Result<()> {
let provider = Provider::<Http>::try_from(RPC_URL)?;
abigen!(SimpleContract, "SimpleStorage.abi");
let contract_address: Address = CONTRACT_ADDRESS.parse()?;
let client = Arc::new(provider);
let contract = SimpleContract::new(contract_address, client);
let value = contract.get_value().call().await?;
println!("Current contract value: {value}");
Ok(())
}This pattern enables off-chain dApp backends to fetch real-time data from on-chain contracts.
👉 Explore secure development practices for blockchain applications
Writing Data to a Smart Contract
Writing data (e.g., updating state) costs gas and must be signed by a wallet:
let _receipt = contract.set_value("new value".to_owned()).send().await?.await?;Note the double .await:
- First waits for the transaction to be submitted (returns
PendingTransaction) - Second waits for confirmation (mined into a block)
Always handle potential reverts or out-of-gas failures in production.
Reading Contract Bytecode from Chain
You can inspect the deployed bytecode of any contract:
let code = provider.get_code(CONTRACT_ADDRESS, None).await?;
println!("Contract bytecode: {code:?}");This is useful for verification, audits, or detecting proxy patterns.
Interacting with ERC20 Tokens
ERC20 is the standard interface for fungible tokens. Here’s how to query balance, supply, symbol, and decimals:
abigen!(
IERC20,
r#"[
function totalSupply() external view returns (uint256)
function balanceOf(address account) external view returns (uint256)
function symbol() external view returns (string memory)
function decimals() external view returns (uint8)
]"#
);
let contract = IERC20::new(erc20_address, client);
if let Ok(total_supply) = contract.total_supply().call().await {
println!("Total supply: {total_supply}");
}
if let Ok(symbol) = contract.symbol().call().await {
println!("Symbol: {symbol}");
}
if let Ok(decimals) = contract.decimals().call().await {
println!("Decimals: {decimals}");
}Use the official IERC20.json ABI for full compatibility.
Frequently Asked Questions
Q: Do I need solc installed to use ethers-rs?
A: Yes, if you're compiling .sol files at runtime. For production use, pre-generate ABIs to avoid runtime dependencies.
Q: Can I use environment variables to manage private keys safely?
A: Absolutely. Never hardcode keys. Use .env files or secret managers in production environments.
Q: What’s the difference between .call() and .send()?
A: .call() reads state (free), while .send() writes state (requires gas and wallet signing).
Q: How do I handle events like ValueChanged?
A: Use filters: contract.value_changed_filter().from_block(0).query().await?.
Q: Is ethers-rs production-ready?
A: Yes. It's actively maintained and used in production dApps and infrastructure tools.
Q: Can I interact with other EVM chains (e.g., Polygon, Arbitrum)?
A: Yes. Just change the RPC URL and chain ID accordingly.
👉 Discover how OKX supports multi-chain development and deployment
Summary
Building Ethereum smart contract tooling in Rust with ethers-rs offers performance, safety, and clarity. Whether you're deploying new contracts or integrating with existing ones like ERC20 tokens, Rust provides a robust foundation for blockchain interaction.
Key takeaways:
- Use
solc+Abigenfor type-safe bindings - Always match EVM versions across tooling
- Prefer precompiled ABIs in production
- Leverage async/await for smooth node interaction
- Follow naming conventions across languages
With these patterns, you can build scalable, secure dApp backends or blockchain automation tools entirely in Rust.