diff --git a/Cargo.lock b/Cargo.lock index 3f3fd1896a84d..033c296c16441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,17 @@ dependencies = [ "strum 0.27.2", ] +[[package]] +name = "alloy-chains" +version = "0.2.6" +source = "git+https://github.com/alloy-rs/chains?rev=e5812b8e092801dc9f872ef955b1f071f17e85db#e5812b8e092801dc9f872ef955b1f071f17e85db" +dependencies = [ + "alloy-primitives", + "num_enum", + "serde", + "strum 0.27.2", +] + [[package]] name = "alloy-consensus" version = "1.0.24" @@ -260,7 +271,7 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3165210652f71dfc094b051602bafd691f506c54050a174b1cba18fb5ef706a3" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-eip2124", "alloy-primitives", "auto_impl", @@ -356,7 +367,7 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3417f4187eaf7f7fb0d7556f0197bca26f0b23c4bb3aca0c9d566dc1c5d727a2" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-hardforks", "auto_impl", ] @@ -398,7 +409,7 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478a42fe167057b7b919cd8b0c2844f0247f667473340dad100eaf969de5754e" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-consensus", "alloy-eips", "alloy-json-rpc", @@ -1036,7 +1047,7 @@ dependencies = [ name = "anvil" version = "1.3.1" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-consensus", "alloy-contract", "alloy-dyn-abi", @@ -2514,7 +2525,7 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" name = "cast" version = "1.3.1" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-consensus", "alloy-contract", "alloy-dyn-abi", @@ -3721,6 +3732,19 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "enscribe" +version = "1.3.1" +dependencies = [ + "alloy-chains 0.2.6 (git+https://github.com/alloy-rs/chains?rev=e5812b8e092801dc9f872ef955b1f071f17e85db)", + "alloy-ens", + "alloy-sol-types", + "eyre", + "foundry-cli", + "foundry-common", + "foundry-config", +] + [[package]] name = "enum-ordinalize" version = "4.3.0" @@ -3992,8 +4016,10 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" name = "forge" version = "1.3.1" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "alloy-contract", "alloy-dyn-abi", + "alloy-ens", "alloy-hardforks", "alloy-json-abi", "alloy-network", @@ -4003,6 +4029,7 @@ dependencies = [ "alloy-serde", "alloy-signer", "alloy-signer-local", + "alloy-sol-types", "alloy-transport", "anvil", "axum", @@ -4013,6 +4040,7 @@ dependencies = [ "clearscreen", "comfy-table", "dunce", + "enscribe", "evm-disassembler", "eyre", "forge-doc", @@ -4023,6 +4051,7 @@ dependencies = [ "forge-sol-macro-gen", "forge-verify", "foundry-block-explorers", + "foundry-cheatcodes-spec", "foundry-cli", "foundry-common", "foundry-compilers", @@ -4132,7 +4161,7 @@ dependencies = [ name = "forge-script" version = "1.3.1" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-consensus", "alloy-dyn-abi", "alloy-eips", @@ -4146,6 +4175,7 @@ dependencies = [ "clap", "dialoguer", "dunce", + "enscribe", "eyre", "forge-script-sequence", "forge-verify", @@ -4275,7 +4305,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc107bbc3b4480995fdf337ca0ddedc631728175f418d3136ead9df8f4dc465e" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-json-abi", "alloy-primitives", "foundry-compilers", @@ -4291,7 +4321,7 @@ dependencies = [ name = "foundry-cheatcodes" version = "1.3.1" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-consensus", "alloy-dyn-abi", "alloy-ens", @@ -4352,7 +4382,7 @@ dependencies = [ name = "foundry-cli" version = "1.3.1" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-dyn-abi", "alloy-eips", "alloy-ens", @@ -4583,7 +4613,7 @@ dependencies = [ name = "foundry-config" version = "1.3.1" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-primitives", "clap", "dirs", @@ -4683,7 +4713,7 @@ dependencies = [ name = "foundry-evm-core" version = "1.3.1" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-consensus", "alloy-dyn-abi", "alloy-evm", @@ -4791,7 +4821,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bdf390c3633b0eb14c6bb26a0aeb63ea0200f1350ccbe07493f23148f58c4a5" dependencies = [ - "alloy-chains", + "alloy-chains 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-consensus", "alloy-hardforks", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 659de06813b3a..5bbd895c851af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "crates/forge/", "crates/script-sequence/", "crates/macros/", + "crates/enscribe/", "crates/test-utils/", "crates/lint/", ] @@ -202,6 +203,8 @@ foundry-test-utils = { path = "crates/test-utils" } foundry-wallets = { path = "crates/wallets" } foundry-linking = { path = "crates/linking" } +enscribe = { path = "crates/enscribe" } + # solc & compilation utilities foundry-block-explorers = { version = "0.20.0", default-features = false } foundry-compilers = { version = "0.18.2", default-features = false } diff --git a/crates/enscribe/Cargo.toml b/crates/enscribe/Cargo.toml new file mode 100644 index 0000000000000..cf822daf12048 --- /dev/null +++ b/crates/enscribe/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "enscribe" +description = "setting ens names for smart contracts" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +eyre.workspace = true + +foundry-cli.workspace = true +foundry-common.workspace = true +foundry-config.workspace = true + +alloy-ens.workspace = true +alloy-chains = { git = "https://github.com/alloy-rs/chains", rev = "e5812b8e092801dc9f872ef955b1f071f17e85db"} +alloy-sol-types.workspace = true \ No newline at end of file diff --git a/crates/enscribe/src/abi.rs b/crates/enscribe/src/abi.rs new file mode 100644 index 0000000000000..b72c1926f0595 --- /dev/null +++ b/crates/enscribe/src/abi.rs @@ -0,0 +1,67 @@ +use alloy_sol_types::sol; +sol! { + /// ENS Registry contract. + #[sol(rpc)] + contract EnsRegistry { + /// Sets subnode record + function setSubnodeRecord( + bytes32 node, + bytes32 label, + address owner, + address resolver, + uint64 ttl + ) external; + + /// checks if an ens name record already exists + function recordExists(bytes32 node) external returns (bool); + /// returns the owner of this node + function owner(bytes32 node) external returns (address); + } + + /// ENS Name Wrapper contract + #[sol(rpc)] + contract NameWrapper { + function isWrapped(bytes32 node) external returns (bool); + function setSubnodeRecord( + bytes32 node, + string label, + address owner, + address resolver, + uint64 ttl, + uint32 fuses, + uint64 expiry, + ) external; + } + + /// ENS Public Resolver contract + #[sol(rpc)] + contract PublicResolver { + function setAddr(bytes32 node, address addr) external; + function addr(bytes32 node) external returns (address); + function setName(bytes32 node, string newName) external; + } + + /// ENS Reverse Registrar contract + #[sol(rpc)] + contract ReverseRegistrar { + function setName(string memory name) external returns (bytes32); + function setNameForAddr(address addr, address owner, address resolver, string name) external; + } + + /// Enscribe contract + #[sol(rpc)] + contract Enscribe { + function setName( + address contractAddress, + string label, + string parentName, + bytes32 parentNode + ) external returns (bool success); + } + + /// Ownable contract + #[sol(rpc)] + contract Ownable { + function owner() external returns (address); + } +} diff --git a/crates/enscribe/src/lib.rs b/crates/enscribe/src/lib.rs new file mode 100644 index 0000000000000..3b4709aa4771e --- /dev/null +++ b/crates/enscribe/src/lib.rs @@ -0,0 +1,7 @@ +//! enscribe does ENS name setting for contracts. + +#[allow(clippy::too_many_arguments)] +mod abi; +pub mod name; + +pub use name::set_primary_name; diff --git a/crates/enscribe/src/name.rs b/crates/enscribe/src/name.rs new file mode 100644 index 0000000000000..3e65acbfeea48 --- /dev/null +++ b/crates/enscribe/src/name.rs @@ -0,0 +1,279 @@ +use crate::abi::{ + EnsRegistry, EnsRegistry::recordExistsCall, NameWrapper, NameWrapper::isWrappedCall, Ownable, + Ownable::ownerCall, PublicResolver, PublicResolver::addrCall, ReverseRegistrar, +}; +use alloy_chains::NamedChain; +use alloy_ens::{ProviderEnsExt, namehash}; +use alloy_provider::{ + Provider, ProviderBuilder, WalletProvider, + network::{AnyNetwork, EthereumWallet}, +}; +use alloy_sol_types::{ + SolCall, + private::{Address, B256, keccak256}, +}; +use eyre::Result; +use foundry_cli::utils; +use foundry_common::sh_println; +use foundry_config::Config; + +pub async fn set_primary_name( + config: &Config, + wallet: EthereumWallet, + contract_addr: Address, + name: String, + _is_reverse_setter: bool, +) -> Result<()> { + let provider = utils::get_provider(config)?; + let provider = ProviderBuilder::<_, _, AnyNetwork>::default() + .with_recommended_fillers() + .wallet(wallet) + .connect_provider(provider); + + let sender_addr = provider.default_signer_address(); + let chain_id = provider.get_chain_id().await?; + let ens_registry_addr: Address = alloy_ens::ENS_ADDRESS; + let reverse_registrar_addr: Address = get_reverse_registrar(&provider).await?; + let public_resolver_addr: Address = get_public_resolver(&provider, &name).await?; + let name_wrapper_addr: Address = get_name_wrapper(chain_id).await?; + let is_ownable = is_contract_ownable(&provider, contract_addr).await; + let is_reverse_claimer = + is_contract_reverse_claimer(&provider, contract_addr, sender_addr, ens_registry_addr) + .await?; + + // we can't name a contract that isn't Ownable or ReverseClaimer + if !is_ownable && !is_reverse_claimer { + sh_println!("Contract doesn't seem to implement Ownable or ReverseClaimer interfaces.")?; + return Ok(()); + } + + let contract_type = if is_ownable { "Ownable" } else { "ReverseClaimer" }.to_owned(); + sh_println!("Contract is {contract_type} contract.")?; + + let name_splits = name.split('.').collect::>(); + let label = name_splits[0]; + let parent = name_splits[1]; + let tld = name_splits[2]; + + let parent_name = format!("{parent}.{tld}"); + let parent_name_hash = namehash(&parent_name); + let label_hash = keccak256(label); + let complete_name_hash = namehash(&name); + + if !name_already_registered(&provider, complete_name_hash, ens_registry_addr).await? { + create_subname( + &provider, + sender_addr, + ens_registry_addr, + public_resolver_addr, + name_wrapper_addr, + label, + parent_name_hash, + label_hash, + ) + .await?; + } + + set_resolutions( + &provider, + public_resolver_addr, + complete_name_hash, + name.clone(), + contract_addr, + is_reverse_claimer, + sender_addr, + reverse_registrar_addr, + ) + .await?; + + sh_println!()?; + sh_println!("✨ Contract named: https://app.enscribe.xyz/explore/{chain_id}/{contract_addr}")?; + + Ok(()) +} + +/// checks if the given contract address implements Ownable +async fn is_contract_ownable>( + provider: &P, + contract_addr: Address, +) -> bool { + let ownable = Ownable::new(contract_addr, provider); + let tx = ownable.owner(); + provider.call(tx.into_transaction_request()).await.is_ok() +} + +/// gets the ENS reverse registrar address from the ENS registry +async fn get_reverse_registrar + ProviderEnsExt>( + provider: &P, +) -> Result
{ + let rev_registrar_addr = provider.get_reverse_registrar().await?; + Ok(*rev_registrar_addr.address()) +} + +async fn get_public_resolver + ProviderEnsExt>( + provider: &P, + name: &str, +) -> Result
{ + if let Some(domain) = name.split_once('.').map(|x| x.1) { + let domain_hash = namehash(domain); + let resolver_addr = provider.get_resolver(domain_hash, domain).await?; + Ok(*resolver_addr.address()) + } else { + Err(eyre::eyre!("invalid name: {name}")) + } +} + +/// checks if the given contract address implements Ownable +async fn is_contract_reverse_claimer>( + provider: &P, + contract_addr: Address, + sender_addr: Address, + ens_registry_addr: Address, +) -> Result { + let addr = &(&contract_addr.to_string().to_ascii_lowercase())[2..]; + let reverse_node = namehash(&format!("{addr}.addr.reverse")); + let ens_registry = EnsRegistry::new(ens_registry_addr, provider); + let tx = ens_registry.owner(reverse_node); + let result = provider.call(tx.into_transaction_request()).await?; + let addr = ownerCall::abi_decode_returns(&result)?; + Ok(addr == sender_addr) +} + +/// probes the ens registry to check if the given `name` is already registered on the chain +async fn name_already_registered>( + provider: &P, + name: B256, + ens_registry_addr: Address, +) -> Result { + let ens_registry = EnsRegistry::new(ens_registry_addr, provider); + let tx = ens_registry.recordExists(name); + let result = provider.call(tx.into_transaction_request()).await?; + let is_name_exists = recordExistsCall::abi_decode_returns(&result)?; + Ok(is_name_exists) +} + +/// creates the subname record +#[expect(clippy::too_many_arguments)] +async fn create_subname>( + provider: &P, + sender_addr: Address, + ens_registry_addr: Address, + public_resolver_addr: Address, + name_wrapper_addr: Address, + label: &str, + parent_name_hash: B256, + label_hash: B256, +) -> Result<()> { + // for Base chains, handle subname creation differently + let chain_id = provider.get_chain_id().await?; + if chain_id == NamedChain::Base as u64 || chain_id == NamedChain::BaseSepolia as u64 { + let ens_registry = EnsRegistry::new(ens_registry_addr, provider); + let tx = ens_registry.setSubnodeRecord( + parent_name_hash, + label_hash, + sender_addr, + public_resolver_addr, + 0, + ); + let result = + provider.send_transaction(tx.into_transaction_request()).await?.watch().await?; + sh_println!("done (txn hash: {:?})", result)?; + return Ok(()); + } + + // check if parent domain (e.g. abhi.eth) is wrapped or unwrapped + let name_wrapper = NameWrapper::new(name_wrapper_addr, provider); + let tx = name_wrapper.isWrapped(parent_name_hash); + let result = provider.call(tx.into_transaction_request()).await?; + let is_wrapped = isWrappedCall::abi_decode_returns(&result)?; + sh_println!("creating subname ...")?; + if is_wrapped { + let tx = name_wrapper.setSubnodeRecord( + parent_name_hash, + label.to_owned(), + sender_addr, + public_resolver_addr, + 0, + 0, + 0, + ); + let result = + provider.send_transaction(tx.into_transaction_request()).await?.watch().await?; + sh_println!("done (txn hash: {:?})", result)?; + } else { + let ens_registry = EnsRegistry::new(ens_registry_addr, provider); + let tx = ens_registry.setSubnodeRecord( + parent_name_hash, + label_hash, + sender_addr, + public_resolver_addr, + 0, + ); + let result = + provider.send_transaction(tx.into_transaction_request()).await?.watch().await?; + sh_println!("done (txn hash: {:?})", result)?; + } + Ok(()) +} + +/// sets forward & reverse resolutions +#[expect(clippy::too_many_arguments)] +async fn set_resolutions>( + provider: &P, + public_resolver_addr: Address, + complete_name_hash: B256, + name: String, + contract_addr: Address, + is_reverse_claimer: bool, + sender_addr: Address, + reverse_registrar_addr: Address, +) -> Result<()> { + sh_println!("checking if fwd resolution already set ...")?; + let public_resolver = PublicResolver::new(public_resolver_addr, provider); + let tx = public_resolver.addr(complete_name_hash); + let result = provider.call(tx.into_transaction_request()).await?; + let result = addrCall::abi_decode_returns(&result)?; + + if result == Address::ZERO { + sh_println!("setting fwd resolution ({} -> {}) ...", name, contract_addr)?; + let tx = public_resolver.setAddr(complete_name_hash, contract_addr); + let result = + provider.send_transaction(tx.into_transaction_request()).await?.watch().await?; + sh_println!("done (txn hash: {:?})", result)?; + } else { + sh_println!("fwd resolution already set")?; + } + + sh_println!("setting rev resolution ({} -> {}) ...", contract_addr, name)?; + if is_reverse_claimer { + let addr = &(&sender_addr.to_string().to_ascii_lowercase())[2..]; + let reverse_node = namehash(&format!("{addr}.addr.reverse")); + let tx = public_resolver.setName(reverse_node, name); + let result = + provider.send_transaction(tx.into_transaction_request()).await?.watch().await?; + sh_println!("done (txn hash: {:?})", result)?; + } else { + let reverse_registrar = ReverseRegistrar::new(reverse_registrar_addr, provider); + let tx = reverse_registrar.setNameForAddr( + contract_addr, + sender_addr, + public_resolver_addr, + name, + ); + let result = + provider.send_transaction(tx.into_transaction_request()).await?.watch().await?; + sh_println!("done (txn hash: {:?})", result)?; + } + + Ok(()) +} + +/// fetches the chain config for `chaind_id` from the Enscribe API +async fn get_name_wrapper(chain_id: u64) -> Result
{ + let chain = NamedChain::try_from(chain_id)?; + let name_wrapper_addr = chain + .name_wrapper_address() + .ok_or_else(|| eyre::eyre!("name wrapper address not found"))?; + + Ok(name_wrapper_addr) +} diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index f1fdb11d4aa7d..92c474a8af9ba 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -31,6 +31,7 @@ foundry-config.workspace = true foundry-evm.workspace = true foundry-evm-core.workspace = true foundry-linking.workspace = true +enscribe.workspace = true comfy-table.workspace = true eyre.workspace = true @@ -102,8 +103,12 @@ quick-junit = "0.5.1" [dev-dependencies] alloy-hardforks.workspace = true anvil.workspace = true +foundry-cheatcodes-spec.workspace = true forge-script-sequence.workspace = true foundry-test-utils.workspace = true +alloy-contract.workspace = true +alloy-ens.workspace = true +alloy-sol-types.workspace = true foundry-wallets.workspace = true futures.workspace = true reqwest = { workspace = true, features = ["json"] } diff --git a/crates/forge/src/args.rs b/crates/forge/src/args.rs index de7087cecb904..b95081a9a16fd 100644 --- a/crates/forge/src/args.rs +++ b/crates/forge/src/args.rs @@ -88,6 +88,7 @@ pub fn run_command(args: Forge) -> Result<()> { CacheSubcommands::Ls(cmd) => cmd.run(), }, ForgeSubcommand::Create(cmd) => global.block_on(cmd.run()), + ForgeSubcommand::Name(cmd) => global.block_on(cmd.run()), ForgeSubcommand::Update(cmd) => cmd.run(), ForgeSubcommand::Install(cmd) => cmd.run(), ForgeSubcommand::Remove(cmd) => cmd.run(), diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index f12099ac51386..f858a8cf831ed 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -84,6 +84,16 @@ pub struct CreateArgs { #[arg(long, env = "ETH_TIMEOUT")] pub timeout: Option, + // #[command(flatten)] + // pub naming: NameArgs, + /// The ENS name to set for the contract. + #[arg(long)] + pub ens_name: String, + + /// Whether the contract is ReverseSetter or not. + #[arg(long, requires = "ens_name")] + pub reverse_setter: bool, + #[command(flatten)] build: BuildOpts, @@ -400,6 +410,19 @@ impl CreateArgs { sh_println!("Transaction hash: {:?}", receipt.transaction_hash)?; }; + if !self.ens_name.is_empty() { + let config = self.load_config()?; + let signer = self.eth.wallet.signer().await?; + enscribe::set_primary_name( + &config, + EthereumWallet::new(signer), + deployed_contract, + self.ens_name, + self.reverse_setter, + ) + .await?; + } + if !self.verify { return Ok(()); } @@ -685,4 +708,16 @@ mod tests { let params = args.parse_constructor_args(&constructor, &args.constructor_args).unwrap(); assert_eq!(params, vec![DynSolValue::Int(I256::unchecked_from(-5), 256)]); } + + #[test] + fn can_parse_ens_name() { + let args: CreateArgs = CreateArgs::parse_from([ + "foundry-cli", + "src/Domains.sol:Domains", + "--ens-name", + "test.abhi.eth", + ]); + + assert_eq!(args.ens_name, "test.abhi.eth".to_owned()); + } } diff --git a/crates/forge/src/cmd/mod.rs b/crates/forge/src/cmd/mod.rs index 0a0945bab99e9..b8c555718b5ba 100644 --- a/crates/forge/src/cmd/mod.rs +++ b/crates/forge/src/cmd/mod.rs @@ -24,6 +24,7 @@ pub mod init; pub mod inspect; pub mod install; pub mod lint; +pub mod name; pub mod remappings; pub mod remove; pub mod selectors; diff --git a/crates/forge/src/cmd/name.rs b/crates/forge/src/cmd/name.rs new file mode 100644 index 0000000000000..484ecb849f5ec --- /dev/null +++ b/crates/forge/src/cmd/name.rs @@ -0,0 +1,79 @@ +use alloy_network::EthereumWallet; +use alloy_primitives::Address; +use clap::Parser; +use foundry_cli::{opts::EthereumOpts, utils::LoadConfig}; +use foundry_config::{ + Config, figment, + figment::{ + Metadata, Profile, + value::{Dict, Map}, + }, + merge_impl_figment_convert, +}; + +merge_impl_figment_convert!(NameArgs, eth); + +/// CLI arguments for `forge name`. +#[derive(Clone, Debug, Parser)] +pub struct NameArgs { + /// The ENS name to set. + #[arg(long)] + pub ens_name: String, + + /// The address of the contract. + #[arg(long)] + pub contract_address: Address, + + #[command(flatten)] + eth: EthereumOpts, +} + +impl NameArgs { + pub async fn run(self) -> eyre::Result<()> { + let config = self.load_config()?; + let signer = self.eth.wallet.signer().await?; + + enscribe::set_primary_name( + &config, + EthereumWallet::new(signer), + self.contract_address, + self.ens_name, + false, + ) + .await?; + Ok(()) + } +} + +impl figment::Provider for NameArgs { + fn metadata(&self) -> Metadata { + Metadata::named("Name Args Provider") + } + + fn data(&self) -> eyre::Result, figment::Error> { + let dict = Dict::default(); + Ok(Map::from([(Config::selected_profile(), dict)])) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_parse_contract_and_name_args() { + let args: NameArgs = NameArgs::parse_from([ + "foundry-cli", + "--contract-address", + "0x3fAB184622Dc19b6109349B94811493BF2a45362", + "--ens-name", + "test.abhi.eth", + ]); + + assert_eq!( + args.contract_address, + "0x3fAB184622Dc19b6109349B94811493BF2a45362".parse::
().unwrap() + ); + assert_eq!(args.ens_name, "test.abhi.eth".to_owned()); + } +} diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index 60e7646814bb6..95de6dab322d2 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -53,6 +53,7 @@ use yansi::Paint; mod filter; mod summary; + use crate::{result::TestKind, traces::render_trace_arena_inner}; pub use filter::FilterArgs; use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite}; diff --git a/crates/forge/src/opts.rs b/crates/forge/src/opts.rs index 83939781dce94..01261f1eb5da6 100644 --- a/crates/forge/src/opts.rs +++ b/crates/forge/src/opts.rs @@ -2,8 +2,8 @@ use crate::cmd::{ bind::BindArgs, bind_json, build::BuildArgs, cache::CacheArgs, clone::CloneArgs, compiler::CompilerArgs, config, coverage, create::CreateArgs, doc::DocArgs, eip712, flatten, fmt::FmtArgs, geiger, generate, init::InitArgs, inspect, install::InstallArgs, lint::LintArgs, - remappings::RemappingArgs, remove::RemoveArgs, selectors::SelectorsSubcommands, snapshot, - soldeer, test, tree, update, + name::NameArgs, remappings::RemappingArgs, remove::RemoveArgs, selectors::SelectorsSubcommands, + snapshot, soldeer, test, tree, update, }; use clap::{Parser, Subcommand, ValueHint}; use forge_script::ScriptArgs; @@ -89,6 +89,10 @@ pub enum ForgeSubcommand { #[command(visible_alias = "c")] Create(CreateArgs), + /// Set an ENS name for a smart contract. + #[command(visible_alias = "n")] + Name(NameArgs), + /// Create a new Forge project. Init(InitArgs), diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index b258da28d02f4..c33642b7fd4be 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -14,6 +14,7 @@ repository.workspace = true workspace = true [dependencies] +enscribe.workspace = true forge-verify.workspace = true foundry-cli.workspace = true foundry-config.workspace = true diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 82fa3ff6eff55..f6641ec40e340 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -210,6 +210,14 @@ pub struct ScriptArgs { #[arg(long, env = "ETH_TIMEOUT")] pub timeout: Option, + /// The ens name to set for the contract. + #[arg(long)] + pub ens_name: Option, + + /// Whether the contract is ReverseSetter or not. + #[arg(long, requires = "ens_name")] + pub reverse_setter: bool, + #[command(flatten)] pub build: BuildOpts, @@ -245,6 +253,7 @@ impl ScriptArgs { pub async fn run_script(self) -> Result<()> { trace!(target: "script", "executing script command"); + let config = self.load_config()?; let state = self.preprocess().await?; let create2_deployer = state.script_config.evm_opts.create2_deployer; let compiled = state.compile()?; @@ -333,7 +342,12 @@ impl ScriptArgs { } // Wait for pending txes and broadcast others. - let broadcasted = bundled.wait_for_pending().await?.broadcast().await?; + let mut broadcasted = bundled.wait_for_pending().await?.broadcast().await?; + + // check if we have to set a name for the deployed contract + if broadcasted.args.ens_name.is_some() { + broadcasted.set_ens_name(&config).await?; + } if broadcasted.args.verify { broadcasted.verify().await?; @@ -776,6 +790,18 @@ mod tests { assert_eq!(args.evm.env.code_size_limit, Some(50000)); } + #[test] + fn can_extract_ens_name() { + let args = ScriptArgs::parse_from([ + "foundry-cli", + "script", + "script/Test.s.sol:TestScript", + "--ens-name", + "test.abhi.eth", + ]); + assert_eq!(args.ens_name, Some("test.abhi.eth".to_owned())); + } + #[test] fn can_extract_script_etherscan_key() { let temp = tempdir().unwrap(); diff --git a/crates/script/src/verify.rs b/crates/script/src/verify.rs index 76ab6b4bc3917..2319916493a95 100644 --- a/crates/script/src/verify.rs +++ b/crates/script/src/verify.rs @@ -3,6 +3,7 @@ use crate::{ build::LinkedBuildData, sequence::{ScriptSequenceKind, get_commit_hash}, }; +use alloy_network::EthereumWallet; use alloy_primitives::{Address, hex}; use eyre::{Result, eyre}; use forge_script_sequence::{AdditionalContract, ScriptSequence}; @@ -41,6 +42,40 @@ impl BroadcastedState { Ok(()) } + + /// sets an ENS name for the deployed contract + pub async fn set_ens_name(&mut self, config: &Config) -> Result<()> { + let contract_deployment_receipt = self + .sequence + .sequences() + .iter() + .filter_map(|s| s.receipts.iter().find(|r| r.contract_address.is_some())) + .next(); + + if let Some(receipt) = contract_deployment_receipt { + let signers = self.args.wallets.get_multi_wallet().await?.into_signers()?; + // todo abhi: simplify this instead of checking key via signers.filter() ... + if signers.contains_key(&receipt.from) { + let wallet = signers + .into_iter() + .filter(|(addr, _)| *addr == receipt.from) + .map(|(_, signer)| EthereumWallet::new(signer)) + .next() + .unwrap(); + + enscribe::set_primary_name( + config, + wallet, + receipt.contract_address.unwrap(), + self.args.ens_name.clone().unwrap(), + self.args.reverse_setter, + ) + .await?; + } + } + + Ok(()) + } } /// Data struct to help `ScriptSequence` verify contracts on `etherscan`.