diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 8c0181367b70..565cd6eda7a8 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -11,7 +11,7 @@ use eyre::Result; use foundry_cli::{handler, prompt, stdin, utils}; use foundry_common::{ abi::get_event, - ens::ProviderEnsExt, + ens::{namehash, ProviderEnsExt}, fmt::{format_tokens, format_uint_exp}, fs, selectors::{ @@ -446,19 +446,19 @@ async fn main() -> Result<()> { // ENS CastSubcommand::Namehash { name } => { let name = stdin::unwrap_line(name)?; - println!("{}", SimpleCast::namehash(&name)?); + println!("{}", namehash(&name)); } CastSubcommand::LookupAddress { who, rpc, verify } => { let config = Config::from(&rpc); let provider = utils::get_provider(&config)?; let who = stdin::unwrap_line(who)?; - let name = provider.lookup_address(who).await?; + let name = provider.lookup_address(&who).await?; if verify { let address = provider.resolve_name(&name).await?; eyre::ensure!( address == who, - "Forward lookup verification failed: got `{name:?}`, expected `{who:?}`" + "Reverse lookup verification failed: got `{address}`, expected `{who}`" ); } println!("{name}"); @@ -470,13 +470,13 @@ async fn main() -> Result<()> { let who = stdin::unwrap_line(who)?; let address = provider.resolve_name(&who).await?; if verify { - let name = provider.lookup_address(address).await?; - assert_eq!( - name, who, - "forward lookup verification failed. got {name}, expected {who}" + let name = provider.lookup_address(&address).await?; + eyre::ensure!( + name == who, + "Forward lookup verification failed: got `{name}`, expected `{who}`" ); } - println!("{}", address.to_checksum(None)); + println!("{address}"); } // Misc diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index 338510eb9419..beb5e4dc3d6e 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -1676,53 +1676,6 @@ impl SimpleCast { Ok(location.to_string()) } - /// Converts ENS names to their namehash representation - /// [Namehash reference](https://docs.ens.domains/contract-api-reference/name-processing#hashing-names) - /// [namehash-rust reference](https://github.com/InstateDev/namehash-rust/blob/master/src/lib.rs) - /// - /// # Example - /// - /// ``` - /// use cast::SimpleCast as Cast; - /// - /// assert_eq!( - /// Cast::namehash("")?, - /// "0x0000000000000000000000000000000000000000000000000000000000000000" - /// ); - /// assert_eq!( - /// Cast::namehash("eth")?, - /// "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae" - /// ); - /// assert_eq!( - /// Cast::namehash("foo.eth")?, - /// "0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f" - /// ); - /// assert_eq!( - /// Cast::namehash("sub.foo.eth")?, - /// "0x500d86f9e663479e5aaa6e99276e55fc139c597211ee47d17e1e92da16a83402" - /// ); - /// # Ok::<_, eyre::Report>(()) - /// ``` - pub fn namehash(ens: &str) -> Result { - let mut node = vec![0u8; 32]; - - if !ens.is_empty() { - let ens_lower = ens.to_lowercase(); - let mut labels: Vec<&str> = ens_lower.split('.').collect(); - labels.reverse(); - - for label in labels { - let mut label_hash = keccak256(label.as_bytes()); - node.append(&mut label_hash.to_vec()); - - label_hash = keccak256(node.as_slice()); - node = label_hash.to_vec(); - } - } - - Ok(hex::encode_prefixed(node)) - } - /// Keccak-256 hashes arbitrary data /// /// # Example diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index bdc17a2fa910..c6f2b0450123 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1,5 +1,6 @@ //! Contains various tests for checking cast commands +use alloy_primitives::{address, b256, Address, B256}; use foundry_test_utils::{ casttest, rpc::{next_http_rpc_endpoint, next_ws_rpc_endpoint}, @@ -794,3 +795,36 @@ interface Interface { }"#; assert_eq!(output.trim(), s); }); + +const ENS_NAME: &str = "emo.eth"; +const ENS_NAMEHASH: B256 = + b256!("0a21aaf2f6414aa664deb341d1114351fdb023cad07bf53b28e57c26db681910"); +const ENS_ADDRESS: Address = address!("28679A1a632125fbBf7A68d850E50623194A709E"); + +casttest!(ens_namehash, |_prj, cmd| { + cmd.args(["namehash", ENS_NAME]); + let out = cmd.stdout_lossy().trim().parse::(); + assert_eq!(out, Ok(ENS_NAMEHASH)); +}); + +casttest!(ens_lookup, |_prj, cmd| { + let eth_rpc_url = next_http_rpc_endpoint(); + cmd.args(["lookup-address", &ENS_ADDRESS.to_string(), "--rpc-url", ð_rpc_url, "--verify"]); + let out = cmd.stdout_lossy(); + assert_eq!(out.trim(), ENS_NAME); +}); + +casttest!(ens_resolve, |_prj, cmd| { + let eth_rpc_url = next_http_rpc_endpoint(); + cmd.args(["resolve-name", ENS_NAME, "--rpc-url", ð_rpc_url, "--verify"]); + let out = cmd.stdout_lossy().trim().parse::
(); + assert_eq!(out, Ok(ENS_ADDRESS)); +}); + +casttest!(ens_resolve_no_dot_eth, |_prj, cmd| { + let eth_rpc_url = next_http_rpc_endpoint(); + let name = ENS_NAME.strip_suffix(".eth").unwrap(); + cmd.args(["resolve-name", name, "--rpc-url", ð_rpc_url, "--verify"]); + let (_out, err) = cmd.unchecked_output_lossy(); + assert!(err.contains("not found"), "{err:?}"); +}); diff --git a/crates/common/src/ens.rs b/crates/common/src/ens.rs index cb1058384131..131980f55fa8 100644 --- a/crates/common/src/ens.rs +++ b/crates/common/src/ens.rs @@ -1,30 +1,31 @@ //! ENS Name resolving utilities. + #![allow(missing_docs)] -use alloy_primitives::{address, keccak256, Address, B256}; + +use self::EnsResolver::EnsResolverInstance; +use alloy_primitives::{address, Address, Keccak256, B256}; use alloy_provider::{Network, Provider}; use alloy_sol_types::sol; use alloy_transport::Transport; use async_trait::async_trait; -use std::str::FromStr; - -use self::EnsResolver::EnsResolverInstance; +use std::{borrow::Cow, str::FromStr}; // ENS Registry and Resolver contracts. sol! { + /// ENS Registry contract. #[sol(rpc)] - // ENS Registry contract. contract EnsRegistry { /// Returns the resolver for the specified node. function resolver(bytes32 node) view returns (address); } + /// ENS Resolver interface. #[sol(rpc)] - // ENS Resolver interface. contract EnsResolver { - // Returns the address associated with the specified node. + /// Returns the address associated with the specified node. function addr(bytes32 node) view returns (address); - // Returns the name associated with an ENS node, for reverse records. + /// Returns the name associated with an ENS node, for reverse records. function name(bytes32 node) view returns (string); } } @@ -36,13 +37,19 @@ pub const ENS_REVERSE_REGISTRAR_DOMAIN: &str = "addr.reverse"; /// Error type for ENS resolution. #[derive(Debug, thiserror::Error)] -pub enum EnsResolutionError { - /// Failed to resolve ENS registry. - #[error("Failed to get resolver from ENS registry: {0}")] - EnsRegistryResolutionFailed(String), +pub enum EnsError { + /// Failed to get resolver from the ENS registry. + #[error("Failed to get resolver from the ENS registry: {0}")] + Resolver(alloy_contract::Error), + /// Failed to get resolver from the ENS registry. + #[error("ENS resolver not found for name {0:?}")] + ResolverNotFound(String), + /// Failed to lookup ENS name from an address. + #[error("Failed to lookup ENS name from an address: {0}")] + Lookup(alloy_contract::Error), /// Failed to resolve ENS name to an address. #[error("Failed to resolve ENS name to an address: {0}")] - EnsResolutionFailed(String), + Resolve(alloy_contract::Error), } /// ENS name or Ethereum Address. @@ -59,7 +66,7 @@ impl NameOrAddress { pub async fn resolve>( &self, provider: &P, - ) -> Result { + ) -> Result { match self { NameOrAddress::Name(name) => provider.resolve_name(name).await, NameOrAddress::Address(addr) => Ok(*addr), @@ -97,35 +104,36 @@ impl FromStr for NameOrAddress { } } +/// Extension trait for ENS contract calls. #[async_trait] pub trait ProviderEnsExt> { - async fn get_resolver(&self) -> Result, EnsResolutionError>; + /// Returns the resolver for the specified node. The `&str` is only used for error messages. + async fn get_resolver( + &self, + node: B256, + error_name: &str, + ) -> Result, EnsError>; - async fn resolve_name(&self, name: &str) -> Result { + /// Performs a forward lookup of an ENS name to an address. + async fn resolve_name(&self, name: &str) -> Result { let node = namehash(name); - let addr = self - .get_resolver() - .await? + let resolver = self.get_resolver(node, name).await?; + let addr = resolver .addr(node) .call() .await - .map_err(|err| EnsResolutionError::EnsResolutionFailed(err.to_string()))? + .map_err(EnsError::Resolve) + .inspect_err(|e| eprintln!("{e:?}"))? ._0; - Ok(addr) } - async fn lookup_address(&self, address: Address) -> Result { - let node = namehash(&reverse_address(address)); - let name = self - .get_resolver() - .await? - .name(node) - .call() - .await - .map_err(|err| EnsResolutionError::EnsResolutionFailed(err.to_string()))? - ._0; - + /// Performs a reverse lookup of an address to an ENS name. + async fn lookup_address(&self, address: &Address) -> Result { + let name = reverse_address(address); + let node = namehash(&name); + let resolver = self.get_resolver(node, &name).await?; + let name = resolver.name(node).call().await.map_err(EnsError::Lookup)?._0; Ok(name) } } @@ -137,15 +145,16 @@ where N: Network, T: Transport + Clone, { - async fn get_resolver(&self) -> Result, EnsResolutionError> { + async fn get_resolver( + &self, + node: B256, + error_name: &str, + ) -> Result, EnsError> { let registry = EnsRegistry::new(ENS_ADDRESS, self); - let address = registry - .resolver(namehash("eth")) - .call() - .await - .map_err(|err| EnsResolutionError::EnsRegistryResolutionFailed(err.to_string()))? - ._0; - + let address = registry.resolver(node).call().await.map_err(EnsError::Resolver)?._0; + if address == Address::ZERO { + return Err(EnsError::ResolverNotFound(error_name.to_string())); + } Ok(EnsResolverInstance::new(address, self)) } } @@ -156,18 +165,36 @@ pub fn namehash(name: &str) -> B256 { return B256::ZERO } - // Remove the variation selector U+FE0F - let name = name.replace('\u{fe0f}', ""); - - // Generate the node starting from the right - name.rsplit('.') - .fold([0u8; 32], |node, label| *keccak256([node, *keccak256(label.as_bytes())].concat())) - .into() + // Remove the variation selector `U+FE0F` if present. + const VARIATION_SELECTOR: char = '\u{fe0f}'; + let name = if name.contains(VARIATION_SELECTOR) { + Cow::Owned(name.replace(VARIATION_SELECTOR, "")) + } else { + Cow::Borrowed(name) + }; + + // Generate the node starting from the right. + // This buffer is `[node @ [u8; 32], label_hash @ [u8; 32]]`. + let mut buffer = [0u8; 64]; + for label in name.rsplit('.') { + // node = keccak256([node, keccak256(label)]) + + // Hash the label. + let mut label_hasher = Keccak256::new(); + label_hasher.update(label.as_bytes()); + label_hasher.finalize_into(&mut buffer[32..]); + + // Hash both the node and the label hash, writing into the node. + let mut buffer_hasher = Keccak256::new(); + buffer_hasher.update(buffer.as_slice()); + buffer_hasher.finalize_into(&mut buffer[..32]); + } + buffer[..32].try_into().unwrap() } /// Returns the reverse-registrar name of an address. -pub fn reverse_address(addr: Address) -> String { - format!("{addr:?}.{ENS_REVERSE_REGISTRAR_DOMAIN}")[2..].to_string() +pub fn reverse_address(addr: &Address) -> String { + format!("{addr:x}.{ENS_REVERSE_REGISTRAR_DOMAIN}") } #[cfg(test)] @@ -175,19 +202,35 @@ mod test { use super::*; fn assert_hex(hash: B256, val: &str) { - assert_eq!(hash.0.to_vec(), hex::decode(val).unwrap()); + assert_eq!(hash.0[..], hex::decode(val).unwrap()[..]); } #[test] fn test_namehash() { for (name, expected) in &[ - ("", "0000000000000000000000000000000000000000000000000000000000000000"), - ("foo.eth", "de9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"), + ("", "0x0000000000000000000000000000000000000000000000000000000000000000"), ("eth", "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"), + ("foo.eth", "0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"), ("alice.eth", "0x787192fc5378cc32aa956ddfdedbf26b24e8d78e40109add0eea2c1a012c3dec"), ("ret↩️rn.eth", "0x3de5f4c02db61b221e7de7f1c40e29b6e2f07eb48d65bf7e304715cd9ed33b24"), ] { assert_hex(namehash(name), expected); } } + + #[test] + fn test_reverse_address() { + for (addr, expected) in [ + ( + "0x314159265dd8dbb310642f98f50c066173c1259b", + "314159265dd8dbb310642f98f50c066173c1259b.addr.reverse", + ), + ( + "0x28679A1a632125fbBf7A68d850E50623194A709E", + "28679a1a632125fbbf7a68d850e50623194a709e.addr.reverse", + ), + ] { + assert_eq!(reverse_address(&addr.parse().unwrap()), expected, "{addr}"); + } + } }