|
| 1 | +package changesets |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + |
| 6 | + "github.com/ethereum/go-ethereum/common" |
| 7 | + chain_selectors "github.com/smartcontractkit/chain-selectors" |
| 8 | + |
| 9 | + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" |
| 10 | + cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment" |
| 11 | + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" |
| 12 | + |
| 13 | + "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/sequences/cctp" |
| 14 | + "github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets" |
| 15 | + mcms_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/mcms" |
| 16 | + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/adapters" |
| 17 | +) |
| 18 | + |
| 19 | +// MigrateHybridLockReleaseLiquidityConfig is the configuration for the MigrateHybridLockReleaseLiquidity changeset. |
| 20 | +// This changeset is EVM-only and intended for Ethereum mainnet/Sepolia (home chains) where the hybrid lock-release |
| 21 | +// pool and siloed lockboxes live. |
| 22 | +type MigrateHybridLockReleaseLiquidityConfig struct { |
| 23 | + // ChainSelector is the home chain where liquidity will be migrated (must be Ethereum mainnet or Sepolia). |
| 24 | + ChainSelector uint64 |
| 25 | + // HybridLockReleaseTokenPool is the address of the existing HybridLockReleaseUSDCTokenPool. |
| 26 | + HybridLockReleaseTokenPool string |
| 27 | + // SiloedUSDCTokenPool is the address of the SiloedUSDCTokenPool to migrate liquidity into. |
| 28 | + SiloedUSDCTokenPool string |
| 29 | + // USDCToken is the address of the USDC token contract. |
| 30 | + USDCToken string |
| 31 | + // LockReleaseChainSelectors specifies which remote chains' locked liquidity to migrate. |
| 32 | + LockReleaseChainSelectors []uint64 |
| 33 | + // LiquidityWithdrawPercent is the percent of locked liquidity to migrate (1-100). |
| 34 | + LiquidityWithdrawPercent uint8 |
| 35 | + // MCMS configures the resulting proposal. Required because this migration |
| 36 | + // operates on timelock-owned contracts and all operations must execute |
| 37 | + // atomically within a single MCMS proposal batch. |
| 38 | + MCMS mcms_utils.Input |
| 39 | +} |
| 40 | + |
| 41 | +// MigrateHybridLockReleaseLiquidity returns an EVM-only changeset that migrates liquidity from a |
| 42 | +// HybridLockReleaseUSDCTokenPool into per-chain siloed lockboxes on the home chain. |
| 43 | +func MigrateHybridLockReleaseLiquidity(mcmsRegistry *changesets.MCMSReaderRegistry) cldf_deployment.ChangeSetV2[MigrateHybridLockReleaseLiquidityConfig] { |
| 44 | + return cldf_deployment.CreateChangeSet( |
| 45 | + makeApplyMigrateHybridLockReleaseLiquidity(mcmsRegistry), |
| 46 | + makeVerifyMigrateHybridLockReleaseLiquidity(), |
| 47 | + ) |
| 48 | +} |
| 49 | + |
| 50 | +func makeVerifyMigrateHybridLockReleaseLiquidity() func(cldf_deployment.Environment, MigrateHybridLockReleaseLiquidityConfig) error { |
| 51 | + return func(e cldf_deployment.Environment, cfg MigrateHybridLockReleaseLiquidityConfig) error { |
| 52 | + if err := cfg.MCMS.Validate(); err != nil { |
| 53 | + return fmt.Errorf("MCMS config is required and must be valid: %w", err) |
| 54 | + } |
| 55 | + if _, err := chain_selectors.GetSelectorFamily(cfg.ChainSelector); err != nil { |
| 56 | + return fmt.Errorf("invalid chain selector %d: %w", cfg.ChainSelector, err) |
| 57 | + } |
| 58 | + // Liquidity migration is only supported on Ethereum home chains. |
| 59 | + if cfg.ChainSelector != chain_selectors.ETHEREUM_MAINNET.Selector && cfg.ChainSelector != chain_selectors.ETHEREUM_TESTNET_SEPOLIA.Selector { |
| 60 | + return fmt.Errorf("liquidity migration is only supported on Ethereum mainnet or Sepolia, got chain selector %d", cfg.ChainSelector) |
| 61 | + } |
| 62 | + if !common.IsHexAddress(cfg.HybridLockReleaseTokenPool) { |
| 63 | + return fmt.Errorf("invalid HybridLockReleaseTokenPool address for chain %d", cfg.ChainSelector) |
| 64 | + } |
| 65 | + if !common.IsHexAddress(cfg.SiloedUSDCTokenPool) { |
| 66 | + return fmt.Errorf("invalid SiloedUSDCTokenPool address for chain %d", cfg.ChainSelector) |
| 67 | + } |
| 68 | + if !common.IsHexAddress(cfg.USDCToken) { |
| 69 | + return fmt.Errorf("invalid USDCToken address for chain %d", cfg.ChainSelector) |
| 70 | + } |
| 71 | + if len(cfg.LockReleaseChainSelectors) == 0 { |
| 72 | + return fmt.Errorf("at least one lock release chain selector must be provided") |
| 73 | + } |
| 74 | + if cfg.LiquidityWithdrawPercent == 0 || cfg.LiquidityWithdrawPercent > 100 { |
| 75 | + return fmt.Errorf("liquidity withdraw percent must be between 1 and 100") |
| 76 | + } |
| 77 | + for _, sel := range cfg.LockReleaseChainSelectors { |
| 78 | + if _, err := chain_selectors.GetSelectorFamily(sel); err != nil { |
| 79 | + return fmt.Errorf("invalid lock release chain selector %d: %w", sel, err) |
| 80 | + } |
| 81 | + } |
| 82 | + return nil |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +func makeApplyMigrateHybridLockReleaseLiquidity( |
| 87 | + mcmsRegistry *changesets.MCMSReaderRegistry, |
| 88 | +) func(cldf_deployment.Environment, MigrateHybridLockReleaseLiquidityConfig) (cldf_deployment.ChangesetOutput, error) { |
| 89 | + return func(e cldf_deployment.Environment, cfg MigrateHybridLockReleaseLiquidityConfig) (cldf_deployment.ChangesetOutput, error) { |
| 90 | + reader, ok := mcmsRegistry.GetMCMSReader(chain_selectors.FamilyEVM) |
| 91 | + if !ok { |
| 92 | + return cldf_deployment.ChangesetOutput{}, fmt.Errorf("no MCMS reader registered for chain family 'evm'") |
| 93 | + } |
| 94 | + timelockRef, err := reader.GetTimelockRef(e, cfg.ChainSelector, cfg.MCMS) |
| 95 | + if err != nil { |
| 96 | + return cldf_deployment.ChangesetOutput{}, fmt.Errorf("failed to resolve timelock address on chain %d: %w", cfg.ChainSelector, err) |
| 97 | + } |
| 98 | + |
| 99 | + deps := adapters.MigrateHybridLockReleaseLiquidityDeps{ |
| 100 | + BlockChains: e.BlockChains, |
| 101 | + } |
| 102 | + in := adapters.MigrateHybridLockReleaseLiquidityInput{ |
| 103 | + ChainSelector: cfg.ChainSelector, |
| 104 | + HybridLockReleaseTokenPool: cfg.HybridLockReleaseTokenPool, |
| 105 | + SiloedUSDCTokenPool: cfg.SiloedUSDCTokenPool, |
| 106 | + USDCToken: cfg.USDCToken, |
| 107 | + LockReleaseChainSelectors: cfg.LockReleaseChainSelectors, |
| 108 | + LiquidityWithdrawPercent: cfg.LiquidityWithdrawPercent, |
| 109 | + MCMSTimelockAddress: timelockRef.Address, |
| 110 | + } |
| 111 | + |
| 112 | + report, err := cldf_ops.ExecuteSequence(e.OperationsBundle, cctp.MigrateHybridLockReleaseLiquidity, deps, in) |
| 113 | + if err != nil { |
| 114 | + return cldf_deployment.ChangesetOutput{}, fmt.Errorf("failed to migrate hybrid lock-release liquidity on chain %d: %w", cfg.ChainSelector, err) |
| 115 | + } |
| 116 | + |
| 117 | + batchOps := report.Output.BatchOps |
| 118 | + reports := report.ExecutionReports |
| 119 | + |
| 120 | + newDS := datastore.NewMemoryDataStore() |
| 121 | + for _, addr := range report.Output.Addresses { |
| 122 | + if err := newDS.Addresses().Add(addr); err != nil { |
| 123 | + return cldf_deployment.ChangesetOutput{}, fmt.Errorf("failed to add address %s to datastore: %w", addr.Address, err) |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + return changesets.NewOutputBuilder(e, mcmsRegistry). |
| 128 | + WithReports(reports). |
| 129 | + WithBatchOps(batchOps). |
| 130 | + WithDataStore(newDS). |
| 131 | + Build(cfg.MCMS) |
| 132 | + } |
| 133 | +} |
0 commit comments