forge_verify/
sourcify.rs

1use crate::{
2    provider::{VerificationContext, VerificationProvider},
3    retry::RETRY_CHECK_ON_VERIFY,
4    utils::ensure_solc_build_metadata,
5    verify::{ContractLanguage, VerifyArgs, VerifyCheckArgs},
6};
7use alloy_primitives::Address;
8use async_trait::async_trait;
9use eyre::{Context, Result, eyre};
10use foundry_common::retry::RetryError;
11use foundry_compilers::{
12    artifacts::{Source, StandardJsonCompilerInput, vyper::VyperInput},
13    solc::SolcLanguage,
14};
15use futures::FutureExt;
16use reqwest::StatusCode;
17use serde::{Deserialize, Serialize};
18use std::path::Path;
19
20pub static SOURCIFY_URL: &str = "https://sourcify.dev/server/";
21
22/// The type that can verify a contract on `sourcify`
23#[derive(Clone, Debug, Default)]
24#[non_exhaustive]
25pub struct SourcifyVerificationProvider;
26
27#[async_trait]
28impl VerificationProvider for SourcifyVerificationProvider {
29    async fn preflight_verify_check(
30        &mut self,
31        args: VerifyArgs,
32        context: VerificationContext,
33    ) -> Result<()> {
34        let _ = self.prepare_verify_request(&args, &context).await?;
35        Ok(())
36    }
37
38    async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()> {
39        let body = self.prepare_verify_request(&args, &context).await?;
40        let chain_id = args.etherscan.chain.unwrap_or_default().id();
41
42        if !args.skip_is_verified_check && self.is_contract_verified(&args).await? {
43            sh_println!(
44                "\nContract [{}] {:?} is already verified. Skipping verification.",
45                context.target_name,
46                args.address.to_string()
47            )?;
48
49            return Ok(());
50        }
51
52        trace!("submitting verification request {:?}", body);
53
54        let client = reqwest::Client::new();
55        let url =
56            Self::get_verify_url(args.verifier.verifier_url.as_deref(), chain_id, args.address);
57
58        let resp = args
59            .retry
60            .into_retry()
61            .run_async(|| {
62                async {
63                    sh_println!(
64                        "\nSubmitting verification for [{}] {:?}.",
65                        context.target_name,
66                        args.address.to_string()
67                    )?;
68                    let response = client
69                        .post(&url)
70                        .header("Content-Type", "application/json")
71                        .body(serde_json::to_string(&body)?)
72                        .send()
73                        .await?;
74
75                    let status = response.status();
76                    match status {
77                        StatusCode::CONFLICT => {
78                            sh_println!("Contract source code already fully verified")?;
79                            Ok(None)
80                        }
81                        StatusCode::ACCEPTED => {
82                            let text = response.text().await?;
83                            let verify_response: SourcifyVerificationResponse =
84                                serde_json::from_str(&text)
85                                    .wrap_err("Failed to parse Sourcify verification response")?;
86                            Ok(Some(verify_response))
87                        }
88                        _ => {
89                            let error: serde_json::Value = response.json().await?;
90                            eyre::bail!(
91                                "Sourcify verification request for address ({}) \
92                            failed with status code {status}\n\
93                            Details: {error:#}",
94                                args.address,
95                            );
96                        }
97                    }
98                }
99                .boxed()
100            })
101            .await?;
102
103        if let Some(resp) = resp {
104            let job_url = Self::get_job_status_url(
105                args.verifier.verifier_url.as_deref(),
106                resp.verification_id.clone(),
107            );
108            sh_println!(
109                "Submitted contract for verification:\n\tVerification Job ID: `{}`\n\tURL: {}",
110                resp.verification_id,
111                job_url
112            )?;
113
114            if args.watch {
115                let check_args = VerifyCheckArgs {
116                    id: resp.verification_id,
117                    etherscan: args.etherscan,
118                    retry: RETRY_CHECK_ON_VERIFY,
119                    verifier: args.verifier,
120                };
121                return self.check(check_args).await;
122            }
123        }
124
125        Ok(())
126    }
127
128    async fn check(&self, args: VerifyCheckArgs) -> Result<()> {
129        let url = Self::get_job_status_url(args.verifier.verifier_url.as_deref(), args.id.clone());
130
131        args.retry
132            .into_retry()
133            .run_async_until_break(|| async {
134                let response = reqwest::get(&url)
135                    .await
136                    .wrap_err("Failed to request verification status")
137                    .map_err(RetryError::Retry)?;
138
139                if response.status() == StatusCode::NOT_FOUND {
140                    return Err(RetryError::Break(eyre!(
141                        "No verification job found for ID {}",
142                        args.id
143                    )));
144                }
145
146                if !response.status().is_success() {
147                    return Err(RetryError::Retry(eyre!(
148                        "Failed to request verification status with status code {}",
149                        response.status()
150                    )));
151                }
152
153                let job_response: SourcifyJobResponse = response
154                    .json()
155                    .await
156                    .wrap_err("Failed to parse job response")
157                    .map_err(RetryError::Retry)?;
158
159                if !job_response.is_job_completed {
160                    return Err(RetryError::Retry(eyre!("Verification is still pending...")));
161                }
162
163                if let Some(error) = job_response.error {
164                    if error.custom_code == "already_verified" {
165                        let _ = sh_println!("Contract source code already verified");
166                        return Ok(());
167                    }
168
169                    return Err(RetryError::Break(eyre!(
170                        "Verification job failed:\nError Code: `{}`\nMessage: `{}`",
171                        error.custom_code,
172                        error.message
173                    )));
174                }
175
176                if let Some(contract_status) = job_response.contract.match_status {
177                    let _ = sh_println!(
178                        "Contract successfully verified:\nStatus: `{}`",
179                        contract_status,
180                    );
181                }
182                Ok(())
183            })
184            .await
185            .wrap_err("Checking verification result failed")
186    }
187}
188
189impl SourcifyVerificationProvider {
190    fn get_base_url(verifier_url: Option<&str>) -> &str {
191        verifier_url.unwrap_or(SOURCIFY_URL)
192    }
193
194    fn get_verify_url(
195        verifier_url: Option<&str>,
196        chain_id: u64,
197        contract_address: Address,
198    ) -> String {
199        let base_url = Self::get_base_url(verifier_url);
200        format!("{base_url}v2/verify/{chain_id}/{contract_address}")
201    }
202
203    fn get_job_status_url(verifier_url: Option<&str>, job_id: String) -> String {
204        let base_url = Self::get_base_url(verifier_url);
205        format!("{base_url}v2/verify/{job_id}")
206    }
207
208    fn get_lookup_url(
209        verifier_url: Option<&str>,
210        chain_id: u64,
211        contract_address: Address,
212    ) -> String {
213        let base_url = Self::get_base_url(verifier_url);
214        format!("{base_url}v2/contract/{chain_id}/{contract_address}")
215    }
216
217    /// Configures the API request to the sourcify API using the given [`VerifyArgs`].
218    async fn prepare_verify_request(
219        &self,
220        args: &VerifyArgs,
221        context: &VerificationContext,
222    ) -> Result<SourcifyVerifyRequest> {
223        let lang = args.detect_language(context);
224        let contract_identifier = format!(
225            "{}:{}",
226            context
227                .target_path
228                .strip_prefix(context.project.root())
229                .unwrap_or(context.target_path.as_path())
230                .display(),
231            context.target_name
232        );
233        let creation_transaction_hash = args.creation_transaction_hash.map(|h| h.to_string());
234
235        match lang {
236            ContractLanguage::Solidity => {
237                let mut input: StandardJsonCompilerInput = context
238                    .project
239                    .standard_json_input(&context.target_path)
240                    .wrap_err("Failed to get standard json input")?
241                    .normalize_evm_version(&context.compiler_version);
242
243                let mut settings = context.compiler_settings.solc.settings.clone();
244                settings.libraries.libs = input
245                    .settings
246                    .libraries
247                    .libs
248                    .into_iter()
249                    .map(|(f, libs)| {
250                        (f.strip_prefix(context.project.root()).unwrap_or(&f).to_path_buf(), libs)
251                    })
252                    .collect();
253
254                settings.remappings = input.settings.remappings;
255
256                // remove all incompatible settings
257                settings.sanitize(&context.compiler_version, SolcLanguage::Solidity);
258
259                input.settings = settings;
260
261                let std_json_input = serde_json::to_value(&input)
262                    .wrap_err("Failed to serialize standard json input")?;
263                let compiler_version =
264                    ensure_solc_build_metadata(context.compiler_version.clone()).await?.to_string();
265
266                Ok(SourcifyVerifyRequest {
267                    std_json_input,
268                    compiler_version,
269                    contract_identifier,
270                    creation_transaction_hash,
271                })
272            }
273            ContractLanguage::Vyper => {
274                let path = Path::new(&context.target_path);
275                let sources = Source::read_all_from(path, &["vy", "vyi"])?;
276                let input = VyperInput::new(
277                    sources,
278                    context.clone().compiler_settings.vyper,
279                    &context.compiler_version,
280                );
281                let std_json_input = serde_json::to_value(&input)
282                    .wrap_err("Failed to serialize vyper json input")?;
283
284                let compiler_version = context.compiler_version.to_string();
285
286                Ok(SourcifyVerifyRequest {
287                    std_json_input,
288                    compiler_version,
289                    contract_identifier,
290                    creation_transaction_hash,
291                })
292            }
293        }
294    }
295
296    async fn is_contract_verified(&self, args: &VerifyArgs) -> Result<bool> {
297        let chain_id = args.etherscan.chain.unwrap_or_default().id();
298        let url =
299            Self::get_lookup_url(args.verifier.verifier_url.as_deref(), chain_id, args.address);
300
301        match reqwest::get(&url).await {
302            Ok(response) => {
303                if response.status().is_success() {
304                    let contract_response: SourcifyContractResponse =
305                        response.json().await.wrap_err("Failed to parse contract response")?;
306
307                    let creation_exact = contract_response
308                        .creation_match
309                        .as_ref()
310                        .map(|s| s == "exact_match")
311                        .unwrap_or(false);
312
313                    let runtime_exact = contract_response
314                        .runtime_match
315                        .as_ref()
316                        .map(|s| s == "exact_match")
317                        .unwrap_or(false);
318
319                    Ok(creation_exact && runtime_exact)
320                } else {
321                    Ok(false)
322                }
323            }
324            Err(error) => Err(error).wrap_err_with(|| {
325                format!("Failed to query verification status for {}", args.address)
326            }),
327        }
328    }
329}
330
331#[derive(Debug, Serialize)]
332#[serde(rename_all = "camelCase")]
333pub struct SourcifyVerifyRequest {
334    std_json_input: serde_json::Value,
335    compiler_version: String,
336    contract_identifier: String,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    creation_transaction_hash: Option<String>,
339}
340
341#[derive(Debug, Deserialize)]
342#[serde(rename_all = "camelCase")]
343pub struct SourcifyVerificationResponse {
344    verification_id: String,
345}
346
347#[derive(Debug, Deserialize)]
348#[serde(rename_all = "camelCase")]
349pub struct SourcifyJobResponse {
350    is_job_completed: bool,
351    contract: SourcifyContractResponse,
352    error: Option<SourcifyErrorResponse>,
353}
354
355#[derive(Debug, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct SourcifyContractResponse {
358    #[serde(rename = "match")]
359    match_status: Option<String>,
360    creation_match: Option<String>,
361    runtime_match: Option<String>,
362}
363
364#[derive(Debug, Deserialize)]
365#[serde(rename_all = "camelCase")]
366pub struct SourcifyErrorResponse {
367    custom_code: String,
368    message: String,
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use clap::Parser;
375    use foundry_test_utils::forgetest_async;
376
377    forgetest_async!(creates_correct_verify_request_body, |prj, _cmd| {
378        prj.add_source("Counter", "contract Counter {}");
379
380        let args = VerifyArgs::parse_from([
381            "foundry-cli",
382            "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
383            "src/Counter.sol:Counter",
384            "--compiler-version",
385            "0.8.19",
386            "--root",
387            &prj.root().to_string_lossy(),
388        ]);
389
390        let context = args.resolve_context().await.unwrap();
391        let provider = SourcifyVerificationProvider::default();
392        let request = provider.prepare_verify_request(&args, &context).await.unwrap();
393
394        assert_eq!(request.compiler_version, "0.8.19+commit.7dd6d404");
395        assert_eq!(request.contract_identifier, "src/Counter.sol:Counter");
396        assert!(request.creation_transaction_hash.is_none());
397
398        assert!(request.std_json_input.is_object());
399        let json_obj = request.std_json_input.as_object().unwrap();
400        assert!(json_obj.contains_key("sources"));
401        assert!(json_obj.contains_key("settings"));
402
403        let sources = json_obj.get("sources").unwrap().as_object().unwrap();
404        assert!(sources.contains_key("src/Counter.sol"));
405        let counter_source = sources.get("src/Counter.sol").unwrap().as_object().unwrap();
406        let content = counter_source.get("content").unwrap().as_str().unwrap();
407        assert!(content.contains("contract Counter {}"));
408    });
409}