Skip to main content

forge_verify/
sourcify.rs

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