Experimental Ethereum CLI built on Foundry and Alloy.
Right now the main implemented command is:
ethx send— acast send-like transaction sender
It also supports wrapping calls through a smart-account encoder, currently:
safe
Additional Safe convenience commands include:
ethx safe execute-deployment— execute CALL transactions from a Foundry broadcast JSON through a Safeethx safe queue— encode one call and queue it in the Safe Transaction Serviceethx safe queue-deployment— sign and queue Foundry broadcast JSON transactions in the Safe Transaction Service
ethx send \
--rpc-url https://rpc.example \
--private-key <KEY> \
0xTarget \
"transfer(address,uint256)" \
0xRecipient \
1000000000000000000ethx send \
--rpc-url https://rpc.example \
--private-key <KEY> \
--create \
0x6080...ethx safe execute-deployment \
--rpc-url https://rpc.example \
--private-key <OWNER_KEY> \
--safe 0xSafe \
broadcast/MyScript.s.sol/1/run-latest.jsonIf the JSON contains one CALL transaction, ethx executes it directly through the Safe. If it
contains multiple CALL transactions, ethx batches them with Safe MultiSend and executes the
MultiSend call via DELEGATECALL. Contract deployment transactions (CREATE/CREATE2) are not
supported by this command.
Use --multi-send <ADDRESS> to override the default canonical MultiSend address.
For multisig Safes, encode a call and queue it offchain instead of broadcasting immediately:
ethx safe queue \
--rpc-url https://rpc.example \
--private-key <OWNER_KEY> \
--safe 0xSafe \
0xInnerTarget \
"transfer(address,uint256)" \
0xRecipient \
1000000000000000000Use --value <VALUE> to attach ETH value to the inner Safe transaction.
For multisig Safes, queue the Safe transaction offchain instead of broadcasting immediately:
ethx safe queue-deployment \
--rpc-url https://rpc.example \
--private-key <OWNER_KEY> \
--safe 0xSafe \
broadcast/MyScript.s.sol/1/run-latest.jsonethx asks the Safe Transaction Service for trusted pending transactions and queues at the next
available nonce unless --safe-nonce is provided. It signs the Safe transaction hash with the local
owner key and submits the proposal to the Safe Transaction Service. --safe-api-url <URL> can
override the default API URL; defaults are known for common Safe-supported chains such as Ethereum, Sepolia, Optimism, Arbitrum, Base,
Polygon, Gnosis Chain, BNB Chain, Avalanche, zkSync Era, Linea, Celo, Scroll, Sonic, and Unichain.
This uses the encoder abstraction to wrap the inner call into Safe execTransaction(...) calldata
and send the outer transaction to the Safe itself.
ethx send \
--rpc-url https://rpc.example \
--private-key <OWNER_KEY> \
--encoder safe \
--target 0xSafe \
--safe-eoa-signature 0x<owner_sig> \
0xInnerTarget \
"transfer(address,uint256)" \
0xRecipient \
1etherYou can also supply other Safe signature forms:
--safe-contract-signature 0xOwnerContract:0x<1271_sig>
--safe-approved-hash 0xOwnerMeaning of addresses:
TOpositional argument: inner call destination--target: Safe address
ethx send can either send a transaction directly, or first pass the requested call through an
encoder.
Without an encoder:
- the positional
TOaddress is the transaction recipient - the parsed
sig + argsbecome the final calldata
With an encoder:
- the positional
TOaddress is the inner call destination - the parsed
sig + argsbecome the inner calldata --targetis the outer account/contract that actually receives the transaction- the selected encoder transforms that inner call into the outer calldata expected by the target
This is meant to support smart-account style flows where the final onchain transaction is sent to an account contract, but still describes some inner call to be executed by that account.
Today the only implemented encoder is safe, which wraps the inner call into Safe
execTransaction(...) calldata.
From an implementation perspective, the abstraction is centered on:
RawCall: a normalized inner call shape (to,value,data)CallEncoder: a trait that transforms aRawCallinto anotherRawCallCallEncoder::EncodeContext: encoder-specific extra state needed during encoding
That lets each encoder decide:
- how the outer calldata should be built
- whether the outer recipient should differ from the inner recipient
- what extra preparation/validation is needed before encoding
The Safe implementation uses this to attach Safe-specific context such as:
- operation mode
- Safe gas/refund fields
- structured signature inputs
- prepared packed signature bytes
The intent is that other smart-account/account-abstraction styles can be added later by:
- defining their own context type
- implementing
CallEncoderfor their wrapper format - adding CLI parsing/mapping for their encoder-specific options
This is an experiment, not a polished replacement for cast.
Implemented today:
- Foundry broadcast JSON CALL execution through Safe, with MultiSend batching for multiple calls
- Foundry-style function signature + args parsing via
foundry_cli::utils::parse_function_args - Foundry wallet resolution via
foundry-wallets - regular call sending
- contract creation via
--create - unlocked sending via
eth_sendTransaction - signer-based sending
- Safe
execTransaction(...)wrapping - Safe tx hash derivation for:
1.0.01.1.11.2.01.3.01.4.11.5.0
- structured Safe signature inputs:
- EOA signatures
- EIP-1271 contract signatures
- approved-hash signatures
- Safe owner/threshold validation and optional auto-add of the current signer signature
- Safe packed signature encoding, including dynamic offsets for contract signatures
nix-shell -p cargo rustfmt clippy --run 'cargo build'cargo buildWith Nix flakes:
nix run . -- --help
nix run . -- send --helpWith Cargo:
cargo run -- --help
cargo run -- send --helpFor the Safe encoder, ethx currently:
- queries Safe metadata:
VERSION()nonce()getThreshold()getOwners()- batching via multicall when available, with fallback to direct calls
- derives the Safe tx signing hash locally
- supports these input forms:
--safe-eoa-signature <SIG>--safe-contract-signature <OWNER>:<SIG>--safe-approved-hash <OWNER>
- validates EOA signatures by recovering the owner from the Safe tx hash
- validates EIP-1271 signatures by calling:
owner.isValidSignature(safeTxHash, signature)
- validates approved-hash signatures by checking:
- executor is the owner, or
approvedHashes(owner, safeTxHash) != 0
- verifies referenced/recovered owners are actual Safe owners
- sorts signatures by owner address ascending
- checks threshold early
- if the provided owner signatures are below threshold and a local signer is available, signs the Safe tx hash with the current signer and appends that signature when appropriate
- encodes the final Safe packed signatures payload, including contract-signature offsets and dynamic tails
Supported today:
- standard recoverable 65-byte EOA signatures
- EIP-1271 contract signatures
- approved-hash signatures
Not supported yet:
- Safe
v == 2P-256 / RIP-7212-style signatures
- The local variable named
ens_chainin the code is currently only used for Foundry parsing / resolution behavior, not as the tx chain id. - Transaction filling is still mostly explicit/manual at the moment.
Useful commands:
cargo fmt --all
cargo test
cargo check
cargo clippy --all-targets --all-features -- -D warnings