Skip to main content

newton_cli/commands/
version.rs

1/// Version compatibility checking and migration commands
2use alloy::{
3    network::EthereumWallet, primitives::Address, providers::ProviderBuilder, signers::local::PrivateKeySigner,
4    transports::http::reqwest::Url,
5};
6use clap::Parser;
7use eyre::{Context, Result};
8use newton_prover_chainio::{
9    policy_client::get_policy_address_for_client,
10    version::{
11        get_policy_data_factory_for_policy_data, get_policy_data_factory_version, get_policy_factory_for_policy,
12        get_policy_factory_version,
13    },
14};
15use newton_prover_core::{
16    config::rpc::RpcProviderConfig,
17    newton_policy::NewtonPolicy,
18    newton_policy_client::NewtonPolicyClient,
19    newton_policy_data::NewtonPolicyData,
20    newton_policy_data_factory::NewtonPolicyDataFactory,
21    newton_policy_factory::NewtonPolicyFactory,
22    version::{is_compatible, MIN_COMPATIBLE_VERSION, PROTOCOL_VERSION},
23};
24use serde::Serialize;
25use std::str::FromStr;
26use tracing::{debug, error, info, warn};
27
28#[derive(Parser, Debug)]
29pub enum VersionCommand {
30    /// Check compatibility of a policy client with current protocol version
31    CheckCompatibility {
32        /// Policy client contract address
33        #[arg(long)]
34        policy_client: Address,
35
36        /// Chain ID
37        #[arg(long)]
38        chain_id: u64,
39
40        /// RPC URL (optional, will use default from config)
41        #[arg(long)]
42        rpc_url: Option<String>,
43    },
44
45    /// Migrate a policy client to a compatible version
46    Migrate {
47        /// Policy client contract address to migrate
48        #[arg(long)]
49        policy_client: Address,
50
51        /// Private key of the policy client owner
52        #[arg(long, env = "PRIVATE_KEY")]
53        private_key: String,
54
55        /// Chain ID
56        #[arg(long)]
57        chain_id: u64,
58
59        /// RPC URL (optional, will use default from config)
60        #[arg(long)]
61        rpc_url: Option<String>,
62
63        /// Skip compatibility check before migration
64        #[arg(long)]
65        skip_check: bool,
66
67        /// Dry run (show what would be done without executing)
68        #[arg(long)]
69        dry_run: bool,
70    },
71
72    /// Display protocol version information
73    Info,
74}
75
76#[derive(Serialize, Debug)]
77pub struct CompatibilityReport {
78    pub protocol_version: String,
79    pub policy_client: Address,
80    pub policy_address: Address,
81    pub policy_factory_version: String,
82    pub policy_compatible: bool,
83    pub policy_data_reports: Vec<PolicyDataReport>,
84    pub all_compatible: bool,
85    pub migration_required: bool,
86}
87
88#[derive(Serialize, Debug)]
89pub struct PolicyDataReport {
90    pub address: Address,
91    pub factory_version: String,
92    pub compatible: bool,
93}
94
95impl VersionCommand {
96    pub async fn run(self) -> Result<()> {
97        match self {
98            VersionCommand::CheckCompatibility {
99                policy_client,
100                chain_id,
101                rpc_url,
102            } => check_compatibility(policy_client, chain_id, rpc_url).await,
103            VersionCommand::Migrate {
104                policy_client,
105                private_key,
106                chain_id,
107                rpc_url,
108                skip_check,
109                dry_run,
110            } => migrate_policy(policy_client, private_key, chain_id, rpc_url, skip_check, dry_run).await,
111            VersionCommand::Info => {
112                print_version_info();
113                Ok(())
114            }
115        }
116    }
117}
118
119async fn check_compatibility(policy_client: Address, chain_id: u64, rpc_url: Option<String>) -> Result<()> {
120    let rpc_url = rpc_url.unwrap_or_else(|| {
121        RpcProviderConfig::load(chain_id)
122            .expect("Failed to load RPC config")
123            .http
124    });
125
126    info!("Checking version compatibility for policy client: {}", policy_client);
127    info!("Protocol version: {}", PROTOCOL_VERSION);
128
129    // Get policy address
130    let policy_address = get_policy_address_for_client(policy_client, rpc_url.clone()).await?;
131    info!("Policy address: {}", policy_address);
132
133    // Get policy factory and version
134    let policy_factory = get_policy_factory_for_policy(policy_address, &rpc_url).await?;
135    let policy_version = get_policy_factory_version(policy_factory, &rpc_url).await?;
136    info!("Policy factory version: {}", policy_version);
137
138    // Check policy compatibility
139    let policy_compatible = is_compatible(&policy_version, MIN_COMPATIBLE_VERSION)?;
140    if policy_compatible {
141        info!("✓ Policy version is compatible");
142    } else {
143        warn!(
144            "✗ Policy version is INCOMPATIBLE (minimum required: v{})",
145            MIN_COMPATIBLE_VERSION
146        );
147    }
148
149    // Check policy data compatibility
150    info!("Checking policy data...");
151
152    // Get policy data addresses from policy contract
153    let provider = ProviderBuilder::new().connect_http(rpc_url.parse()?);
154    let policy_contract = NewtonPolicy::new(policy_address, provider);
155
156    let policy_data_addresses = policy_contract
157        .getPolicyData()
158        .call()
159        .await
160        .context("Failed to get policy data addresses")?;
161
162    info!("Found {} policy data contracts", policy_data_addresses.len());
163
164    let mut policy_data_reports = Vec::new();
165    let mut all_compatible = policy_compatible;
166
167    for policy_data_addr in policy_data_addresses {
168        debug!("Checking policy data at {}...", policy_data_addr);
169
170        match check_policy_data_compatibility(policy_data_addr, &rpc_url).await {
171            Ok((version, compatible)) => {
172                debug!("Version: {}, Compatible: {}", version, compatible);
173                policy_data_reports.push(PolicyDataReport {
174                    address: policy_data_addr,
175                    factory_version: version,
176                    compatible,
177                });
178                all_compatible = all_compatible && compatible;
179            }
180            Err(e) => {
181                error!("Error checking compatibility: {}", e);
182                // Consider policy data with errors as incompatible
183                policy_data_reports.push(PolicyDataReport {
184                    address: policy_data_addr,
185                    factory_version: "unknown".to_string(),
186                    compatible: false,
187                });
188                all_compatible = false;
189            }
190        }
191    }
192
193    let report = CompatibilityReport {
194        protocol_version: PROTOCOL_VERSION.to_string(),
195        policy_client,
196        policy_address,
197        policy_factory_version: policy_version.clone(),
198        policy_compatible,
199        policy_data_reports,
200        all_compatible,
201        migration_required: !all_compatible,
202    };
203
204    info!("=== Compatibility Report ===");
205    info!("{}", serde_json::to_string_pretty(&report)?);
206
207    if report.migration_required {
208        warn!("Migration required!");
209        info!("To migrate your policy to the latest version:");
210        info!("  newton-cli policy migrate --policy-client {} \\", policy_client);
211        info!("    --private-key $YOUR_PRIVATE_KEY \\");
212        info!("    --chain-id {}", chain_id);
213        info!("Guide: https://docs.newton.xyz/versioning/migration");
214        std::process::exit(1);
215    } else {
216        info!("✓ All versions are compatible. No migration needed.");
217        Ok(())
218    }
219}
220
221fn print_version_info() {
222    info!("Newton Protocol Version Information");
223    info!("====================================");
224    info!("Protocol Version: {}", PROTOCOL_VERSION);
225    info!("Minimum Compatible Version: {}", MIN_COMPATIBLE_VERSION);
226    info!("Version Compatibility:");
227    info!("  - Major versions must match exactly");
228    info!("  - Minor version must be >= minimum");
229    info!("  - Patch version is ignored (backward-compatible bug fixes)");
230    info!("For more information, see: https://docs.newton.xyz/versioning");
231}
232
233/// Check compatibility of a policy data contract
234async fn check_policy_data_compatibility(policy_data_addr: Address, rpc_url: &str) -> Result<(String, bool)> {
235    let factory = get_policy_data_factory_for_policy_data(policy_data_addr, rpc_url).await?;
236    let version = get_policy_data_factory_version(factory, rpc_url).await?;
237    let compatible = is_compatible(&version, MIN_COMPATIBLE_VERSION)?;
238    Ok((version, compatible))
239}
240
241/// Get policy configuration from chain
242async fn get_policy_info(policy_address: Address, rpc_url: &str) -> Result<PolicyInfo> {
243    let provider = ProviderBuilder::new().connect_http(rpc_url.parse()?);
244    let policy_contract = NewtonPolicy::new(policy_address, provider);
245
246    let policy_cid = policy_contract
247        .getPolicyCid()
248        .call()
249        .await
250        .context("Failed to get policy CID")?;
251
252    let schema_cid = policy_contract
253        .getSchemaCid()
254        .call()
255        .await
256        .context("Failed to get schema CID")?;
257
258    let entrypoint = policy_contract
259        .getEntrypoint()
260        .call()
261        .await
262        .context("Failed to get entrypoint")?;
263
264    let metadata_cid = policy_contract
265        .getMetadataCid()
266        .call()
267        .await
268        .context("Failed to get metadata CID")?;
269
270    let policy_data = policy_contract
271        .getPolicyData()
272        .call()
273        .await
274        .context("Failed to get policy data addresses")?;
275
276    Ok(PolicyInfo {
277        policy_cid,
278        schema_cid,
279        entrypoint,
280        metadata_cid,
281        policy_data,
282    })
283}
284
285#[derive(Debug, Clone)]
286struct PolicyInfo {
287    policy_cid: String,
288    schema_cid: String,
289    entrypoint: String,
290    metadata_cid: String,
291    policy_data: Vec<Address>,
292}
293
294/// Migrate a policy client to use the latest compatible policy version
295async fn migrate_policy(
296    policy_client: Address,
297    private_key: String,
298    chain_id: u64,
299    rpc_url: Option<String>,
300    skip_check: bool,
301    dry_run: bool,
302) -> Result<()> {
303    let rpc_url = rpc_url.unwrap_or_else(|| {
304        RpcProviderConfig::load(chain_id)
305            .expect("Failed to load RPC config")
306            .http
307    });
308
309    info!("=== Policy Migration Tool ===");
310    info!("Policy Client: {}", policy_client);
311    info!("Chain ID: {}", chain_id);
312    info!("Protocol Version: {}", PROTOCOL_VERSION);
313
314    // Step 1: Check compatibility (unless skipped)
315    if !skip_check {
316        info!("Step 1: Checking current compatibility...");
317
318        let policy_address = get_policy_address_for_client(policy_client, rpc_url.clone()).await?;
319        let policy_factory = get_policy_factory_for_policy(policy_address, &rpc_url).await?;
320        let current_version = get_policy_factory_version(policy_factory, &rpc_url).await?;
321
322        info!("Current policy version: {}", current_version);
323        info!("Minimum required version: {}", MIN_COMPATIBLE_VERSION);
324
325        let is_compatible_now = is_compatible(&current_version, MIN_COMPATIBLE_VERSION)?;
326
327        if is_compatible_now {
328            info!("✓ Policy is already compatible!");
329            info!("No migration needed.");
330            return Ok(());
331        }
332
333        warn!("✗ Policy is incompatible and needs migration");
334    }
335
336    // Step 2: Read current policy configuration
337    info!("Step 2: Reading current policy configuration...");
338
339    // Get current policy address and configuration
340    let policy_address = get_policy_address_for_client(policy_client, rpc_url.clone()).await?;
341    info!("Current policy address: {}", policy_address);
342
343    // Get policy configuration from chain
344    let policy_info = get_policy_info(policy_address, &rpc_url).await?;
345    info!("- Policy CID: {}", policy_info.policy_cid);
346    info!("- Schema CID: {}", policy_info.schema_cid);
347    info!("- Entrypoint: {}", policy_info.entrypoint);
348    info!("- Metadata CID: {}", policy_info.metadata_cid);
349    info!("- Policy Data count: {}", policy_info.policy_data.len());
350
351    // Step 3: Deploy new policy with latest factory
352    info!("Step 3: Deploying new policy data (if needed) and new policy with latest factory version...");
353
354    // First, handle policy data migration
355    let mut new_policy_data_addresses = Vec::new();
356    let mut incompatible_policy_data = Vec::new();
357
358    for (i, policy_data_addr) in policy_info.policy_data.iter().enumerate() {
359        debug!("Checking policy data {} at {}...", i + 1, policy_data_addr);
360
361        match check_policy_data_compatibility(*policy_data_addr, &rpc_url).await {
362            Ok((version, compatible)) => {
363                debug!("Version: {}, Compatible: {}", version, compatible);
364                if !compatible {
365                    incompatible_policy_data.push(*policy_data_addr);
366                } else {
367                    // Compatible, reuse existing address
368                    new_policy_data_addresses.push(*policy_data_addr);
369                }
370            }
371            Err(e) => {
372                error!("Error checking compatibility: {}", e);
373                incompatible_policy_data.push(*policy_data_addr);
374            }
375        }
376    }
377
378    if !incompatible_policy_data.is_empty() {
379        info!(
380            "Step 3a: Migrating {} incompatible policy data contracts...",
381            incompatible_policy_data.len()
382        );
383
384        if dry_run {
385            warn!(
386                "DRY RUN MODE - Would migrate {} incompatible policy data contracts:",
387                incompatible_policy_data.len()
388            );
389            for addr in &incompatible_policy_data {
390                info!("- {}", addr);
391            }
392            // In dry run, just add placeholders
393            for _ in &incompatible_policy_data {
394                new_policy_data_addresses.push(Address::ZERO);
395            }
396        } else {
397            // Parse private key and create signer
398            let private_key_str = private_key.strip_prefix("0x").unwrap_or(&private_key);
399            let signer = PrivateKeySigner::from_str(private_key_str).context("Failed to parse private key")?;
400            let signer_address = signer.address();
401            let wallet = EthereumWallet::from(signer);
402
403            // Create provider with signer
404            let url = Url::parse(&rpc_url).context("Invalid RPC URL")?;
405            let provider = ProviderBuilder::new().wallet(wallet).connect_http(url);
406
407            for (i, policy_data_addr) in incompatible_policy_data.iter().enumerate() {
408                info!(
409                    "Migrating policy data {} of {}: {}",
410                    i + 1,
411                    incompatible_policy_data.len(),
412                    policy_data_addr
413                );
414
415                // Get policy data factory address
416                let policy_data_factory = get_policy_data_factory_for_policy_data(*policy_data_addr, &rpc_url).await?;
417                info!("Using policy data factory at: {}", policy_data_factory);
418
419                // Get current policy data configuration
420                let policy_data_contract = NewtonPolicyData::new(*policy_data_addr, provider.clone());
421                let wasm_cid = policy_data_contract
422                    .getWasmCid()
423                    .call()
424                    .await
425                    .context("Failed to get WASM CID")?;
426                let secrets_schema_cid = policy_data_contract
427                    .getSecretsSchemaCid()
428                    .call()
429                    .await
430                    .context("Failed to get secrets schema")?;
431                let expire_after = policy_data_contract
432                    .getExpireAfter()
433                    .call()
434                    .await
435                    .context("Failed to get expireAfter")?;
436                let metadata_cid = policy_data_contract
437                    .getMetadataCid()
438                    .call()
439                    .await
440                    .context("Failed to get metadata CID")?;
441
442                info!("- WASM CID: {}", wasm_cid);
443                info!("- Secrets Schema CID: {}", secrets_schema_cid);
444                info!("- Expire After: {}", expire_after);
445                info!("- Metadata CID: {}", metadata_cid);
446
447                // Deploy new policy data with same configuration
448                let factory_contract = NewtonPolicyDataFactory::new(policy_data_factory, provider.clone());
449
450                info!("Submitting deployPolicyData transaction...");
451                let tx = factory_contract
452                    .deployPolicyData(wasm_cid, secrets_schema_cid, expire_after, metadata_cid, signer_address)
453                    .send()
454                    .await
455                    .context("Failed to send deployPolicyData transaction")?;
456
457                info!("Transaction sent: {:?}", tx.tx_hash());
458                info!("Waiting for transaction confirmation...");
459
460                let receipt = tx.get_receipt().await.context("Failed to get transaction receipt")?;
461
462                // Extract new policy data address from PolicyDataDeployed event
463                let logs = receipt.inner.logs();
464                let policy_data_deployed_event = logs
465                    .iter()
466                    .find_map(|log| log.log_decode::<NewtonPolicyDataFactory::PolicyDataDeployed>().ok())
467                    .ok_or_else(|| eyre::eyre!("PolicyDataDeployed event not found in transaction logs"))?;
468
469                let new_policy_data = policy_data_deployed_event.data().policyData;
470                info!("✓ New policy data deployed at: {}", new_policy_data);
471                info!(
472                    "Factory version: {}",
473                    policy_data_deployed_event.data().implementationVersion
474                );
475
476                new_policy_data_addresses.push(new_policy_data);
477            }
478
479            info!("✓ All incompatible policy data contracts migrated successfully");
480        }
481    } else {
482        info!("✓ All policy data contracts are compatible, no migration needed");
483    }
484
485    // Now deploy the new policy
486    info!("Step 3b: Deploying new policy with latest factory version...");
487
488    let new_policy_address = if dry_run {
489        info!("DRY RUN MODE - Would deploy new policy with:");
490        info!("- Factory version: {} (latest)", PROTOCOL_VERSION);
491        info!("- Entrypoint: {}", policy_info.entrypoint);
492        info!("- Policy CID: {}", policy_info.policy_cid);
493        info!("- Schema CID: {}", policy_info.schema_cid);
494        info!("- Policy Data: {} contracts", new_policy_data_addresses.len());
495        info!("- Metadata CID: {}", policy_info.metadata_cid);
496        info!("Note: This would call NewtonPolicyFactory.deployPolicy() with the above parameters");
497        Address::ZERO // Placeholder for dry run
498    } else {
499        info!("Deploying new policy using latest factory...");
500
501        // Parse private key and create signer
502        let private_key_str = private_key.strip_prefix("0x").unwrap_or(&private_key);
503        let signer = PrivateKeySigner::from_str(private_key_str).context("Failed to parse private key")?;
504        let signer_address = signer.address();
505        let wallet = EthereumWallet::from(signer);
506
507        // Create provider with signer
508        let url = Url::parse(&rpc_url).context("Invalid RPC URL")?;
509        let provider = ProviderBuilder::new().wallet(wallet).connect_http(url);
510
511        // Get policy factory address from the existing policy
512        let policy_factory = get_policy_factory_for_policy(policy_address, &rpc_url).await?;
513        info!("Using policy factory at: {}", policy_factory);
514
515        let factory_contract = NewtonPolicyFactory::new(policy_factory, provider.clone());
516
517        // Deploy new policy with same configuration and new policy data addresses
518        info!("Submitting deployPolicy transaction...");
519        let tx = factory_contract
520            .deployPolicy(
521                policy_info.entrypoint.clone(),
522                policy_info.policy_cid.clone(),
523                policy_info.schema_cid.clone(),
524                new_policy_data_addresses.clone(),
525                policy_info.metadata_cid.clone(),
526                signer_address,
527            )
528            .send()
529            .await
530            .context("Failed to send deployPolicy transaction")?;
531
532        info!("Transaction sent: {:?}", tx.tx_hash());
533        info!("Waiting for transaction confirmation...");
534
535        let receipt = tx.get_receipt().await.context("Failed to get transaction receipt")?;
536
537        // Extract new policy address from PolicyDeployed event
538        let logs = receipt.inner.logs();
539        let policy_deployed_event = logs
540            .iter()
541            .find_map(|log| log.log_decode::<NewtonPolicyFactory::PolicyDeployed>().ok())
542            .ok_or_else(|| eyre::eyre!("PolicyDeployed event not found in transaction logs"))?;
543
544        let new_policy = policy_deployed_event.data().policy;
545        info!("✓ New policy deployed at: {}", new_policy);
546        info!(
547            "Factory version: {}",
548            policy_deployed_event.data().implementationVersion
549        );
550
551        new_policy
552    };
553
554    // Step 4: Update policy client to point to new policy
555    info!("Step 4: Updating policy client to use new policy...");
556
557    if dry_run {
558        info!(
559            "DRY RUN MODE - Would call setPolicyAddress({}) on policy client {}",
560            new_policy_address, policy_client
561        );
562        info!("Note: This calls the owner-only setPolicyAddress() function on the existing policy client");
563        info!("No redeployment needed - the policy client address stays the same");
564    } else {
565        // Parse private key and create signer (same credentials as Step 3b)
566        let private_key_str = private_key.strip_prefix("0x").unwrap_or(&private_key);
567        let signer = PrivateKeySigner::from_str(private_key_str).context("Failed to parse private key")?;
568        let wallet = EthereumWallet::from(signer);
569        let url = Url::parse(&rpc_url).context("Invalid RPC URL")?;
570        let provider = ProviderBuilder::new().wallet(wallet).connect_http(url);
571
572        let policy_client_contract = NewtonPolicyClient::new(policy_client, provider);
573
574        info!(
575            "Calling setPolicyAddress({}) on policy client {}...",
576            new_policy_address, policy_client
577        );
578
579        let tx = policy_client_contract
580            .setPolicyAddress(new_policy_address)
581            .send()
582            .await
583            .context("Failed to send setPolicyAddress transaction. Ensure the signer is the policy client owner.")?;
584
585        info!("Transaction sent: {:?}", tx.tx_hash());
586        info!("Waiting for confirmation...");
587
588        let receipt = tx
589            .get_receipt()
590            .await
591            .context("Failed to get setPolicyAddress receipt")?;
592
593        if !receipt.status() {
594            return Err(eyre::eyre!(
595                "setPolicyAddress transaction reverted (tx: {:?}). Possible causes:\n\
596                 - Signer is not the policy client owner\n\
597                 - New policy factory version is incompatible with TaskManager minimum",
598                receipt.transaction_hash
599            ));
600        }
601
602        info!(
603            "✓ Policy client {} now points to new policy {}",
604            policy_client, new_policy_address
605        );
606    }
607
608    // Step 5: Verify migration
609    info!("Step 5: Verifying migration...");
610
611    if dry_run {
612        info!("DRY RUN MODE - Would verify:");
613        info!("✓ New policy is deployed and accessible");
614        info!("✓ Policy client correctly references new policy");
615        info!("✓ New policy version is compatible with protocol v{}", PROTOCOL_VERSION);
616        info!("✓ All policy data is migrated or compatible");
617        info!("Verification would include:");
618        info!("1. Calling getPolicyAddress() on policy client");
619        info!("2. Checking factory version of new policy");
620        info!("3. Verifying policy configuration matches original");
621        info!("4. Testing policy functionality with a test transaction");
622    } else {
623        info!("Verifying migration...");
624
625        // 1. Verify policy client points to new policy
626        let current_policy = get_policy_address_for_client(policy_client, rpc_url.clone()).await?;
627        if current_policy != new_policy_address {
628            return Err(eyre::eyre!(
629                "Verification failed: Policy client points to {}, expected {}",
630                current_policy,
631                new_policy_address
632            ));
633        }
634        info!("✓ Policy client correctly references new policy");
635
636        // 2. Verify new policy factory version is compatible
637        let new_policy_factory = get_policy_factory_for_policy(new_policy_address, &rpc_url).await?;
638        let new_policy_version = get_policy_factory_version(new_policy_factory, &rpc_url).await?;
639
640        if !is_compatible(&new_policy_version, MIN_COMPATIBLE_VERSION)? {
641            return Err(eyre::eyre!(
642                "Verification failed: New policy version {} is not compatible with minimum required {}",
643                new_policy_version,
644                MIN_COMPATIBLE_VERSION
645            ));
646        }
647        info!("✓ New policy version {} is compatible", new_policy_version);
648
649        // 3. Verify policy configuration matches original
650        let new_policy_info = get_policy_info(new_policy_address, &rpc_url).await?;
651        if new_policy_info.policy_cid != policy_info.policy_cid {
652            warn!(
653                "Policy CID mismatch: old={}, new={}",
654                policy_info.policy_cid, new_policy_info.policy_cid
655            );
656        }
657        if new_policy_info.schema_cid != policy_info.schema_cid {
658            warn!(
659                "Schema CID mismatch: old={}, new={}",
660                policy_info.schema_cid, new_policy_info.schema_cid
661            );
662        }
663        if new_policy_info.entrypoint != policy_info.entrypoint {
664            warn!(
665                "Entrypoint mismatch: old={}, new={}",
666                policy_info.entrypoint, new_policy_info.entrypoint
667            );
668        }
669        if new_policy_info.metadata_cid != policy_info.metadata_cid {
670            warn!(
671                "Metadata CID mismatch: old={}, new={}",
672                policy_info.metadata_cid, new_policy_info.metadata_cid
673            );
674        }
675        info!("✓ Policy configuration verified");
676
677        // 4. Verify all policy data
678        if new_policy_info.policy_data.len() != new_policy_data_addresses.len() {
679            return Err(eyre::eyre!(
680                "Verification failed: Policy data count mismatch. Expected {}, got {}",
681                new_policy_data_addresses.len(),
682                new_policy_info.policy_data.len()
683            ));
684        }
685
686        for (i, addr) in new_policy_info.policy_data.iter().enumerate() {
687            if let Ok(factory) = get_policy_data_factory_for_policy_data(*addr, &rpc_url).await {
688                if let Ok(version) = get_policy_data_factory_version(factory, &rpc_url).await {
689                    if !is_compatible(&version, MIN_COMPATIBLE_VERSION)? {
690                        return Err(eyre::eyre!(
691                            "Verification failed: Policy data {} version {} is not compatible",
692                            i + 1,
693                            version
694                        ));
695                    }
696                }
697            }
698        }
699        info!("✓ All policy data verified as compatible");
700
701        info!("✓ Migration verified successfully!");
702    }
703
704    info!("=== Migration Summary ===");
705
706    if dry_run {
707        info!("✓ Dry run completed successfully");
708        info!("The following would be performed:");
709        info!("1. Deploy new policy at factory version {}", PROTOCOL_VERSION);
710        if !incompatible_policy_data.is_empty() {
711            info!(
712                "2. Migrate {} incompatible policy data contracts",
713                incompatible_policy_data.len()
714            );
715        } else {
716            info!("2. Reuse all existing policy data (all compatible)");
717        }
718        info!("3. Update policy client {} to reference new policy", policy_client);
719        info!("4. Verify migration success");
720        info!("");
721        info!("Remove --dry-run to execute the actual migration");
722    } else {
723        info!("✓ Migration completed successfully!");
724        info!("Summary:");
725        info!("- Old policy: {}", policy_address);
726        info!("- New policy: {}", new_policy_address);
727        info!("- Policy client: {}", policy_client);
728        if !incompatible_policy_data.is_empty() {
729            info!("- Migrated {} policy data contracts", incompatible_policy_data.len());
730        }
731    }
732
733    Ok(())
734}