Skip to main content

rain_erc/erc165/
mod.rs

1use alloy::contract::Error as ContractError;
2use alloy::primitives::Address;
3use alloy::providers::Provider;
4use alloy::sol;
5use alloy::sol_types::{SolCall, SolInterface};
6use thiserror::Error;
7
8// IERC165 contract alloy bindings. Inline rather than via
9// `sol!("lib/forge-std/.../IERC165.sol")` so the `#[sol(rpc)]` attribute
10// generates a `Provider`-aware contract instance (IERC165::new(addr, provider)).
11sol!(
12    #[sol(rpc)]
13    interface IERC165 {
14        function supportsInterface(bytes4 interfaceID) external view returns (bool);
15    }
16);
17
18#[derive(Error, Debug)]
19pub enum XorSelectorsError {
20    #[error("no selectors")]
21    NoSelectors,
22}
23
24/// Non-revert errors from an ERC-165 probe.
25///
26/// The ERC-165 spec collapses execution reverts into "interface not
27/// supported" — those stay as `Ok(false)` from the probe. The
28/// "called address has no code / returned empty calldata" case is
29/// also folded into `Ok(false)` for the same reason. Anything else
30/// (RPC transport failure, response decode failure) is a real
31/// failure mode the caller needs to see, so it is surfaced as `Err`.
32#[derive(Error, Debug)]
33pub enum Erc165Error {
34    /// The underlying contract call failed for a reason other than
35    /// the contract reverting or returning empty calldata
36    /// (transport, decode, …).
37    #[error(transparent)]
38    Call(#[from] ContractError),
39}
40
41/// True iff `e` represents the contract executing the call and
42/// reverting (with or without revert data), or the destination
43/// returning empty calldata. Per ERC-165 these are equivalent to
44/// "interface not supported".
45fn is_revert_like(e: &ContractError) -> bool {
46    e.as_revert_data().is_some() || matches!(e, ContractError::ZeroData(_, _))
47}
48
49/// Calculates XOR of the selectors of a type that implements SolInterface
50pub trait XorSelectors<T: SolInterface> {
51    /// get xor of all the selectors.
52    ///
53    /// in order to get interface id the array of selectors should include all the functions
54    /// (and only function) selectors of the interface, in alloy and using its sol! macro
55    /// bindings, the generated Calls enum includes all the fn selectors:
56    /// `{AlloyContractName}::{AlloyContractNameCalls}`
57    ///
58    /// related info can be found here:
59    /// https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified
60    fn xor_selectors() -> Result<[u8; 4], XorSelectorsError> {
61        let selectors = T::selectors().collect::<Vec<_>>();
62        if selectors.is_empty() {
63            return Err(XorSelectorsError::NoSelectors);
64        }
65        let mut result = u32::from_be_bytes(selectors[0]);
66        for selector in &selectors[1..] {
67            result ^= u32::from_be_bytes(*selector);
68        }
69        Ok(result.to_be_bytes())
70    }
71}
72impl<T: SolInterface> XorSelectors<T> for T {}
73
74/// the first check for checking if a contract supports erc165:
75/// the contract claims it supports the IERC165 interface itself
76/// (interfaceID = 0x01ffc9a7).
77///
78/// Returns `Ok(true)` when the call succeeded and the contract
79/// returned `true`; `Ok(false)` when the call succeeded with `false`
80/// or reverted (per spec); `Err` for anything else.
81async fn supports_erc165_check1<P: Provider>(
82    provider: &P,
83    contract_address: Address,
84) -> Result<bool, Erc165Error> {
85    let contract = IERC165::new(contract_address, provider);
86    match contract
87        .supportsInterface(IERC165::supportsInterfaceCall::SELECTOR.into())
88        .call()
89        .await
90    {
91        Ok(v) => Ok(v),
92        Err(e) if is_revert_like(&e) => Ok(false),
93        Err(e) => Err(e.into()),
94    }
95}
96
97/// the second check for checking if a contract supports erc165:
98/// the contract claims it does NOT support the all-ones sentinel
99/// interface (interfaceID = 0xffffffff).
100///
101/// Returns `Ok(true)` when the contract correctly returned `false`
102/// (so check2 passes); `Ok(false)` when it returned `true` or
103/// reverted (per spec); `Err` for anything else.
104async fn supports_erc165_check2<P: Provider>(
105    provider: &P,
106    contract_address: Address,
107) -> Result<bool, Erc165Error> {
108    let contract = IERC165::new(contract_address, provider);
109    match contract
110        .supportsInterface([0xff, 0xff, 0xff, 0xff].into())
111        .call()
112        .await
113    {
114        Ok(v) => Ok(!v),
115        Err(e) if is_revert_like(&e) => Ok(false),
116        Err(e) => Err(e.into()),
117    }
118}
119
120/// checks if the given contract implements ERC165
121/// the process is done as described in ERC165 specs:
122///
123/// https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165
124///
125/// Returns `Ok(true)` if both spec-mandated probes pass, `Ok(false)`
126/// if either probe says "not supported" (including via revert per
127/// spec, including via `ZeroData` for empty calldata responses),
128/// `Err` if a non-revert failure (transport or decode)
129/// prevented us from finishing the probe — callers can treat that as
130/// "answer unknown" rather than silently reading "no support".
131pub async fn supports_erc165<P: Provider>(
132    provider: &P,
133    contract_address: Address,
134) -> Result<bool, Erc165Error> {
135    if !supports_erc165_check1(provider, contract_address).await? {
136        return Ok(false);
137    }
138    supports_erc165_check2(provider, contract_address).await
139}
140
141#[cfg(test)]
142mod tests {
143    use super::XorSelectors;
144    use super::*;
145    use alloy::providers::{ProviderBuilder, mock::Asserter};
146    use alloy::rpc::json_rpc::ErrorPayload;
147    use serde_json::json;
148
149    // test contracts bindings
150    sol! {
151        #[sol(rpc)]
152        interface ITest {
153            function externalFn1() external pure returns (bool);
154            function externalFn2(uint256 val1, uint256 val2) external returns (uint256, bool);
155            function externalFn3(address add) external returns (address);
156            error SomeError();
157            event SomeEvent(uint256 value);
158        }
159    }
160
161    sol! {
162        // Interface with exactly one function — exercises the
163        // single-selector branch of xor_selectors (the loop body
164        // never runs, the result is just that one selector).
165        interface IOne {
166            function only() external;
167        }
168    }
169
170    sol! {
171        // Interface with exactly two functions — exercises the
172        // single-iteration of the XOR loop (initial selector XOR'd
173        // with one more, no further partners).
174        interface ITwo {
175            function first() external;
176            function second(uint256 v) external;
177        }
178    }
179
180    // No empty-interface fixture: the `sol!` macro does not generate
181    // a `Calls` enum for an interface with zero functions, so the
182    // `XorSelectorsError::NoSelectors` branch is only reachable via a
183    // hand-rolled `SolInterface` impl. It stays as a defensive guard.
184
185    fn mocked_provider(asserter: Asserter) -> impl Provider {
186        ProviderBuilder::new().connect_mocked_client(asserter)
187    }
188
189    fn revert_payload() -> ErrorPayload {
190        ErrorPayload {
191            code: -32003,
192            message: "execution reverted".into(),
193            data: Some(serde_json::value::to_raw_value(&json!("0x00")).unwrap()),
194        }
195    }
196
197    /// A non-revert-shaped JSON-RPC error: no `data` field, so
198    /// `Error::as_revert_data()` returns `None` and `is_revert_like`
199    /// is false. Stand-in for transport / RPC failures that the
200    /// probe should propagate as `Err`.
201    fn transport_error_payload() -> ErrorPayload {
202        ErrorPayload {
203            code: -32603,
204            message: "internal error".into(),
205            data: None,
206        }
207    }
208
209    #[test]
210    fn test_get_interface_id() {
211        let result = IERC165::IERC165Calls::xor_selectors().unwrap();
212        let expected: [u8; 4] = 0x01ffc9a7u32.to_be_bytes(); // known IERC165 interface id
213        assert_eq!(result, expected);
214
215        let result = ITest::ITestCalls::xor_selectors().unwrap();
216        let expected: [u8; 4] = 0x3dcd3fedu32.to_be_bytes(); // known ITest interface id
217        assert_eq!(result, expected);
218    }
219
220    #[test]
221    fn test_xor_selectors_single_selector_returns_that_selector() {
222        // Single-function interface: the result must equal that one
223        // function's selector unchanged (no XOR partner).
224        let result = IOne::IOneCalls::xor_selectors().unwrap();
225        let expected = IOne::onlyCall::SELECTOR;
226        assert_eq!(result, expected);
227    }
228
229    #[test]
230    fn test_xor_selectors_two_function_interface_xors_both() {
231        // Pin the loop body's correctness: the result must be the
232        // bitwise XOR of the two function selectors. Using
233        // `selectors().collect()` here re-derives independently of the
234        // implementation under test, so a mutation that swaps XOR for
235        // OR / AND / + would diverge from the manual computation.
236        let result = ITwo::ITwoCalls::xor_selectors().unwrap();
237        let selectors = ITwo::ITwoCalls::selectors().collect::<Vec<_>>();
238        assert_eq!(selectors.len(), 2);
239        let manual = u32::from_be_bytes(selectors[0]) ^ u32::from_be_bytes(selectors[1]);
240        assert_eq!(result, manual.to_be_bytes());
241        // Sanity: the answer is not just one selector unchanged
242        // (that would be the single-selector path).
243        assert_ne!(result, selectors[0]);
244        assert_ne!(result, selectors[1]);
245    }
246
247    #[tokio::test]
248    async fn test_supports_erc165_check1_true_response() {
249        let asserter = Asserter::new();
250        let address = Address::random();
251        asserter
252            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
253
254        let provider = mocked_provider(asserter);
255        let result = supports_erc165_check1(&provider, address).await.unwrap();
256        assert!(result);
257    }
258
259    #[tokio::test]
260    async fn test_supports_erc165_check1_false_response() {
261        let asserter = Asserter::new();
262        let address = Address::random();
263        asserter
264            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
265
266        let provider = mocked_provider(asserter);
267        let result = supports_erc165_check1(&provider, address).await.unwrap();
268        assert!(!result);
269    }
270
271    #[tokio::test]
272    async fn test_supports_erc165_check1_revert_response() {
273        let asserter = Asserter::new();
274        let address = Address::random();
275        asserter.push_failure(revert_payload());
276
277        let provider = mocked_provider(asserter);
278        let result = supports_erc165_check1(&provider, address).await.unwrap();
279        assert!(!result);
280    }
281
282    #[tokio::test]
283    async fn test_supports_erc165_check1_transport_error_propagates() {
284        // A non-revert error (no `data` on the JSON-RPC error) must
285        // surface as Err, not get silently collapsed to Ok(false).
286        let asserter = Asserter::new();
287        let address = Address::random();
288        asserter.push_failure(transport_error_payload());
289
290        let provider = mocked_provider(asserter);
291        let err = supports_erc165_check1(&provider, address)
292            .await
293            .unwrap_err();
294        assert!(matches!(err, Erc165Error::Call(_)));
295    }
296
297    #[tokio::test]
298    async fn test_supports_erc165_check1_zero_data_response() {
299        // An eth_call success with empty calldata (`"0x"`) — typical
300        // when the destination address has no code — must be treated
301        // as "interface not supported" via the ContractError::ZeroData
302        // branch of is_revert_like, not propagated as Err.
303        let asserter = Asserter::new();
304        let address = Address::random();
305        asserter.push_success(&"0x");
306
307        let provider = mocked_provider(asserter);
308        let result = supports_erc165_check1(&provider, address).await.unwrap();
309        assert!(!result);
310    }
311
312    #[tokio::test]
313    async fn test_supports_erc165_check2_returns_false() {
314        let asserter = Asserter::new();
315        let address = Address::random();
316        asserter
317            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
318
319        let provider = mocked_provider(asserter);
320        let result = supports_erc165_check2(&provider, address).await.unwrap();
321        assert!(result);
322    }
323
324    #[tokio::test]
325    async fn test_supports_erc165_check2_returns_true() {
326        let asserter = Asserter::new();
327        let address = Address::random();
328        asserter
329            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
330
331        let provider = mocked_provider(asserter);
332        let result = supports_erc165_check2(&provider, address).await.unwrap();
333        assert!(!result);
334    }
335
336    #[tokio::test]
337    async fn test_supports_erc165_check2_reverts() {
338        let asserter = Asserter::new();
339        let address = Address::random();
340        asserter.push_failure(revert_payload());
341
342        let provider = mocked_provider(asserter);
343        let result = supports_erc165_check2(&provider, address).await.unwrap();
344        assert!(!result);
345    }
346
347    #[tokio::test]
348    async fn test_supports_erc165_check2_transport_error_propagates() {
349        // Same shape as the check1 transport-error test: anything
350        // that isn't a revert must come back as Err.
351        let asserter = Asserter::new();
352        let address = Address::random();
353        asserter.push_failure(transport_error_payload());
354
355        let provider = mocked_provider(asserter);
356        let err = supports_erc165_check2(&provider, address)
357            .await
358            .unwrap_err();
359        assert!(matches!(err, Erc165Error::Call(_)));
360    }
361
362    #[tokio::test]
363    async fn test_supports_erc165_both_checks_pass() {
364        let asserter = Asserter::new();
365        let address = Address::random();
366        // check1 returns true
367        asserter
368            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
369        // check2 returns false (which means it passes)
370        asserter
371            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
372
373        let provider = mocked_provider(asserter);
374        let result = supports_erc165(&provider, address).await.unwrap();
375        assert!(result);
376    }
377
378    #[tokio::test]
379    async fn test_supports_erc165_check1_fails() {
380        let asserter = Asserter::new();
381        let address = Address::random();
382        // check1 returns false
383        asserter
384            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
385        // check2 result doesn't matter since check1 already failed
386        asserter
387            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
388
389        let provider = mocked_provider(asserter);
390        let result = supports_erc165(&provider, address).await.unwrap();
391        assert!(!result);
392    }
393
394    #[tokio::test]
395    async fn test_supports_erc165_check2_fails() {
396        let asserter = Asserter::new();
397        let address = Address::random();
398        // check1 returns true
399        asserter
400            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
401        // check2 returns true (which means it fails)
402        asserter
403            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
404
405        let provider = mocked_provider(asserter);
406        let result = supports_erc165(&provider, address).await.unwrap();
407        assert!(!result);
408    }
409
410    #[tokio::test]
411    async fn test_supports_erc165_check1_reverts() {
412        let asserter = Asserter::new();
413        let address = Address::random();
414        // check1 reverts
415        asserter.push_failure(revert_payload());
416        // check2 result doesn't matter since check1 already failed
417        asserter
418            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
419
420        let provider = mocked_provider(asserter);
421        let result = supports_erc165(&provider, address).await.unwrap();
422        assert!(!result);
423    }
424
425    #[tokio::test]
426    async fn test_supports_erc165_check2_reverts_after_check1_passes() {
427        let asserter = Asserter::new();
428        let address = Address::random();
429        // check1 returns true
430        asserter
431            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
432        // check2 reverts
433        asserter.push_failure(revert_payload());
434
435        let provider = mocked_provider(asserter);
436        let result = supports_erc165(&provider, address).await.unwrap();
437        assert!(!result);
438    }
439
440    #[tokio::test]
441    async fn test_supports_erc165_propagates_check1_transport_error() {
442        // check1 hits a non-revert error; the whole probe must Err
443        // out rather than silently returning Ok(false).
444        let asserter = Asserter::new();
445        let address = Address::random();
446        asserter.push_failure(transport_error_payload());
447
448        let provider = mocked_provider(asserter);
449        let err = supports_erc165(&provider, address).await.unwrap_err();
450        assert!(matches!(err, Erc165Error::Call(_)));
451    }
452
453    #[tokio::test]
454    async fn test_supports_erc165_propagates_check2_transport_error() {
455        // check1 succeeds (true), check2 hits a non-revert error.
456        // The probe must propagate the Err from check2.
457        let asserter = Asserter::new();
458        let address = Address::random();
459        asserter
460            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
461        asserter.push_failure(transport_error_payload());
462
463        let provider = mocked_provider(asserter);
464        let err = supports_erc165(&provider, address).await.unwrap_err();
465        assert!(matches!(err, Erc165Error::Call(_)));
466    }
467}