Skip to content

Commit

Permalink
fix(cast): ENS commands
Browse files Browse the repository at this point in the history
  • Loading branch information
DaniPopes committed Apr 23, 2024
1 parent db64c3e commit c5a4600
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 108 deletions.
18 changes: 9 additions & 9 deletions crates/cast/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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}");
Expand All @@ -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
Expand Down
47 changes: 0 additions & 47 deletions crates/cast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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
Expand Down
34 changes: 34 additions & 0 deletions crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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::<B256>();
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", &eth_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", &eth_rpc_url, "--verify"]);
let out = cmd.stdout_lossy().trim().parse::<Address>();
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", &eth_rpc_url, "--verify"]);
let (_out, err) = cmd.unchecked_output_lossy();
assert!(err.contains("not found"), "{err:?}");
});
147 changes: 95 additions & 52 deletions crates/common/src/ens.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Expand All @@ -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.
Expand All @@ -59,7 +66,7 @@ impl NameOrAddress {
pub async fn resolve<N: Network, T: Transport + Clone, P: Provider<T, N>>(
&self,
provider: &P,
) -> Result<Address, EnsResolutionError> {
) -> Result<Address, EnsError> {
match self {
NameOrAddress::Name(name) => provider.resolve_name(name).await,
NameOrAddress::Address(addr) => Ok(*addr),
Expand Down Expand Up @@ -97,35 +104,36 @@ impl FromStr for NameOrAddress {
}
}

/// Extension trait for ENS contract calls.
#[async_trait]
pub trait ProviderEnsExt<T: Transport + Clone, N: Network, P: Provider<T, N>> {
async fn get_resolver(&self) -> Result<EnsResolverInstance<T, &P, N>, 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<EnsResolverInstance<T, &P, N>, EnsError>;

async fn resolve_name(&self, name: &str) -> Result<Address, EnsResolutionError> {
/// Performs a forward lookup of an ENS name to an address.
async fn resolve_name(&self, name: &str) -> Result<Address, EnsError> {
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<String, EnsResolutionError> {
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<String, EnsError> {
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)
}
}
Expand All @@ -137,15 +145,16 @@ where
N: Network,
T: Transport + Clone,
{
async fn get_resolver(&self) -> Result<EnsResolverInstance<T, &P, N>, EnsResolutionError> {
async fn get_resolver(
&self,
node: B256,
error_name: &str,
) -> Result<EnsResolverInstance<T, &P, N>, 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))
}
}
Expand All @@ -156,38 +165,72 @@ 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);
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)]
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}");
}
}
}

0 comments on commit c5a4600

Please sign in to comment.