| Crates.io | safe-rs |
| lib.rs | safe-rs |
| version | 0.5.0 |
| created_at | 2026-01-21 21:36:20.76702+00 |
| updated_at | 2026-01-25 23:11:11.414934+00 |
| description | Rust library for Safe v1.4.1 smart account interaction |
| homepage | |
| repository | https://github.com/tynes/safe-rs |
| max_upload_size | |
| id | 2060160 |
| size | 339,686 |
A Rust library and CLI for interacting with Safe smart accounts. Built for single-owner (1/1) Safes with a focus on simplicity, safety, and developer experience.
Opinionated by design. safe-rs optimizes for an opinionated usecase: single-owner Safes where you want to execute transactions with confidence. Rather than supporting every Safe configuration, it provides a streamlined experience with compile-time guarantees and optional forking simulation.
Minimal surface area. One way to do things, done well. No configuration sprawl, no optional safety features that can be accidentally disabled.
sol! macrocargo install safe-rs
[dependencies]
safe-rs = "0.1"
Execute an ERC20 transfer through your Safe:
safe send 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
'transfer(address,uint256)' 0xRecipient 1000000 \
--safe 0xYourSafe \
--rpc-url $ETH_RPC_URL \
--private-key $PRIVATE_KEY
Simulate without executing:
safe call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
'transfer(address,uint256)' 0xRecipient 1000000 \
--safe 0xYourSafe \
--rpc-url $ETH_RPC_URL
use safe_rs::{Safe, contracts::IERC20};
use alloy::primitives::{address, U256};
let safe = Safe::connect(provider, signer, safe_address).await?;
safe.verify_single_owner().await?;
let result = safe
.multicall()
.add_typed(token, IERC20::transferCall {
to: recipient,
amount: U256::from(1_000_000),
})
.simulate().await?
.execute().await?;
println!("Transaction: {:?}", result.transaction_hash);
safe sendExecute transactions through a Safe. Always simulates first, then prompts for confirmation.
Single call:
safe send <to> <signature> [args...] --safe <address> --rpc-url <url>
Multiple calls:
safe send \
--call 0xToken:transfer(address,uint256):0xRecipient,1000 \
--call 0xToken:approve(address,uint256):0xSpender,5000 \
--safe 0xYourSafe \
--rpc-url $ETH_RPC_URL
From bundle file:
safe send --bundle transactions.json --safe 0xYourSafe --rpc-url $ETH_RPC_URL
Options:
| Flag | Description |
|---|---|
--simulate-only |
Simulate without executing |
--call-only |
Use MultiSendCallOnly (no delegatecall) |
--no-confirm |
Skip confirmation prompt |
--json |
Output as JSON |
-i, --interactive |
Prompt for private key |
safe callSimulate a transaction without executing. Useful for testing and gas estimation.
safe call <to> <signature> [args...] --safe <address> --rpc-url <url>
safe infoQuery Safe state.
safe info --safe 0xYourSafe --rpc-url $ETH_RPC_URL
Output:
Safe: 0xYourSafe
Nonce: 42
Threshold: 1
Owners:
- 0xOwner1
safe createDeploy a new Safe with deterministic addressing.
safe create \
--owner 0xAdditionalOwner \
--threshold 2 \
--salt-nonce 12345 \
--rpc-url $ETH_RPC_URL \
--private-key $PRIVATE_KEY
Options:
| Flag | Description |
|---|---|
--owner <address> |
Additional owner (repeatable) |
--threshold <n> |
Required signatures (default: 1) |
--salt-nonce <n> |
Salt for deterministic address |
--compute-only |
Show address without deploying |
All commands that require signing support:
| Flag | Description |
|---|---|
--private-key <key> |
Private key (hex) |
-i, --interactive |
Prompt for private key securely |
PRIVATE_KEY env var |
Environment variable |
use safe_rs::Safe;
// Auto-detect chain configuration
let safe = Safe::connect(provider, signer, safe_address).await?;
// Verify single-owner requirement
safe.verify_single_owner().await?;
The MulticallBuilder provides a fluent API for constructing transactions:
// Raw call
let builder = safe.multicall()
.add(Call {
to: address,
value: U256::ZERO,
data: calldata.into(),
operation: Operation::Call,
});
// Typed call (recommended)
let builder = safe.multicall()
.add_typed(token, IERC20::transferCall { to, amount });
// Multiple calls batch automatically
let builder = safe.multicall()
.add_typed(token1, transfer1)
.add_typed(token2, transfer2)
.call_only(); // Use MultiSendCallOnly for safety
Simulation runs the transaction against a fork of the current blockchain state:
let builder = builder.simulate().await?;
// Access simulation result
if let Some(result) = builder.simulation_result() {
println!("Success: {}", result.success);
println!("Gas used: {}", result.gas_used);
println!("Logs: {:?}", result.logs);
// If simulation failed
if let Some(reason) = &result.revert_reason {
println!("Revert reason: {}", reason);
}
}
After simulation, you can execute:
let result = simulated.execute().await?;
println!("Transaction hash: {:?}", result.transaction_hash);
For read-only operations or testing, you don't need to be an owner:
use alloy::signers::local::PrivateKeySigner;
// Use any signer for simulation
let dummy = PrivateKeySigner::random();
let safe = Safe::new(provider, dummy, safe_address, config);
let builder = safe.multicall()
.add_typed(token, call)
.simulate().await?;
// Inspect results without executing
if let Some(result) = builder.simulation_result() {
println!("Would use {} gas", result.gas_used);
}
let nonce = safe.nonce().await?;
let threshold = safe.threshold().await?;
let owners = safe.owners().await?;
The Eoa client provides the same builder API as Safe multicall, but executes each call as a separate transaction. This is useful when you don't have a Safe but want the same batching workflow:
use safe_rs::Eoa;
let eoa = Eoa::connect(provider, signer).await?;
let result = eoa.batch()
.add_typed(token, IERC20::transferCall { to: alice, amount: U256::from(100) })
.add_typed(token, IERC20::transferCall { to: bob, amount: U256::from(200) })
.simulate().await?
.execute().await?;
println!("Executed {} txs, {} succeeded", result.results.len(), result.success_count);
for tx in &result.results {
println!("Tx {}: {:?}", tx.index, tx.tx_hash);
}
Key differences from Safe mode:
| Aspect | Safe Mode | EOA Mode |
|---|---|---|
| Execution | Single atomic tx via MultiSend | Multiple independent txs |
| Failure | All-or-nothing | Can partially succeed |
| Result | Single TxHash |
Vec<TxHash> |
| DelegateCall | Supported | Not supported |
Partial failure handling:
By default, EOA batch execution stops on the first failure. Use continue_on_failure() to execute all transactions regardless:
let result = eoa.batch()
.add_typed(token, transfer1)
.add_typed(token, transfer2)
.continue_on_failure() // Don't stop on first failure
.simulate().await?
.execute().await?;
if let Some(idx) = result.first_failure {
println!("First failure at index {}", idx);
}
The --bundle option accepts JSON files compatible with the Safe Transaction Bundler format:
[
{
"to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"value": "0",
"data": "0xa9059cbb000000000000000000000000...",
"operation": 0
},
{
"to": "0x6B175474E89094C44Da98b954EescdAD80089fD12",
"data": "0x095ea7b3...",
"operation": 0
}
]
Fields:
to — Target address (required)value — Wei to send (optional, default: "0")data — Calldata hex (optional, default: "0x")operation — 0 for Call, 1 for DelegateCall (optional, default: 0)safe-rs includes pre-configured addresses for Safe v1.4.1 contracts:
| Chain | Chain ID |
|---|---|
| Ethereum | 1 |
| Sepolia | 11155111 |
| Arbitrum | 42161 |
| Optimism | 10 |
| Base | 8453 |
| Polygon | 137 |
| BSC | 56 |
| Avalanche | 43114 |
| Gnosis | 100 |
All chains use the same contract addresses (deployed via CREATE2):
| Contract | Address |
|---|---|
| Safe Singleton | 0x41675C099F32341bf84BFc5382aF534df5C7461a |
| MultiSend | 0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526 |
| MultiSendCallOnly | 0x9641d764fc13c8B624c04430C7356C1C7C8102e2 |
| Proxy Factory | 0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67 |
| Fallback Handler | 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99 |
| Variable | Description |
|---|---|
ETH_RPC_URL |
RPC endpoint URL |
SAFE_ADDRESS |
Default Safe address |
PRIVATE_KEY |
Signer private key |
See the examples/ directory:
simple_transfer.rs — Single ERC20 transfermulticall_erc20.rs — Batch multiple operationssimulation_only.rs — Simulation without executionRun examples:
cargo run --example simple_transfer
vs Safe Transaction Service API: safe-rs executes transactions directly on-chain without relying on Safe's infrastructure. No API keys, no rate limits, no external dependencies.
vs ethers/alloy directly: safe-rs handles the complexity of Safe transaction encoding, EIP-712 signing, gas estimation, and multicall batching. You focus on what you want to do, not how Safe works internally.
vs multi-owner Safes: If you need multiple signers, use the Safe web interface or Transaction Service. safe-rs is intentionally limited to 1/1 Safes for simplicity and reliability.
MIT