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_data::NewtonPolicyData,
19    newton_policy_data_factory::NewtonPolicyDataFactory,
20    newton_policy_factory::NewtonPolicyFactory,
21    version::{is_compatible, MIN_COMPATIBLE_POLICY_DATA_VERSION, MIN_COMPATIBLE_POLICY_VERSION, PROTOCOL_VERSION},
22};
23use serde::Serialize;
24use std::str::FromStr;
25use tracing::{debug, error, info, warn};
26
27#[derive(Parser, Debug)]
28pub enum VersionCommand {
29    /// Check compatibility of a policy client with current protocol version
30    CheckCompatibility {
31        /// Policy client contract address
32        #[arg(long)]
33        policy_client: Address,
34
35        /// Chain ID
36        #[arg(long)]
37        chain_id: u64,
38
39        /// RPC URL (optional, will use default from config)
40        #[arg(long)]
41        rpc_url: Option<String>,
42    },
43
44    /// Migrate a policy client to a compatible version
45    Migrate {
46        /// Policy client contract address to migrate
47        #[arg(long)]
48        policy_client: Address,
49
50        /// Private key of the policy client owner
51        #[arg(long, env = "PRIVATE_KEY")]
52        private_key: String,
53
54        /// Chain ID
55        #[arg(long)]
56        chain_id: u64,
57
58        /// RPC URL (optional, will use default from config)
59        #[arg(long)]
60        rpc_url: Option<String>,
61
62        /// Skip compatibility check before migration
63        #[arg(long)]
64        skip_check: bool,
65
66        /// Dry run (show what would be done without executing)
67        #[arg(long)]
68        dry_run: bool,
69    },
70
71    /// Display protocol version information
72    Info,
73}
74
75#[derive(Serialize, Debug)]
76pub struct CompatibilityReport {
77    pub protocol_version: String,
78    pub policy_client: Address,
79    pub policy_address: Address,
80    pub policy_factory_version: String,
81    pub policy_compatible: bool,
82    pub policy_data_reports: Vec<PolicyDataReport>,
83    pub all_compatible: bool,
84    pub migration_required: bool,
85}
86
87#[derive(Serialize, Debug)]
88pub struct PolicyDataReport {
89    pub address: Address,
90    pub factory_version: String,
91    pub compatible: bool,
92}
93
94impl VersionCommand {
95    pub async fn run(self) -> Result<()> {
96        match self {
97            VersionCommand::CheckCompatibility {
98                policy_client,
99                chain_id,
100                rpc_url,
101            } => check_compatibility(policy_client, chain_id, rpc_url).await,
102            VersionCommand::Migrate {
103                policy_client,
104                private_key,
105                chain_id,
106                rpc_url,
107                skip_check,
108                dry_run,
109            } => migrate_policy(policy_client, private_key, chain_id, rpc_url, skip_check, dry_run).await,
110            VersionCommand::Info => {
111                print_version_info();
112                Ok(())
113            }
114        }
115    }
116}
117
118async fn check_compatibility(policy_client: Address, chain_id: u64, rpc_url: Option<String>) -> Result<()> {
119    let rpc_url = rpc_url.unwrap_or_else(|| {
120        RpcProviderConfig::load(chain_id)
121            .expect("Failed to load RPC config")
122            .http
123    });
124
125    info!("Checking version compatibility for policy client: {}", policy_client);
126    info!("Protocol version: {}", PROTOCOL_VERSION);
127
128    // Get policy address
129    let policy_address = get_policy_address_for_client(policy_client, rpc_url.clone()).await?;
130    info!("Policy address: {}", policy_address);
131
132    // Get policy factory and version
133    let policy_factory = get_policy_factory_for_policy(policy_address, &rpc_url).await?;
134    let policy_version = get_policy_factory_version(policy_factory, &rpc_url).await?;
135    info!("Policy factory version: {}", policy_version);
136
137    // Check policy compatibility
138    let policy_compatible = is_compatible(&policy_version, MIN_COMPATIBLE_POLICY_VERSION)?;
139    if policy_compatible {
140        info!("✓ Policy version is compatible");
141    } else {
142        warn!(
143            "✗ Policy version is INCOMPATIBLE (minimum required: v{})",
144            MIN_COMPATIBLE_POLICY_VERSION
145        );
146    }
147
148    // Check policy data compatibility
149    info!("Checking policy data...");
150
151    // Get policy data addresses from policy contract
152    let provider = ProviderBuilder::new().connect_http(rpc_url.parse()?);
153    let policy_contract = NewtonPolicy::new(policy_address, provider);
154
155    let policy_data_addresses = policy_contract
156        .getPolicyData()
157        .call()
158        .await
159        .context("Failed to get policy data addresses")?;
160
161    info!("Found {} policy data contracts", policy_data_addresses.len());
162
163    let mut policy_data_reports = Vec::new();
164    let mut all_compatible = policy_compatible;
165
166    for policy_data_addr in policy_data_addresses {
167        debug!("Checking policy data at {}...", policy_data_addr);
168
169        match check_policy_data_compatibility(policy_data_addr, &rpc_url).await {
170            Ok((version, compatible)) => {
171                debug!("Version: {}, Compatible: {}", version, compatible);
172                policy_data_reports.push(PolicyDataReport {
173                    address: policy_data_addr,
174                    factory_version: version,
175                    compatible,
176                });
177                all_compatible = all_compatible && compatible;
178            }
179            Err(e) => {
180                error!("Error checking compatibility: {}", e);
181                // Consider policy data with errors as incompatible
182                policy_data_reports.push(PolicyDataReport {
183                    address: policy_data_addr,
184                    factory_version: "unknown".to_string(),
185                    compatible: false,
186                });
187                all_compatible = false;
188            }
189        }
190    }
191
192    let report = CompatibilityReport {
193        protocol_version: PROTOCOL_VERSION.to_string(),
194        policy_client,
195        policy_address,
196        policy_factory_version: policy_version.clone(),
197        policy_compatible,
198        policy_data_reports,
199        all_compatible,
200        migration_required: !all_compatible,
201    };
202
203    info!("=== Compatibility Report ===");
204    info!("{}", serde_json::to_string_pretty(&report)?);
205
206    if report.migration_required {
207        warn!("Migration required!");
208        info!("To migrate your policy to the latest version:");
209        info!("  newton-cli policy migrate --policy-client {} \\", policy_client);
210        info!("    --private-key $YOUR_PRIVATE_KEY \\");
211        info!("    --chain-id {}", chain_id);
212        info!("Guide: https://docs.newt.foundation/versioning/migration");
213        std::process::exit(1);
214    } else {
215        info!("✓ All versions are compatible. No migration needed.");
216        Ok(())
217    }
218}
219
220fn print_version_info() {
221    info!("Newton Protocol Version Information");
222    info!("====================================");
223    info!("Protocol Version: {}", PROTOCOL_VERSION);
224    info!("Minimum Policy Version: {}", MIN_COMPATIBLE_POLICY_VERSION);
225    info!("Minimum Policy Data Version: {}", MIN_COMPATIBLE_POLICY_DATA_VERSION);
226    info!("Version Compatibility:");
227    info!("  - Major versions must match exactly");
228    info!("  - Minor version must be >= minimum");
229    info!("  - Patch version must be >= minimum (if minor versions match)");
230    info!("For more information, see: https://docs.newt.foundation/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_POLICY_DATA_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_POLICY_VERSION);
324
325        let is_compatible_now = is_compatible(&current_version, MIN_COMPATIBLE_POLICY_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 - Architecture Note
555    info!("Step 4: Policy client architecture analysis...");
556
557    warn!("⚠️  IMPORTANT: Policy Client Architecture Limitation");
558    warn!("");
559    warn!("After analyzing the NewtonPolicyClient architecture:");
560    warn!("- The policy client stores a single immutable policy contract address");
561    warn!("- This address is set during initialization and cannot be changed");
562    warn!("- The policy address is NOT stored as upgradeable/mutable state");
563    warn!("");
564    warn!(
565        "To complete the migration to use the new policy at {}:",
566        new_policy_address
567    );
568    warn!("");
569    info!("REQUIRED: Deploy a NEW policy client");
570    info!("  1. Deploy a new policy client contract");
571    info!("  2. Initialize it with:");
572    info!("     - policyTaskManager: <YOUR_TASK_MANAGER_ADDRESS>");
573    info!("     - policy: {}", new_policy_address);
574    info!("     - owner: <YOUR_OWNER_ADDRESS>");
575    info!("  3. Call setPolicy(policyConfig) on the new client to register your configuration");
576    info!("  4. Update your application to use the new client address");
577    info!(
578        "  5. Migrate any state/assets from old client {} to new client",
579        policy_client
580    );
581    info!("");
582    info!("Example deployment using cast:");
583    info!("  # Deploy new policy client");
584    info!("  cast send <FACTORY_ADDRESS> \"deployPolicyClient(address,address,address)\" \\");
585    info!("    <TASK_MANAGER> {} <OWNER>", new_policy_address);
586    info!("");
587    info!("Alternative: If your policy client is upgradeable (proxy pattern):");
588    info!("  - Check if you can upgrade the implementation to point to new policy");
589    info!("  - Or deploy a new implementation with the new policy address");
590    info!("  - Update the proxy to use the new implementation");
591    info!("");
592
593    if !dry_run {
594        info!("✓ New policy successfully deployed at: {}", new_policy_address);
595        info!("✓ New policy includes all migrated policy data");
596        info!("✗ Policy client update requires manual deployment (see above)");
597        return Ok(());
598    }
599
600    // Step 5: Verify migration
601    info!("Step 5: Verifying migration...");
602
603    if dry_run {
604        info!("DRY RUN MODE - Would verify:");
605        info!("✓ New policy is deployed and accessible");
606        info!("✓ Policy client correctly references new policy");
607        info!("✓ New policy version is compatible with protocol v{}", PROTOCOL_VERSION);
608        info!("✓ All policy data is migrated or compatible");
609        info!("Verification would include:");
610        info!("1. Calling getPolicyAddress() on policy client");
611        info!("2. Checking factory version of new policy");
612        info!("3. Verifying policy configuration matches original");
613        info!("4. Testing policy functionality with a test transaction");
614    } else {
615        info!("Verifying migration...");
616
617        // 1. Verify policy client points to new policy
618        let current_policy = get_policy_address_for_client(policy_client, rpc_url.clone()).await?;
619        if current_policy != new_policy_address {
620            return Err(eyre::eyre!(
621                "Verification failed: Policy client points to {}, expected {}",
622                current_policy,
623                new_policy_address
624            ));
625        }
626        info!("✓ Policy client correctly references new policy");
627
628        // 2. Verify new policy factory version is compatible
629        let new_policy_factory = get_policy_factory_for_policy(new_policy_address, &rpc_url).await?;
630        let new_policy_version = get_policy_factory_version(new_policy_factory, &rpc_url).await?;
631
632        if !is_compatible(&new_policy_version, MIN_COMPATIBLE_POLICY_VERSION)? {
633            return Err(eyre::eyre!(
634                "Verification failed: New policy version {} is not compatible with minimum required {}",
635                new_policy_version,
636                MIN_COMPATIBLE_POLICY_VERSION
637            ));
638        }
639        info!("✓ New policy version {} is compatible", new_policy_version);
640
641        // 3. Verify policy configuration matches original
642        let new_policy_info = get_policy_info(new_policy_address, &rpc_url).await?;
643        if new_policy_info.policy_cid != policy_info.policy_cid {
644            warn!(
645                "Policy CID mismatch: old={}, new={}",
646                policy_info.policy_cid, new_policy_info.policy_cid
647            );
648        }
649        if new_policy_info.schema_cid != policy_info.schema_cid {
650            warn!(
651                "Schema CID mismatch: old={}, new={}",
652                policy_info.schema_cid, new_policy_info.schema_cid
653            );
654        }
655        if new_policy_info.entrypoint != policy_info.entrypoint {
656            warn!(
657                "Entrypoint mismatch: old={}, new={}",
658                policy_info.entrypoint, new_policy_info.entrypoint
659            );
660        }
661        if new_policy_info.metadata_cid != policy_info.metadata_cid {
662            warn!(
663                "Metadata CID mismatch: old={}, new={}",
664                policy_info.metadata_cid, new_policy_info.metadata_cid
665            );
666        }
667        info!("✓ Policy configuration verified");
668
669        // 4. Verify all policy data
670        if new_policy_info.policy_data.len() != new_policy_data_addresses.len() {
671            return Err(eyre::eyre!(
672                "Verification failed: Policy data count mismatch. Expected {}, got {}",
673                new_policy_data_addresses.len(),
674                new_policy_info.policy_data.len()
675            ));
676        }
677
678        for (i, addr) in new_policy_info.policy_data.iter().enumerate() {
679            if let Ok(factory) = get_policy_data_factory_for_policy_data(*addr, &rpc_url).await {
680                if let Ok(version) = get_policy_data_factory_version(factory, &rpc_url).await {
681                    if !is_compatible(&version, MIN_COMPATIBLE_POLICY_DATA_VERSION)? {
682                        return Err(eyre::eyre!(
683                            "Verification failed: Policy data {} version {} is not compatible",
684                            i + 1,
685                            version
686                        ));
687                    }
688                }
689            }
690        }
691        info!("✓ All policy data verified as compatible");
692
693        info!("✓ Migration verified successfully!");
694    }
695
696    info!("=== Migration Summary ===");
697
698    if dry_run {
699        info!("✓ Dry run completed successfully");
700        info!("The following would be performed:");
701        info!("1. Deploy new policy at factory version {}", PROTOCOL_VERSION);
702        if !incompatible_policy_data.is_empty() {
703            info!(
704                "2. Migrate {} incompatible policy data contracts",
705                incompatible_policy_data.len()
706            );
707        } else {
708            info!("2. Reuse all existing policy data (all compatible)");
709        }
710        info!("3. Update policy client {} to reference new policy", policy_client);
711        info!("4. Verify migration success");
712        info!("");
713        info!("Remove --dry-run to execute the actual migration");
714    } else {
715        info!("✓ Migration completed successfully!");
716        info!("Summary:");
717        info!("- Old policy: {}", policy_address);
718        info!("- New policy: {}", new_policy_address);
719        info!("- Policy client: {}", policy_client);
720        if !incompatible_policy_data.is_empty() {
721            info!("- Migrated {} policy data contracts", incompatible_policy_data.len());
722        }
723    }
724
725    Ok(())
726}