xmtp_id/scw_verifier/
chain_rpc_verifier.rs

1//! Interaction with [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) smart contracts.
2use crate::associations::AccountId;
3use crate::scw_verifier::SmartContractSignatureVerifier;
4use alloy::network::TransactionBuilder;
5use alloy::primitives::{Address, BlockNumber, Bytes, FixedBytes};
6use alloy::providers::{DynProvider, Provider, ProviderBuilder};
7use alloy::{sol, sol_types::SolConstructor};
8use hex::FromHexError;
9use std::sync::Arc;
10
11use super::{ValidationResponse, VerifierError};
12
13// https://github.com/AmbireTech/signature-validator/blob/7706bda/index.ts#L13
14// Contract from AmbireTech that is also used by Viem.
15// Note that this is not a complete ERC-6492 implementation as it lacks Prepare/Side-effect logic compared to official reference implementation, so it might evolve in the future.
16// For now it's accepted as [Coinbase Smart Wallet doc](https://github.com/AmbireTech/signature-validator/blob/7706bda/index.ts#L13) uses it for offchain verification.
17const VALIDATE_SIG_OFFCHAIN_BYTECODE: &str = include_str!("signature_validation.hex");
18
19sol!(
20    contract VerifySig {
21      constructor (
22        address _signer,
23        bytes32 _hash,
24        bytes memory _signature
25      );
26    }
27);
28
29#[derive(Debug, Clone)]
30pub struct RpcSmartContractWalletVerifier {
31    provider: Arc<DynProvider>,
32}
33
34impl RpcSmartContractWalletVerifier {
35    pub fn new(provider_url: String) -> Result<Self, VerifierError> {
36        Ok(Self {
37            provider: Arc::new(
38                ProviderBuilder::new()
39                    .connect_http(provider_url.parse()?)
40                    .erased(),
41            ),
42        })
43    }
44
45    pub fn new_from_provider(provider: impl Provider + 'static) -> Self {
46        Self {
47            provider: Arc::new(DynProvider::new(provider)),
48        }
49    }
50}
51
52#[xmtp_common::async_trait]
53impl SmartContractSignatureVerifier for RpcSmartContractWalletVerifier {
54    async fn is_valid_signature(
55        &self,
56        signer: AccountId,
57        hash: [u8; 32],
58        signature: Bytes,
59        block_number: Option<BlockNumber>,
60    ) -> Result<ValidationResponse, VerifierError> {
61        let code = hex::decode(VALIDATE_SIG_OFFCHAIN_BYTECODE.trim())?;
62        let account_address: Address = signer
63            .account_address
64            .parse()
65            .map_err(|_| FromHexError::InvalidStringLength)?;
66        let call = VerifySig::constructorCall::new((
67            account_address,
68            FixedBytes::<32>::new(hash),
69            signature,
70        ));
71
72        let data = call.abi_encode();
73        let data = [code, data].concat();
74        let block_number = match block_number {
75            Some(bn) => bn,
76            None => self
77                .provider
78                .get_block_number()
79                .await
80                .map_err(VerifierError::Provider)?,
81        };
82        let mut tx = self.provider.transaction_request();
83        tx.set_input(data);
84        let result = self.provider.call(tx).block(block_number.into()).await?;
85
86        // Check if result indicates valid signature (0x01)
87        let expected_valid = Bytes::from_static(&[0x01]);
88        let is_valid = result == expected_valid;
89
90        Ok(ValidationResponse {
91            is_valid,
92            block_number: Some(block_number),
93            error: None,
94        })
95    }
96}
97
98// Anvil does not work with WASM
99// because its a wrapper over the system-binary
100#[cfg(all(test, not(target_arch = "wasm32")))]
101pub(crate) mod tests {
102    #![allow(clippy::unwrap_used)]
103    use crate::utils::test::{SignatureWithNonce, SmartWalletContext, smart_wallet};
104
105    use super::*;
106    use alloy::dyn_abi::SolType;
107    use alloy::primitives::{B256, U256};
108    use alloy::providers::ext::AnvilApi;
109    use alloy::signers::Signer;
110    use std::time::Duration;
111
112    #[rstest::rstest]
113    #[timeout(Duration::from_secs(30))]
114    #[tokio::test]
115    async fn test_coinbase_smart_wallet(#[future] smart_wallet: SmartWalletContext) {
116        let SmartWalletContext {
117            factory,
118            sw,
119            owner0,
120            owner1,
121            sw_address,
122        } = smart_wallet.await;
123        let provider = factory.provider();
124        let chain_id = provider.get_chain_id().await.unwrap();
125        let hash = B256::random();
126        let replay_safe_hash = sw.replaySafeHash(hash).call().await.unwrap();
127        let verifier = RpcSmartContractWalletVerifier::new_from_provider(provider.clone());
128        let sig0 = owner0.sign_hash(&replay_safe_hash).await.unwrap();
129        let account_id = AccountId::new_evm(chain_id, format!("{}", sw_address));
130
131        let res = verifier
132            .is_valid_signature(
133                account_id.clone(),
134                *hash,
135                SignatureWithNonce::abi_encode(&(U256::from(0), Bytes::from(sig0.as_bytes())))
136                    .into(),
137                None,
138            )
139            .await
140            .unwrap();
141        assert!(res.is_valid);
142
143        // verify owner1 is a valid owner
144        let sig1 = owner1.sign_hash(&replay_safe_hash).await.unwrap();
145        let res = verifier
146            .is_valid_signature(
147                account_id.clone(),
148                *hash,
149                SignatureWithNonce::abi_encode(&(U256::from(1), Bytes::from(sig1.as_bytes())))
150                    .into(),
151                None,
152            )
153            .await
154            .unwrap();
155        assert!(res.is_valid);
156
157        // owner0 signature must not be used to verify owner1
158        let res = verifier
159            .is_valid_signature(
160                account_id.clone(),
161                *hash,
162                SignatureWithNonce::abi_encode(&(U256::from(1), Bytes::from(sig0.as_bytes())))
163                    .into(),
164                None,
165            )
166            .await
167            .unwrap();
168        assert!(!res.is_valid);
169    }
170
171    #[rstest::rstest]
172    #[timeout(Duration::from_secs(60))]
173    #[tokio::test]
174    async fn test_smart_wallet_time_travel(#[future] smart_wallet: SmartWalletContext) {
175        let SmartWalletContext {
176            factory,
177            sw,
178            owner1,
179            sw_address,
180            ..
181        } = smart_wallet.await;
182
183        let provider = factory.provider();
184        let verifier = RpcSmartContractWalletVerifier::new_from_provider(provider.clone());
185        let chain_id = provider.get_chain_id().await.unwrap();
186        let hash = B256::random();
187        let replay_safe_hash = sw.replaySafeHash(hash).call().await.unwrap();
188        let sig1 = owner1.sign_hash(&replay_safe_hash).await.unwrap();
189        let account_id = AccountId::new_evm(chain_id, format!("{}", sw_address));
190        let block_number = provider.get_block_number().await.unwrap();
191        println!("{}", block_number);
192        provider.anvil_mine(Some(50), None).await.unwrap();
193        println!("{}", provider.get_block_number().await.unwrap());
194        // remove owner1 and check owner1 is no longer a valid owner
195        let _tx = sw
196            .removeOwnerAtIndex(U256::from(1))
197            .from(owner1.address())
198            .send()
199            .await
200            .unwrap()
201            .get_receipt()
202            .await
203            .unwrap();
204
205        let res = verifier
206            .is_valid_signature(
207                account_id.clone(),
208                *hash,
209                SignatureWithNonce::abi_encode(&(U256::from(1), sig1.as_bytes())).into(),
210                None,
211            )
212            .await;
213        assert!(res.is_err());
214        // when verify a non-existing owner, it errors
215        // time travel to the pre-removel block number and verify owner1 WAS a valid owner
216
217        let res = verifier
218            .is_valid_signature(
219                account_id.clone(),
220                *hash,
221                SignatureWithNonce::abi_encode(&(U256::from(1), sig1.as_bytes())).into(),
222                Some(block_number),
223            )
224            .await
225            .unwrap();
226        assert!(res.is_valid);
227    }
228
229    // Testing ERC-6492 with deployed / undeployed coinbase smart wallet(ERC-1271) contracts, and EOA.
230    #[rstest::rstest]
231    #[timeout(Duration::from_secs(60))]
232    #[tokio::test]
233    async fn test_is_valid_signature(#[future] smart_wallet: SmartWalletContext) {
234        let SmartWalletContext {
235            factory,
236            sw,
237            owner0: owner,
238            sw_address,
239            ..
240        } = smart_wallet.await;
241        let provider = factory.provider();
242        let chain_id = provider.get_chain_id().await.unwrap();
243        let hash = B256::random();
244        let replay_safe_hash = sw.replaySafeHash(hash).call().await.unwrap();
245        let verifier = RpcSmartContractWalletVerifier::new_from_provider(provider.clone());
246        let signature = owner.sign_hash(&replay_safe_hash).await.unwrap();
247        let signature: Bytes =
248            SignatureWithNonce::abi_encode(&(U256::from(0), signature.as_bytes())).into();
249        let account_id = AccountId::new_evm(chain_id, format!("{}", sw_address));
250
251        // Testing ERC-6492 signatures with deployed ERC-1271.
252        assert!(
253            verifier
254                .is_valid_signature(account_id.clone(), *hash, signature.clone(), None)
255                .await
256                .unwrap()
257                .is_valid
258        );
259
260        assert!(
261            !verifier
262                .is_valid_signature(account_id.clone(), *B256::random(), signature, None)
263                .await
264                .unwrap()
265                .is_valid
266        );
267
268        // Testing if EOA wallet signature is valid on ERC-6492
269        let signature = owner.sign_hash(&hash).await.unwrap();
270        let owner_account_id = AccountId::new_evm(chain_id, format!("{:?}", owner.address()));
271        assert!(
272            verifier
273                .is_valid_signature(
274                    owner_account_id.clone(),
275                    *hash,
276                    signature.as_bytes().into(),
277                    None
278                )
279                .await
280                .unwrap()
281                .is_valid
282        );
283
284        assert!(
285            !verifier
286                .is_valid_signature(
287                    owner_account_id,
288                    *B256::random(),
289                    signature.as_bytes().into(),
290                    None
291                )
292                .await
293                .unwrap()
294                .is_valid
295        );
296    }
297}