Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cast): ENS commands #7765

Merged
merged 1 commit into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.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)]
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}");
}
}
}
Loading