1use alloy::{
37 primitives::{Address, FixedBytes},
38 providers::{MulticallError, Provider},
39 sol,
40 transports::{TransportError, TransportErrorKind},
41};
42
43use crate::web3::{HttpRpcProvider, erc165::ERC165::ERC165Instance};
44
45sol!(
46 #[allow(clippy::exhaustive_structs, reason="comes from sol macro")]
47 #[allow(clippy::exhaustive_enums, reason="comes from sol macro")]
48 #[sol(rpc)]
49 interface ERC165 {
50 function supportsInterface(bytes4 interfaceID) external view returns (bool);
57 }
58);
59
60pub const ERC_165_SUPPORTS_INTERFACE_SELECTOR: [u8; 4] = [0x01, 0xff, 0xc9, 0xa7];
66pub const INVALID_INTERFACE_SELECTOR: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
72
73#[must_use]
83pub fn erc165_interface_selector(selectors: impl IntoIterator<Item = [u8; 4]>) -> FixedBytes<4> {
84 FixedBytes::from(selectors.into_iter().fold([0u8; 4], |mut acc, selector| {
85 for (a, b) in acc.iter_mut().zip(selector) {
86 *a ^= b;
87 }
88 acc
89 }))
90}
91
92fn unwrap_erc165_call(
100 call: Result<bool, alloy::contract::Error>,
101) -> Result<(), ERC165ConfirmError> {
102 match call {
103 Ok(true) => Ok(()),
104 Err(alloy::contract::Error::ZeroData(_, _)) => Err(ERC165ConfirmError::NotAContract),
105 Err(alloy::contract::Error::TransportError(TransportError::Transport(transport_error))) => {
107 Err(ERC165ConfirmError::TransportError(transport_error))
108 }
109 Ok(false) | Err(_) => Err(ERC165ConfirmError::Unsupported),
111 }
112}
113
114#[derive(Debug, thiserror::Error)]
116#[non_exhaustive]
117pub enum ERC165ConfirmError {
118 #[error("The requested address is not a deployed contract")]
121 NotAContract,
122 #[error("The contract does not support the requested interface")]
124 Unsupported,
125 #[error(transparent)]
127 TransportError(#[from] TransportErrorKind),
128}
129
130impl From<MulticallError> for ERC165ConfirmError {
131 fn from(error: MulticallError) -> Self {
132 match error {
133 MulticallError::NoReturnData | MulticallError::DecodeError(_) => {
134 ERC165ConfirmError::NotAContract
135 }
136 MulticallError::TransportError(TransportError::Transport(transport_error)) => {
137 ERC165ConfirmError::TransportError(transport_error)
138 }
139 MulticallError::ValueTx
140 | MulticallError::CallFailed(_)
141 | MulticallError::TransportError(_) => ERC165ConfirmError::Unsupported,
142 }
143 }
144}
145
146impl HttpRpcProvider {
147 pub async fn ensure_erc165_conform(&self, address: Address) -> Result<(), ERC165ConfirmError> {
169 let maybe_erc165 = ERC165Instance::new(address, self.inner());
170 let supports_erc165_call =
171 maybe_erc165.supportsInterface(FixedBytes::from(ERC_165_SUPPORTS_INTERFACE_SELECTOR));
172 let supports_invalid_interface_call =
173 maybe_erc165.supportsInterface(FixedBytes::from(INVALID_INTERFACE_SELECTOR));
174 let (supports_erc165, supports_invalid) = self
175 .inner()
176 .multicall()
177 .add(supports_erc165_call)
178 .add(supports_invalid_interface_call)
179 .aggregate()
180 .await?;
181
182 if supports_erc165 && !supports_invalid {
183 Ok(())
184 } else {
185 Err(ERC165ConfirmError::Unsupported)
186 }
187 }
188
189 pub async fn erc165_supports_interface_unchecked(
208 &self,
209 address: Address,
210 selectors: impl IntoIterator<Item = [u8; 4]>,
211 ) -> Result<(), ERC165ConfirmError> {
212 let erc165 = ERC165Instance::new(address, self.inner());
213 let supports_interface = erc165
214 .supportsInterface(erc165_interface_selector(selectors))
215 .call()
216 .await;
217 unwrap_erc165_call(supports_interface)
218 }
219
220 pub async fn erc165_supports_interface(
237 &self,
238 address: Address,
239 selectors: impl IntoIterator<Item = [u8; 4]>,
240 ) -> Result<(), ERC165ConfirmError> {
241 let maybe_erc165 = ERC165Instance::new(address, self.inner());
242 let supports_interface_call =
243 maybe_erc165.supportsInterface(erc165_interface_selector(selectors));
244 let supports_erc165_call =
245 maybe_erc165.supportsInterface(FixedBytes::from(ERC_165_SUPPORTS_INTERFACE_SELECTOR));
246 let supports_invalid_interface_call =
247 maybe_erc165.supportsInterface(FixedBytes::from(INVALID_INTERFACE_SELECTOR));
248 let (supports_interface, supports_erc165, supports_invalid) = self
249 .inner()
250 .multicall()
251 .add(supports_interface_call)
252 .add(supports_erc165_call)
253 .add(supports_invalid_interface_call)
254 .aggregate()
255 .await?;
256
257 if supports_interface && supports_erc165 && !supports_invalid {
258 Ok(())
259 } else {
260 Err(ERC165ConfirmError::Unsupported)
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 #[cfg(feature = "web3-asserter")]
268 use alloy::{
269 primitives::{Bytes, U256, address},
270 providers::mock::Asserter,
271 sol_types::SolValue,
272 };
273 use alloy::{sol, sol_types::SolCall};
274
275 use crate::web3::erc165::ERC165;
276 #[cfg(feature = "web3-asserter")]
277 use crate::web3::{HttpRpcProvider, erc165::ERC165ConfirmError};
278
279 sol! {
280 interface Solidity101 {
281 function hello() external pure;
282 function world(int256) external pure;
283 }
284 }
285
286 #[test]
287 fn test_selector_hashes() {
288 assert_eq!(
289 super::erc165_interface_selector([ERC165::supportsInterfaceCall::SELECTOR]),
290 super::ERC_165_SUPPORTS_INTERFACE_SELECTOR
291 );
292 assert_eq!(super::erc165_interface_selector([]), [0, 0, 0, 0]);
293
294 let selectors = [
295 Solidity101::helloCall::SELECTOR,
296 Solidity101::worldCall::SELECTOR,
297 ];
298 assert_eq!(
299 super::erc165_interface_selector(selectors),
300 [0xc6, 0xbe, 0x8b, 0x58]
301 );
302 assert_eq!(
303 super::erc165_interface_selector(selectors.into_iter().rev()),
304 [0xc6, 0xbe, 0x8b, 0x58],
305 "selector order should not matter"
306 );
307 assert_ne!(
308 super::erc165_interface_selector([
309 Solidity101::helloCall::SELECTOR,
310 Solidity101::worldCall::SELECTOR,
311 Solidity101::helloCall::SELECTOR,
312 ]),
313 [0xc6, 0xbe, 0x8b, 0x58],
314 "repeating a selector should change the interface identifier"
315 );
316 }
317
318 #[cfg(feature = "web3-asserter")]
319 fn aggregate_response(values: impl IntoIterator<Item = bool>) -> Bytes {
320 let return_data = values
321 .into_iter()
322 .map(|value| Bytes::from(value.abi_encode()))
323 .collect::<Vec<_>>();
324 Bytes::from((U256::ZERO, return_data).abi_encode_params())
325 }
326
327 #[cfg(feature = "web3-asserter")]
328 fn provider_with_response(response: &Bytes) -> (HttpRpcProvider, Asserter) {
329 let asserter = Asserter::new();
330 asserter.push_success(response);
331 let provider = HttpRpcProvider::with_mock_asserter(asserter.clone());
332 (provider, asserter)
333 }
334
335 #[cfg(feature = "web3-asserter")]
336 #[tokio::test]
337 async fn ensure_erc165_conform_handles_contract_responses() {
338 for (values, should_succeed) in [
339 ([true, false], true),
340 ([false, false], false),
341 ([true, true], false),
342 ] {
343 let (provider, asserter) = provider_with_response(&aggregate_response(values));
344 let result = provider
345 .ensure_erc165_conform(address!("0000000000000000000000000000000000000001"))
346 .await;
347
348 if should_succeed {
349 result.expect("mocked contract should be ERC-165 conformant");
350 } else {
351 assert!(
352 matches!(result, Err(ERC165ConfirmError::Unsupported)),
353 "non-conformant response should be unsupported"
354 );
355 }
356 assert!(
357 asserter.read_q().is_empty(),
358 "the check should consume exactly one RPC response"
359 );
360 }
361 }
362
363 #[cfg(feature = "web3-asserter")]
364 #[tokio::test]
365 async fn ensure_erc165_conform_maps_call_errors() {
366 let (provider, asserter) = provider_with_response(&Bytes::new());
367 let result = provider
368 .ensure_erc165_conform(address!("0000000000000000000000000000000000000001"))
369 .await;
370 assert!(
371 matches!(result, Err(ERC165ConfirmError::NotAContract)),
372 "empty return data should identify a non-contract"
373 );
374 assert!(asserter.read_q().is_empty(), "response should be consumed");
375
376 let provider = HttpRpcProvider::with_mock_asserter(Asserter::new());
377 let result = provider
378 .ensure_erc165_conform(address!("0000000000000000000000000000000000000001"))
379 .await;
380 assert!(
381 matches!(result, Err(ERC165ConfirmError::TransportError(_))),
382 "an empty mock queue should produce a transport error"
383 );
384 }
385
386 #[cfg(feature = "web3-asserter")]
387 #[tokio::test]
388 async fn erc165_supports_interface_handles_contract_responses() {
389 for (values, should_succeed) in [
390 ([true, true, false], true),
391 ([false, true, false], false),
392 ([true, false, false], false),
393 ([true, true, true], false),
394 ] {
395 let (provider, asserter) = provider_with_response(&aggregate_response(values));
396 let result = provider
397 .erc165_supports_interface(
398 address!("0000000000000000000000000000000000000001"),
399 [ERC165::supportsInterfaceCall::SELECTOR],
400 )
401 .await;
402
403 if should_succeed {
404 result.expect("mocked contract should support the requested interface");
405 } else {
406 assert!(
407 matches!(result, Err(ERC165ConfirmError::Unsupported)),
408 "unsupported or non-conformant response should be rejected"
409 );
410 }
411 assert!(
412 asserter.read_q().is_empty(),
413 "the check should consume exactly one RPC response"
414 );
415 }
416 }
417
418 #[cfg(feature = "web3-asserter")]
419 #[tokio::test]
420 async fn erc165_supports_interface_maps_call_errors() {
421 let (provider, asserter) = provider_with_response(&Bytes::new());
422 let result = provider
423 .erc165_supports_interface(
424 address!("0000000000000000000000000000000000000001"),
425 [ERC165::supportsInterfaceCall::SELECTOR],
426 )
427 .await;
428 assert!(
429 matches!(result, Err(ERC165ConfirmError::NotAContract)),
430 "empty return data should identify a non-contract"
431 );
432 assert!(asserter.read_q().is_empty(), "response should be consumed");
433
434 let provider = HttpRpcProvider::with_mock_asserter(Asserter::new());
435 let result = provider
436 .erc165_supports_interface(
437 address!("0000000000000000000000000000000000000001"),
438 [ERC165::supportsInterfaceCall::SELECTOR],
439 )
440 .await;
441 assert!(
442 matches!(result, Err(ERC165ConfirmError::TransportError(_))),
443 "an empty mock queue should produce a transport error"
444 );
445 }
446
447 #[cfg(feature = "web3-asserter")]
448 #[tokio::test]
449 async fn erc165_supports_interface_unchecked_handles_contract_responses() {
450 for (value, should_succeed) in [(true, true), (false, false)] {
451 let response = Bytes::from(value.abi_encode());
452 let (provider, asserter) = provider_with_response(&response);
453 let result = provider
454 .erc165_supports_interface_unchecked(
455 address!("0000000000000000000000000000000000000001"),
456 [ERC165::supportsInterfaceCall::SELECTOR],
457 )
458 .await;
459
460 if should_succeed {
461 result.expect("mocked contract should support the requested interface");
462 } else {
463 assert!(
464 matches!(result, Err(ERC165ConfirmError::Unsupported)),
465 "false response should be unsupported"
466 );
467 }
468 assert!(
469 asserter.read_q().is_empty(),
470 "the check should consume exactly one RPC response"
471 );
472 }
473 }
474
475 #[cfg(feature = "web3-asserter")]
476 #[tokio::test]
477 async fn erc165_supports_interface_unchecked_maps_call_errors() {
478 let (provider, asserter) = provider_with_response(&Bytes::new());
479 let result = provider
480 .erc165_supports_interface_unchecked(
481 address!("0000000000000000000000000000000000000001"),
482 [ERC165::supportsInterfaceCall::SELECTOR],
483 )
484 .await;
485 assert!(
486 matches!(result, Err(ERC165ConfirmError::NotAContract)),
487 "empty return data should identify a non-contract"
488 );
489 assert!(asserter.read_q().is_empty(), "response should be consumed");
490
491 let provider = HttpRpcProvider::with_mock_asserter(Asserter::new());
492 let result = provider
493 .erc165_supports_interface_unchecked(
494 address!("0000000000000000000000000000000000000001"),
495 [ERC165::supportsInterfaceCall::SELECTOR],
496 )
497 .await;
498 assert!(
499 matches!(result, Err(ERC165ConfirmError::TransportError(_))),
500 "an empty mock queue should produce a transport error"
501 );
502 }
503}