diff --git a/deployment/changesets/cs_add_ccip_package_id.go b/deployment/changesets/cs_add_ccip_package_id.go index d14fab76..ccabf1ce 100644 --- a/deployment/changesets/cs_add_ccip_package_id.go +++ b/deployment/changesets/cs_add_ccip_package_id.go @@ -26,13 +26,16 @@ const ( ) type AddCCIPPackageIdConfig struct { - SuiChainSelector uint64 `yaml:"suiChainSelector"` - Target AddPackageIdTarget `yaml:"target"` - CCIPPackageId string `yaml:"ccipPackageId"` - OnRampPackageId string `yaml:"onRampPackageId,omitempty"` - OffRampPackageId string `yaml:"offRampPackageId,omitempty"` - PackageId string `yaml:"packageId"` - TimelockConfig *utils.TimelockConfig `yaml:"timelockConfig,omitempty"` + SuiChainSelector uint64 `yaml:"suiChainSelector"` + Target AddPackageIdTarget `yaml:"target"` + CCIPPackageId string `yaml:"ccipPackageId"` // original CCIP package ID + LatestCCIPPackageId string `yaml:"latestCcipPackageId,omitempty"` // optional: upgraded CCIP binary + OnRampPackageId string `yaml:"onRampPackageId,omitempty"` + LatestOnRampPackageId string `yaml:"latestOnRampPackageId,omitempty"` // optional: upgraded OnRamp binary + OffRampPackageId string `yaml:"offRampPackageId,omitempty"` + LatestOffRampPackageId string `yaml:"latestOffRampPackageId,omitempty"` // optional: upgraded OffRamp binary + PackageId string `yaml:"packageId"` + TimelockConfig *utils.TimelockConfig `yaml:"timelockConfig,omitempty"` } var _ cldf.ChangeSetV2[AddCCIPPackageIdConfig] = AddCCIPPackageId{} @@ -106,10 +109,11 @@ func executeAddPackageId(e cldf.Environment, config AddCCIPPackageIdConfig, chai switch target { case AddPackageIdTargetCCIP: r, err := operations.ExecuteOperation(e.OperationsBundle, ccipops.AddPackageIdStateObjectOp, deps, ccipops.AddPackageIdStateObjectInput{ - CCIPPackageId: config.CCIPPackageId, + PackageId: config.CCIPPackageId, + LatestPackageId: config.LatestCCIPPackageId, CCIPObjectRefObjectId: chainState.CCIPObjectRef, OwnerCapObjectId: chainState.CCIPOwnerCapObjectId, - PackageId: config.PackageId, + NewPackageId: config.PackageId, }) if err != nil { return operations.Report[any, any]{}, fmt.Errorf("failed to add package ID to CCIP state object for Sui chain %d: %w", config.SuiChainSelector, err) @@ -118,10 +122,11 @@ func executeAddPackageId(e cldf.Environment, config AddCCIPPackageIdConfig, chai case AddPackageIdTargetOnRamp: r, err := operations.ExecuteOperation(e.OperationsBundle, onrampops.AddPackageIdOp, deps, onrampops.AddPackageIdInput{ - OnRampPackageId: config.OnRampPackageId, + PackageId: config.OnRampPackageId, + LatestPackageId: config.LatestOnRampPackageId, StateObjectId: chainState.OnRampStateObjectId, OwnerCapObjectId: chainState.OnRampOwnerCapObjectId, - PackageId: config.PackageId, + NewPackageId: config.PackageId, }) if err != nil { return operations.Report[any, any]{}, fmt.Errorf("failed to add package ID to OnRamp for Sui chain %d: %w", config.SuiChainSelector, err) @@ -130,10 +135,11 @@ func executeAddPackageId(e cldf.Environment, config AddCCIPPackageIdConfig, chai case AddPackageIdTargetOffRamp: r, err := operations.ExecuteOperation(e.OperationsBundle, offrampops.AddPackageIdOffRampOp, deps, offrampops.AddPackageIdOffRampInput{ - OffRampPackageId: config.OffRampPackageId, + PackageId: config.OffRampPackageId, + LatestPackageId: config.LatestOffRampPackageId, StateObjectId: chainState.OffRampStateObjectId, OwnerCapObjectId: chainState.OffRampOwnerCapId, - PackageId: config.PackageId, + NewPackageId: config.PackageId, }) if err != nil { return operations.Report[any, any]{}, fmt.Errorf("failed to add package ID to OffRamp for Sui chain %d: %w", config.SuiChainSelector, err) diff --git a/deployment/changesets/cs_block_function.go b/deployment/changesets/cs_block_function.go index d887ea8e..fc5c4dc1 100644 --- a/deployment/changesets/cs_block_function.go +++ b/deployment/changesets/cs_block_function.go @@ -18,7 +18,8 @@ import ( type BlockFunctionConfig struct { SuiChainSelector uint64 `yaml:"suiChainSelector"` - CCIPPackageId string `yaml:"ccipPackageId"` + PackageId string `yaml:"packageId"` + LatestPackageId string `yaml:"latestPackageId,omitempty"` ModuleName string `yaml:"moduleName"` FunctionName string `yaml:"functionName"` Version uint8 `yaml:"version"` @@ -56,7 +57,8 @@ func (d BlockFunction) Apply(e cldf.Environment, config BlockFunctionConfig) (cl } report, err := operations.ExecuteOperation(e.OperationsBundle, ccipops.BlockFunctionOp, deps, ccipops.BlockFunctionInput{ - CCIPPackageId: config.CCIPPackageId, + PackageId: config.PackageId, + LatestPackageId: config.LatestPackageId, StateObjectId: chainState.CCIPObjectRef, OwnerCapObjectId: chainState.CCIPOwnerCapObjectId, ModuleName: config.ModuleName, @@ -95,8 +97,8 @@ func (d BlockFunction) Apply(e cldf.Environment, config BlockFunctionConfig) (cl } func (d BlockFunction) VerifyPreconditions(e cldf.Environment, config BlockFunctionConfig) error { - if strings.TrimSpace(config.CCIPPackageId) == "" { - return fmt.Errorf("ccipPackageId is required") + if strings.TrimSpace(config.PackageId) == "" { + return fmt.Errorf("packageId is required") } return nil } diff --git a/deployment/changesets/cs_block_version.go b/deployment/changesets/cs_block_version.go index cf35b368..d889d59e 100644 --- a/deployment/changesets/cs_block_version.go +++ b/deployment/changesets/cs_block_version.go @@ -18,7 +18,8 @@ import ( type BlockVersionConfig struct { SuiChainSelector uint64 `yaml:"suiChainSelector"` - CCIPPackageId string `yaml:"ccipPackageId"` + PackageId string `yaml:"packageId"` + LatestPackageId string `yaml:"latestPackageId,omitempty"` ModuleName string `yaml:"moduleName"` Version uint8 `yaml:"version"` TimelockConfig *utils.TimelockConfig `yaml:"timelockConfig,omitempty"` @@ -55,7 +56,8 @@ func (d BlockVersion) Apply(e cldf.Environment, config BlockVersionConfig) (cldf } report, err := operations.ExecuteOperation(e.OperationsBundle, ccipops.BlockVersionOp, deps, ccipops.BlockVersionInput{ - CCIPPackageId: config.CCIPPackageId, + PackageId: config.PackageId, + LatestPackageId: config.LatestPackageId, StateObjectId: chainState.CCIPObjectRef, OwnerCapObjectId: chainState.CCIPOwnerCapObjectId, ModuleName: config.ModuleName, @@ -93,8 +95,8 @@ func (d BlockVersion) Apply(e cldf.Environment, config BlockVersionConfig) (cldf } func (d BlockVersion) VerifyPreconditions(e cldf.Environment, config BlockVersionConfig) error { - if strings.TrimSpace(config.CCIPPackageId) == "" { - return fmt.Errorf("ccipPackageId is required") + if strings.TrimSpace(config.PackageId) == "" { + return fmt.Errorf("packageId is required") } return nil } diff --git a/deployment/go.mod b/deployment/go.mod index 44d8b7fc..2ddf6577 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -16,8 +16,8 @@ require ( github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260311190822-5cbfc939dd16 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260406055916-9aa6b6c0ae81 github.com/smartcontractkit/chainlink-deployments-framework v0.75.0 - github.com/smartcontractkit/chainlink-sui v0.0.0-20260205175622-33e65031f9a9 - github.com/smartcontractkit/mcms v0.40.1 + github.com/smartcontractkit/chainlink-sui v0.0.0-20260408222042-6c7b4c27a8b2 + github.com/smartcontractkit/mcms v0.41.0 // Replace with official release before merging github.com/stretchr/testify v1.11.1 golang.org/x/sync v0.19.0 ) diff --git a/deployment/go.sum b/deployment/go.sum index 0f735058..197e12f4 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -652,8 +652,8 @@ github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12i github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d h1:LokA9PoCNb8mm8mDT52c3RECPMRsGz1eCQORq+J3n74= github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d/go.mod h1:Acy3BTBxou83ooMESLO90s8PKSu7RvLCzwSTbxxfOK0= -github.com/smartcontractkit/mcms v0.40.1 h1:r9bU/2GfIf6mHHM4PklSBkfi2Lq4+EGILC81e4IqKz0= -github.com/smartcontractkit/mcms v0.40.1/go.mod h1:7YqJPR8w9GiO1L/JjjTrwlSwAZ7i3J7cgOcu88PqtvU= +github.com/smartcontractkit/mcms v0.41.0 h1:SKRR/znedyDmj11isYgUW00j9+/n3FwGPHi1L/QWFfk= +github.com/smartcontractkit/mcms v0.41.0/go.mod h1:VTg6wrglc+efxfc7YIk4AcOZlrw1i+vkj5UqzG2p7K8= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= diff --git a/deployment/ops/ccip/op_registry.go b/deployment/ops/ccip/op_registry.go index b8040bf9..54edf750 100644 --- a/deployment/ops/ccip/op_registry.go +++ b/deployment/ops/ccip/op_registry.go @@ -21,6 +21,7 @@ var AllOperationsCCIP = []any{ *TransferOwnershipStateObjectOp, *AcceptOwnershipStateObjectOp, *ExecuteOwnershipTransferToMcmsStateObjectOp, + *AddAllowedModulesStateObjectOp, // Token Admin Registry Operations *TokenAdminRegistryInitializeOp, *TokenAdminRegistryUnregisterPoolOp, diff --git a/deployment/ops/ccip/op_state_object.go b/deployment/ops/ccip/op_state_object.go index db87d70b..f51af4b0 100644 --- a/deployment/ops/ccip/op_state_object.go +++ b/deployment/ops/ccip/op_state_object.go @@ -1,7 +1,9 @@ package ccipops import ( + "encoding/hex" "fmt" + "strings" "github.com/Masterminds/semver/v3" @@ -15,10 +17,11 @@ import ( // =================== Add Package ID Operations =================== // type AddPackageIdStateObjectInput struct { - CCIPPackageId string + PackageId string // original package ID (MCMS registry identity; used as binary when LatestPackageId is "") + LatestPackageId string // optional: upgraded package ID (PTB execution target when set) CCIPObjectRefObjectId string OwnerCapObjectId string - PackageId string + NewPackageId string // the package ID to register in the CCIP StateObject } type AddPackageIdStateObjectObjects struct { @@ -26,12 +29,17 @@ type AddPackageIdStateObjectObjects struct { } var addPackageIdStateObjectHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input AddPackageIdStateObjectInput) (output sui_ops.OpTxResult[AddPackageIdStateObjectObjects], err error) { - contract, err := module_state_object.NewStateObject(input.CCIPPackageId, deps.Client) + // When the package has been upgraded, PTB must target the latest bytecode for execution. + binaryPkgId := input.PackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + contract, err := module_state_object.NewStateObject(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[AddPackageIdStateObjectObjects]{}, fmt.Errorf("failed to create StateObject contract: %w", err) } - encodedCall, err := contract.Encoder().AddPackageId(bind.Object{Id: input.CCIPObjectRefObjectId}, bind.Object{Id: input.OwnerCapObjectId}, input.PackageId) + encodedCall, err := contract.Encoder().AddPackageId(bind.Object{Id: input.CCIPObjectRefObjectId}, bind.Object{Id: input.OwnerCapObjectId}, input.NewPackageId) if err != nil { return sui_ops.OpTxResult[AddPackageIdStateObjectObjects]{}, fmt.Errorf("failed to encode AddPackageId call: %w", err) } @@ -39,11 +47,18 @@ var addPackageIdStateObjectHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDep if err != nil { return sui_ops.OpTxResult[AddPackageIdStateObjectObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + // When the package has been upgraded, the on-chain MCMS registry still holds the original package's + // proof type, so tx.To must be the original package ID. The PTB MoveCall must target the latest package + // so upgraded bytecode runs. Use LatestPackageId so the proposal generator can separate the two. + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID // current PackageID is the latest (from binaryPkgId) + call.PackageID = input.PackageId // replace with original for on-chain identity + } if deps.Signer == nil { - b.Logger.Infow("Skipping execution of AddPackageId on StateObject as per no Signer provided", "packageId", input.PackageId) + b.Logger.Infow("Skipping execution of AddPackageId on StateObject as per no Signer provided", "newPackageId", input.NewPackageId) return sui_ops.OpTxResult[AddPackageIdStateObjectObjects]{ Digest: "", - PackageId: input.CCIPPackageId, + PackageId: input.PackageId, Objects: AddPackageIdStateObjectObjects{}, Call: call, }, nil @@ -60,11 +75,11 @@ var addPackageIdStateObjectHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDep return sui_ops.OpTxResult[AddPackageIdStateObjectObjects]{}, fmt.Errorf("failed to execute AddPackageId on StateObject: %w", err) } - b.Logger.Infow("Package ID added to CCIP StateObject", "packageId", input.PackageId) + b.Logger.Infow("Package ID added to CCIP StateObject", "newPackageId", input.NewPackageId) return sui_ops.OpTxResult[AddPackageIdStateObjectObjects]{ Digest: tx.Digest, - PackageId: input.CCIPPackageId, + PackageId: input.PackageId, Objects: AddPackageIdStateObjectObjects{}, Call: call, }, nil @@ -536,3 +551,109 @@ var ExecuteOwnershipTransferToMcmsStateObjectOp = cld_ops.NewOperation( "Executes ownership transfer to MCMS for the CCIP StateObject", executeOwnershipTransferToMcmsStateObjectHandler, ) + +// AddAllowedModulesStateObjectInput configures mcms_registry::add_allowed_modules +type AddAllowedModulesStateObjectInput struct { + PackageId string // original CCIP package ID (MCMS registry identity; used as binary when LatestPackageId is "") + LatestPackageId string // optional: upgraded CCIP package ID (PTB execution target when set) + MCMSRegistryObjId string // mcms_registry Registry shared object ID + AllowedModules []string +} + +type AddAllowedModulesStateObjectObjects struct{} + +// bcsAppendULEB128 appends n encoded as an unsigned LEB128 integer to data (BCS length encoding). +func bcsAppendULEB128(data []byte, n uint64) []byte { + for { + b := byte(n & 0x7F) + n >>= 7 + if n == 0 { + return append(data, b) + } + data = append(data, b|0x80) + } +} + +var addAllowedModulesStateObjectHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input AddAllowedModulesStateObjectInput) (output sui_ops.OpTxResult[AddAllowedModulesStateObjectObjects], err error) { + if strings.TrimSpace(input.PackageId) == "" { + return sui_ops.OpTxResult[AddAllowedModulesStateObjectObjects]{}, fmt.Errorf("PackageId is required") + } + if len(input.AllowedModules) == 0 { + return sui_ops.OpTxResult[AddAllowedModulesStateObjectObjects]{}, fmt.Errorf("AllowedModules must not be empty") + } + + // When the package has been upgraded, PTB must target the latest bytecode for execution. + binaryPkgId := input.PackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + + // state_object::mcms_add_allowed_modules routes to mcms_registry::add_allowed_modules internally. + // There is no direct (non-MCMS) add_allowed_modules function; this op is for proposal generation only. + // The function BCS-decodes data as: + // 1. validate_obj_addr(registry) → 32 raw address bytes + // 2. deserialize_vector>(modules) → ULEB128(N) + each: ULEB128(len)+bytes + registryHex := strings.TrimPrefix(strings.TrimSpace(input.MCMSRegistryObjId), "0x") + if len(registryHex)%2 != 0 { + registryHex = "0" + registryHex + } + registryBytes, err := hex.DecodeString(registryHex) + if err != nil { + return sui_ops.OpTxResult[AddAllowedModulesStateObjectObjects]{}, fmt.Errorf("invalid MCMSRegistryObjId: %w", err) + } + if len(registryBytes) < 32 { + padded := make([]byte, 32) + copy(padded[32-len(registryBytes):], registryBytes) + registryBytes = padded + } + if len(registryBytes) != 32 { + return sui_ops.OpTxResult[AddAllowedModulesStateObjectObjects]{}, fmt.Errorf("MCMSRegistryObjId must be a 32-byte Sui address, got %d bytes", len(registryBytes)) + } + + data := make([]byte, 0, 32+1+len(input.AllowedModules)*16) + data = append(data, registryBytes...) + data = bcsAppendULEB128(data, uint64(len(input.AllowedModules))) + for _, m := range input.AllowedModules { + modBytes := []byte(m) + data = bcsAppendULEB128(data, uint64(len(modBytes))) + data = append(data, modBytes...) + } + + call := sui_ops.TransactionCall{ + PackageID: binaryPkgId, + Module: "state_object", + Function: "add_allowed_modules", + Data: data, + StateObjID: input.MCMSRegistryObjId, + TypeArgs: []string{}, + } + // When the package has been upgraded, the on-chain MCMS registry still holds the original package's + // proof type, so tx.To must be the original package ID. The PTB MoveCall must target the latest package + // so upgraded bytecode runs. Use LatestPackageID so the proposal generator can separate the two. + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID // current PackageID is the latest (from binaryPkgId) + call.PackageID = input.PackageId // replace with original for on-chain MCMS identity + } + + if deps.Signer == nil { + b.Logger.Infow("Skipping execution of add_allowed_modules on StateObject as per no Signer provided", + "registry", input.MCMSRegistryObjId, "modules", input.AllowedModules) + return sui_ops.OpTxResult[AddAllowedModulesStateObjectObjects]{ + Digest: "", + PackageId: input.PackageId, + Objects: AddAllowedModulesStateObjectObjects{}, + Call: call, + }, nil + } + + return sui_ops.OpTxResult[AddAllowedModulesStateObjectObjects]{}, fmt.Errorf( + "direct execution of state_object::add_allowed_modules is not supported; the function can only be invoked through the MCMS executor", + ) +} + +var AddAllowedModulesStateObjectOp = cld_ops.NewOperation( + sui_ops.NewSuiOperationName("ccip", "state_object", "add_allowed_modules"), + semver.MustParse("0.1.0"), + "Adds module names to the MCMS registry allowlist via the CCIP state_object MCMS entrypoint (state_object::add_allowed_modules)", + addAllowedModulesStateObjectHandler, +) diff --git a/deployment/ops/ccip/op_state_object_test.go b/deployment/ops/ccip/op_state_object_test.go index 9fbb2603..ed051b50 100644 --- a/deployment/ops/ccip/op_state_object_test.go +++ b/deployment/ops/ccip/op_state_object_test.go @@ -88,10 +88,10 @@ func TestStateObjectOperations(t *testing.T) { t.Run("Test Add Package ID", func(t *testing.T) { newPackageId := "0x123456789abcdef" // Example package ID addReport, err := cld_ops.ExecuteOperation(bundle, AddPackageIdStateObjectOp, deps, AddPackageIdStateObjectInput{ - CCIPPackageId: ccipReport.Output.PackageId, + PackageId: ccipReport.Output.PackageId, CCIPObjectRefObjectId: ccipReport.Output.Objects.CCIPObjectRefObjectId, OwnerCapObjectId: ccipReport.Output.Objects.OwnerCapObjectId, - PackageId: newPackageId, + NewPackageId: newPackageId, }) require.NoError(t, err, "failed to add package ID") require.NotEmpty(t, addReport.Output.Digest, "add package ID transaction should have a digest") @@ -101,10 +101,10 @@ func TestStateObjectOperations(t *testing.T) { // First add a package ID to remove newPackageId := "0xabcdef1234567890abcdef1234567890abcdef12" _, err := cld_ops.ExecuteOperation(bundle, AddPackageIdStateObjectOp, deps, AddPackageIdStateObjectInput{ - CCIPPackageId: ccipReport.Output.PackageId, + PackageId: ccipReport.Output.PackageId, CCIPObjectRefObjectId: ccipReport.Output.Objects.CCIPObjectRefObjectId, OwnerCapObjectId: ccipReport.Output.Objects.OwnerCapObjectId, - PackageId: newPackageId, + NewPackageId: newPackageId, }) // Now remove the package ID removeReport, err := cld_ops.ExecuteOperation(bundle, RemovePackageIdStateObjectOp, deps, RemovePackageIdStateObjectInput{ diff --git a/deployment/ops/ccip/op_upgrade_registry.go b/deployment/ops/ccip/op_upgrade_registry.go index 98effac2..2e59411c 100644 --- a/deployment/ops/ccip/op_upgrade_registry.go +++ b/deployment/ops/ccip/op_upgrade_registry.go @@ -79,7 +79,8 @@ var UpgradeRegistryInitializeOp = cld_ops.NewOperation( // =================== Version Blocking Operations =================== // type BlockVersionInput struct { - CCIPPackageId string + PackageId string // original package ID (MCMS registry identity; used as binary when LatestPackageId is "") + LatestPackageId string // optional: upgraded package ID (PTB execution target when set) StateObjectId string OwnerCapObjectId string ModuleName string @@ -91,10 +92,14 @@ type BlockVersionObjects struct { } var blockVersionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input BlockVersionInput) (output sui_ops.OpTxResult[BlockVersionObjects], err error) { - if err := requireCCIPPackageID(input.CCIPPackageId); err != nil { + if err := requireCCIPPackageID(input.PackageId); err != nil { return sui_ops.OpTxResult[BlockVersionObjects]{}, err } - contract, err := module_upgrade_registry.NewUpgradeRegistry(input.CCIPPackageId, deps.Client) + binaryPkgId := input.PackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + contract, err := module_upgrade_registry.NewUpgradeRegistry(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[BlockVersionObjects]{}, fmt.Errorf("failed to create UpgradeRegistry contract: %w", err) } @@ -109,12 +114,16 @@ var blockVersionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input Bl if err != nil { return sui_ops.OpTxResult[BlockVersionObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID // current PackageID is the latest (from binaryPkgId) + call.PackageID = input.PackageId // replace with original for on-chain identity + } if deps.Signer == nil { b.Logger.Infow("Skipping execution of BlockVersion on UpgradeRegistry as per no Signer provided", "moduleName", input.ModuleName, "version", input.Version) return sui_ops.OpTxResult[BlockVersionObjects]{ Digest: "", - PackageId: input.CCIPPackageId, + PackageId: input.PackageId, Objects: BlockVersionObjects{}, Call: call, }, nil @@ -134,7 +143,7 @@ var blockVersionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input Bl return sui_ops.OpTxResult[BlockVersionObjects]{ Digest: tx.Digest, - PackageId: input.CCIPPackageId, + PackageId: input.PackageId, Objects: BlockVersionObjects{}, Call: call, }, nil @@ -150,7 +159,8 @@ var BlockVersionOp = cld_ops.NewOperation( // =================== Unblock Version Operations =================== // type UnblockVersionInput struct { - CCIPPackageId string + PackageId string // original package ID (MCMS registry identity; used as binary when LatestPackageId is "") + LatestPackageId string // optional: upgraded package ID (PTB execution target when set) StateObjectId string OwnerCapObjectId string ModuleName string @@ -162,10 +172,14 @@ type UnblockVersionObjects struct { } var unblockVersionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input UnblockVersionInput) (output sui_ops.OpTxResult[UnblockVersionObjects], err error) { - if err := requireCCIPPackageID(input.CCIPPackageId); err != nil { + if err := requireCCIPPackageID(input.PackageId); err != nil { return sui_ops.OpTxResult[UnblockVersionObjects]{}, err } - contract, err := module_upgrade_registry.NewUpgradeRegistry(input.CCIPPackageId, deps.Client) + binaryPkgId := input.PackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + contract, err := module_upgrade_registry.NewUpgradeRegistry(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[UnblockVersionObjects]{}, fmt.Errorf("failed to create UpgradeRegistry contract: %w", err) } @@ -180,12 +194,16 @@ var unblockVersionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input if err != nil { return sui_ops.OpTxResult[UnblockVersionObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID // current PackageID is the latest (from binaryPkgId) + call.PackageID = input.PackageId // replace with original for on-chain identity + } if deps.Signer == nil { b.Logger.Infow("Skipping execution of UnblockVersion on UpgradeRegistry as per no Signer provided", "moduleName", input.ModuleName, "version", input.Version) return sui_ops.OpTxResult[UnblockVersionObjects]{ Digest: "", - PackageId: input.CCIPPackageId, + PackageId: input.PackageId, Objects: UnblockVersionObjects{}, Call: call, }, nil @@ -205,7 +223,7 @@ var unblockVersionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input return sui_ops.OpTxResult[UnblockVersionObjects]{ Digest: tx.Digest, - PackageId: input.CCIPPackageId, + PackageId: input.PackageId, Objects: UnblockVersionObjects{}, Call: call, }, nil @@ -221,7 +239,8 @@ var UnblockVersionOp = cld_ops.NewOperation( // =================== Function Blocking Operations =================== // type BlockFunctionInput struct { - CCIPPackageId string + PackageId string // original package ID (MCMS registry identity; used as binary when LatestPackageId is "") + LatestPackageId string // optional: upgraded package ID (PTB execution target when set) StateObjectId string OwnerCapObjectId string ModuleName string @@ -234,10 +253,14 @@ type BlockFunctionObjects struct { } var blockFunctionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input BlockFunctionInput) (output sui_ops.OpTxResult[BlockFunctionObjects], err error) { - if err := requireCCIPPackageID(input.CCIPPackageId); err != nil { + if err := requireCCIPPackageID(input.PackageId); err != nil { return sui_ops.OpTxResult[BlockFunctionObjects]{}, err } - contract, err := module_upgrade_registry.NewUpgradeRegistry(input.CCIPPackageId, deps.Client) + binaryPkgId := input.PackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + contract, err := module_upgrade_registry.NewUpgradeRegistry(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[BlockFunctionObjects]{}, fmt.Errorf("failed to create UpgradeRegistry contract: %w", err) } @@ -252,12 +275,16 @@ var blockFunctionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input B if err != nil { return sui_ops.OpTxResult[BlockFunctionObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID // current PackageID is the latest (from binaryPkgId) + call.PackageID = input.PackageId // replace with original for on-chain identity + } if deps.Signer == nil { b.Logger.Infow("Skipping execution of BlockFunction on UpgradeRegistry as per no Signer provided", "moduleName", input.ModuleName, "functionName", input.FunctionName, "version", input.Version) return sui_ops.OpTxResult[BlockFunctionObjects]{ Digest: "", - PackageId: input.CCIPPackageId, + PackageId: input.PackageId, Objects: BlockFunctionObjects{}, Call: call, }, nil @@ -278,7 +305,7 @@ var blockFunctionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input B return sui_ops.OpTxResult[BlockFunctionObjects]{ Digest: tx.Digest, - PackageId: input.CCIPPackageId, + PackageId: input.PackageId, Objects: BlockFunctionObjects{}, Call: call, }, nil @@ -294,7 +321,8 @@ var BlockFunctionOp = cld_ops.NewOperation( // =================== Unblock Function Operations =================== // type UnblockFunctionInput struct { - CCIPPackageId string + PackageId string // original package ID (MCMS registry identity; used as binary when LatestPackageId is "") + LatestPackageId string // optional: upgraded package ID (PTB execution target when set) StateObjectId string OwnerCapObjectId string ModuleName string @@ -307,10 +335,14 @@ type UnblockFunctionObjects struct { } var unblockFunctionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input UnblockFunctionInput) (output sui_ops.OpTxResult[UnblockFunctionObjects], err error) { - if err := requireCCIPPackageID(input.CCIPPackageId); err != nil { + if err := requireCCIPPackageID(input.PackageId); err != nil { return sui_ops.OpTxResult[UnblockFunctionObjects]{}, err } - contract, err := module_upgrade_registry.NewUpgradeRegistry(input.CCIPPackageId, deps.Client) + binaryPkgId := input.PackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + contract, err := module_upgrade_registry.NewUpgradeRegistry(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[UnblockFunctionObjects]{}, fmt.Errorf("failed to create UpgradeRegistry contract: %w", err) } @@ -325,12 +357,16 @@ var unblockFunctionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input if err != nil { return sui_ops.OpTxResult[UnblockFunctionObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID // current PackageID is the latest (from binaryPkgId) + call.PackageID = input.PackageId // replace with original for on-chain identity + } if deps.Signer == nil { b.Logger.Infow("Skipping execution of UnblockFunction on UpgradeRegistry as per no Signer provided", "moduleName", input.ModuleName, "functionName", input.FunctionName, "version", input.Version) return sui_ops.OpTxResult[UnblockFunctionObjects]{ Digest: "", - PackageId: input.CCIPPackageId, + PackageId: input.PackageId, Objects: UnblockFunctionObjects{}, Call: call, }, nil @@ -351,7 +387,7 @@ var unblockFunctionHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input return sui_ops.OpTxResult[UnblockFunctionObjects]{ Digest: tx.Digest, - PackageId: input.CCIPPackageId, + PackageId: input.PackageId, Objects: UnblockFunctionObjects{}, Call: call, }, nil diff --git a/deployment/ops/ccip/op_upgrade_registry_test.go b/deployment/ops/ccip/op_upgrade_registry_test.go index 155f585a..8cca8c98 100644 --- a/deployment/ops/ccip/op_upgrade_registry_test.go +++ b/deployment/ops/ccip/op_upgrade_registry_test.go @@ -129,7 +129,7 @@ func TestUpgradeRegistryOperations(t *testing.T) { t.Run("Test Version Blocking", func(t *testing.T) { // Test blocking a version _, err := cld_ops.ExecuteOperation(bundle, BlockVersionOp, deps, BlockVersionInput{ - CCIPPackageId: report.Output.CCIPPackageId, + PackageId: report.Output.CCIPPackageId, StateObjectId: report.Output.Objects.CCIPObjectRefObjectId, OwnerCapObjectId: report.Output.Objects.OwnerCapObjectId, ModuleName: "test_module", @@ -172,7 +172,7 @@ func TestUpgradeRegistryOperations(t *testing.T) { t.Run("Test Function Blocking", func(t *testing.T) { // Test blocking a specific function _, err := cld_ops.ExecuteOperation(bundle, BlockFunctionOp, deps, BlockFunctionInput{ - CCIPPackageId: report.Output.CCIPPackageId, + PackageId: report.Output.CCIPPackageId, StateObjectId: report.Output.Objects.CCIPObjectRefObjectId, OwnerCapObjectId: report.Output.Objects.OwnerCapObjectId, ModuleName: "test_module_2", @@ -218,7 +218,7 @@ func TestUpgradeRegistryOperations(t *testing.T) { // Test verifying a blocked function (should fail) // First block the function _, err = cld_ops.ExecuteOperation(bundle, BlockFunctionOp, deps, BlockFunctionInput{ - CCIPPackageId: report.Output.CCIPPackageId, + PackageId: report.Output.CCIPPackageId, StateObjectId: report.Output.Objects.CCIPObjectRefObjectId, OwnerCapObjectId: report.Output.Objects.OwnerCapObjectId, ModuleName: "test_module_3", diff --git a/deployment/ops/ccip_offramp/op_deploy.go b/deployment/ops/ccip_offramp/op_deploy.go index 7fbadac1..d4f95818 100644 --- a/deployment/ops/ccip_offramp/op_deploy.go +++ b/deployment/ops/ccip_offramp/op_deploy.go @@ -241,10 +241,11 @@ var applySourceChainConfigUpdateHandler = func(b cld_ops.Bundle, deps sui_ops.Op } type AddPackageIdOffRampInput struct { - OffRampPackageId string + PackageId string // original package ID (MCMS registry identity; used as binary when LatestPackageId is "") + LatestPackageId string // optional: upgraded package ID (PTB execution target when set) StateObjectId string OwnerCapObjectId string - PackageId string + NewPackageId string // the package ID to register in the OffRamp state } type AddPackageIdOffRampObjects struct { @@ -252,30 +253,59 @@ type AddPackageIdOffRampObjects struct { } var addPackageIdOffRampHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input AddPackageIdOffRampInput) (output sui_ops.OpTxResult[AddPackageIdOffRampObjects], err error) { - offRampPackage, err := module_offramp.NewOfframp(input.OffRampPackageId, deps.Client) + // When the package has been upgraded, PTB must target the latest bytecode for execution. + binaryPkgId := input.PackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + offRampPackage, err := module_offramp.NewOfframp(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[AddPackageIdOffRampObjects]{}, err } + encodedCall, err := offRampPackage.Encoder().AddPackageId(bind.Object{Id: input.StateObjectId}, bind.Object{Id: input.OwnerCapObjectId}, input.NewPackageId) + if err != nil { + return sui_ops.OpTxResult[AddPackageIdOffRampObjects]{}, fmt.Errorf("failed to encode AddPackageId call: %w", err) + } + call, err := sui_ops.ToTransactionCall(encodedCall, input.StateObjectId) + if err != nil { + return sui_ops.OpTxResult[AddPackageIdOffRampObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) + } + // When the package has been upgraded, the on-chain MCMS registry still holds the original package's + // proof type, so tx.To must be the original package ID. Use LatestPackageId so the proposal + // generator routes the PTB MoveCall to the upgraded bytecode. + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID // current PackageID is the latest (from binaryPkgId) + call.PackageID = input.PackageId // replace with original for on-chain identity + } + if deps.Signer == nil { + b.Logger.Infow("Skipping execution of AddPackageId on OffRamp as per no Signer provided", "newPackageId", input.NewPackageId) + return sui_ops.OpTxResult[AddPackageIdOffRampObjects]{ + Digest: "", + PackageId: input.PackageId, + Objects: AddPackageIdOffRampObjects{}, + Call: call, + }, nil + } + opts := deps.GetCallOpts() opts.Signer = deps.Signer - tx, err := offRampPackage.AddPackageId( + tx, err := offRampPackage.Bound().ExecuteTransaction( b.GetContext(), opts, - bind.Object{Id: input.StateObjectId}, - bind.Object{Id: input.OwnerCapObjectId}, - input.PackageId, + encodedCall, ) if err != nil { return sui_ops.OpTxResult[AddPackageIdOffRampObjects]{}, fmt.Errorf("failed to execute AddPackageId on offRamp: %w", err) } - b.Logger.Infow("Package ID added to OffRamp", "packageId", input.PackageId) + b.Logger.Infow("Package ID added to OffRamp", "newPackageId", input.NewPackageId) return sui_ops.OpTxResult[AddPackageIdOffRampObjects]{ Digest: tx.Digest, - PackageId: input.OffRampPackageId, + PackageId: input.PackageId, Objects: AddPackageIdOffRampObjects{}, + Call: call, }, nil } diff --git a/deployment/ops/ccip_onramp/op_deploy.go b/deployment/ops/ccip_onramp/op_deploy.go index e8f673e1..3eb1400a 100644 --- a/deployment/ops/ccip_onramp/op_deploy.go +++ b/deployment/ops/ccip_onramp/op_deploy.go @@ -515,10 +515,11 @@ var GetDestChainConfigOp = cld_ops.NewOperation( ) type AddPackageIdInput struct { - OnRampPackageId string + PackageId string // original package ID (MCMS registry identity; used as binary when LatestPackageId is "") + LatestPackageId string // optional: upgraded package ID (PTB execution target when set) StateObjectId string OwnerCapObjectId string - PackageId string + NewPackageId string // the package ID to register in the OnRamp state } type AddPackageIdObjects struct { @@ -526,7 +527,11 @@ type AddPackageIdObjects struct { } var addPackageIdHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input AddPackageIdInput) (output sui_ops.OpTxResult[AddPackageIdObjects], err error) { - onRampPackage, err := module_onramp.NewOnramp(input.OnRampPackageId, deps.Client) + binaryPkgId := input.PackageId + if input.LatestPackageId != "" { + binaryPkgId = input.LatestPackageId + } + onRampPackage, err := module_onramp.NewOnramp(binaryPkgId, deps.Client) if err != nil { return sui_ops.OpTxResult[AddPackageIdObjects]{}, err } @@ -534,7 +539,7 @@ var addPackageIdHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input Ad encodedCall, err := onRampPackage.Encoder().AddPackageId( bind.Object{Id: input.StateObjectId}, bind.Object{Id: input.OwnerCapObjectId}, - input.PackageId, + input.NewPackageId, ) if err != nil { return sui_ops.OpTxResult[AddPackageIdObjects]{}, fmt.Errorf("failed to encode AddPackageId call: %w", err) @@ -543,11 +548,15 @@ var addPackageIdHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input Ad if err != nil { return sui_ops.OpTxResult[AddPackageIdObjects]{}, fmt.Errorf("failed to convert encoded call to TransactionCall: %w", err) } + if input.LatestPackageId != "" { + call.LatestPackageID = call.PackageID // current PackageID is the latest (from binaryPkgId) + call.PackageID = input.PackageId // replace with original for on-chain identity + } if deps.Signer == nil { - b.Logger.Infow("Skipping execution of AddPackageId on OnRamp as per no Signer provided", "packageId", input.PackageId) + b.Logger.Infow("Skipping execution of AddPackageId on OnRamp as per no Signer provided", "newPackageId", input.NewPackageId) return sui_ops.OpTxResult[AddPackageIdObjects]{ Digest: "", - PackageId: input.OnRampPackageId, + PackageId: input.PackageId, Objects: AddPackageIdObjects{}, Call: call, }, nil @@ -564,11 +573,11 @@ var addPackageIdHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input Ad return sui_ops.OpTxResult[AddPackageIdObjects]{}, fmt.Errorf("failed to execute AddPackageId on OnRamp: %w", err) } - b.Logger.Infow("Package ID added to OnRamp", "packageId", input.PackageId) + b.Logger.Infow("Package ID added to OnRamp", "newPackageId", input.NewPackageId) return sui_ops.OpTxResult[AddPackageIdObjects]{ Digest: tx.Digest, - PackageId: input.OnRampPackageId, + PackageId: input.PackageId, Objects: AddPackageIdObjects{}, Call: call, }, nil diff --git a/deployment/ops/mcms/op_proposal_generate.go b/deployment/ops/mcms/op_proposal_generate.go index 55cede7c..c4c8eff3 100644 --- a/deployment/ops/mcms/op_proposal_generate.go +++ b/deployment/ops/mcms/op_proposal_generate.go @@ -78,6 +78,13 @@ var generateProposalHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, inpu if err != nil { return mcms.TimelockProposal{}, fmt.Errorf("failed to create transaction for operation %s: %w", def.ID, err) } + // If the op signalled an upgraded package, record it so the timelock converter + // propagates it as InternalLatestPackageIDs in the outer batch operation. + if call.LatestPackageID != "" { + if setErr := suisdk.SetLatestPackageID(&tx, call.LatestPackageID); setErr != nil { + return mcms.TimelockProposal{}, fmt.Errorf("failed to set latest package ID for operation %s: %w", def.ID, setErr) + } + } mcmsTxs[i] = tx } diff --git a/deployment/ops/mcms/op_proposal_generate_test.go b/deployment/ops/mcms/op_proposal_generate_test.go index 19ae50d5..f9eaa080 100644 --- a/deployment/ops/mcms/op_proposal_generate_test.go +++ b/deployment/ops/mcms/op_proposal_generate_test.go @@ -98,10 +98,10 @@ func TestMCMSDynamicProposalGenerateSeq(t *testing.T) { inputs := []any{ ccipops.AddPackageIdStateObjectInput{ - CCIPPackageId: testCCIPPackageId, + PackageId: testCCIPPackageId, CCIPObjectRefObjectId: testObjectRefId, OwnerCapObjectId: testOwnerCapId, - PackageId: testPackageId, + NewPackageId: testPackageId, }, ccipops.TransferOwnershipStateObjectInput{ CCIPPackageId: testCCIPPackageId, @@ -228,10 +228,10 @@ func TestMCMSDynamicProposalGenerateSeq(t *testing.T) { inputs := []any{ ccipops.AddPackageIdStateObjectInput{ - CCIPPackageId: testCCIPPackageId, + PackageId: testCCIPPackageId, CCIPObjectRefObjectId: testObjectRefId, OwnerCapObjectId: testOwnerCapId, - PackageId: testPackageId, + NewPackageId: testPackageId, }, } @@ -270,10 +270,10 @@ func TestMCMSDynamicProposalGenerateSeq(t *testing.T) { inputs := []any{ ccipops.AddPackageIdStateObjectInput{ - CCIPPackageId: testCCIPPackageId, + PackageId: testCCIPPackageId, CCIPObjectRefObjectId: testObjectRefId, OwnerCapObjectId: testOwnerCapId, - PackageId: testPackageId, + NewPackageId: testPackageId, }, // Missing second input diff --git a/deployment/ops/types.go b/deployment/ops/types.go index 19344340..b46f39a0 100644 --- a/deployment/ops/types.go +++ b/deployment/ops/types.go @@ -22,12 +22,13 @@ type OpTxResult[O any] struct { } type TransactionCall struct { - PackageID string - Module string - Function string - Data []byte - StateObjID string - TypeArgs []string + PackageID string + LatestPackageID string // optional: when set, the PTB MoveCall targets this (upgraded) package while PackageID remains the on-chain MCMS identity + Module string + Function string + Data []byte + StateObjID string + TypeArgs []string } type OpTxDeps struct { diff --git a/integration-tests/go.mod b/integration-tests/go.mod index a15e01ee..3a5813e0 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -10,9 +10,9 @@ require ( github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260311190822-5cbfc939dd16 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260406055916-9aa6b6c0ae81 github.com/smartcontractkit/chainlink-deployments-framework v0.75.0 - github.com/smartcontractkit/chainlink-sui v0.0.0-20260205175622-33e65031f9a9 + github.com/smartcontractkit/chainlink-sui v0.0.0-20260408222042-6c7b4c27a8b2 github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20250903045200-c3d973201e55 - github.com/smartcontractkit/mcms v0.40.1 + github.com/smartcontractkit/mcms v0.41.0 // Replace with official release before merging github.com/stretchr/testify v1.11.1 ) diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 7f5517ab..c2b8557e 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -656,8 +656,8 @@ github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12i github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d h1:LokA9PoCNb8mm8mDT52c3RECPMRsGz1eCQORq+J3n74= github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d/go.mod h1:Acy3BTBxou83ooMESLO90s8PKSu7RvLCzwSTbxxfOK0= -github.com/smartcontractkit/mcms v0.40.1 h1:r9bU/2GfIf6mHHM4PklSBkfi2Lq4+EGILC81e4IqKz0= -github.com/smartcontractkit/mcms v0.40.1/go.mod h1:7YqJPR8w9GiO1L/JjjTrwlSwAZ7i3J7cgOcu88PqtvU= +github.com/smartcontractkit/mcms v0.41.0 h1:SKRR/znedyDmj11isYgUW00j9+/n3FwGPHi1L/QWFfk= +github.com/smartcontractkit/mcms v0.41.0/go.mod h1:VTg6wrglc+efxfc7YIk4AcOZlrw1i+vkj5UqzG2p7K8= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= diff --git a/integration-tests/mcms/ccip_test.go b/integration-tests/mcms/ccip_test.go index 8a01e62e..50bb0d79 100644 --- a/integration-tests/mcms/ccip_test.go +++ b/integration-tests/mcms/ccip_test.go @@ -57,25 +57,13 @@ func (s *CCIPMCMSTestSuite) Test_CCIP_MCMS() { s.RegisterRouterUpgradeCap() }) - // CCIP UPGRADE - s.T().Run("Upgrade CCIP through MCMS", func(t *testing.T) { - s.RunUpgradeCCIPProposal("FeeQuoter 1.7.0") + s.T().Run("CCIP/Offramp breaking upgrades with post calls", func(t *testing.T) { + s.RunUpgradeBreakingCCIPProposal() + s.RunUpgradeBreakingOfframpProposal() + s.RunLatestUpgradedCCIPProposal() }) - s.T().Run("Re-Upgrade CCIP through MCMS", func(t *testing.T) { - s.RunUpgradeCCIPProposal("FeeQuoter 1.8.0") - }) - - // CCIP OFFRAMP UPGRADE - s.T().Run("Upgrade CCIPOfframp through MCMS", func(t *testing.T) { - s.RunUpgradeOfframpProposal("OffRamp 1.7.0") - }) - - s.T().Run("Re-Upgrade CCIPOfframp through MCMS", func(t *testing.T) { - s.RunUpgradeOfframpProposal("OffRamp 1.8.0") - }) - - // ONRAMP UPGRADE + // ONRAMP SIMPLE UPGRADE s.T().Run("Upgrade CCIPOnramp through MCMS", func(t *testing.T) { s.RunUpgradeOnrampProposal("OnRamp 1.7.0") }) @@ -84,7 +72,7 @@ func (s *CCIPMCMSTestSuite) Test_CCIP_MCMS() { s.RunUpgradeOnrampProposal("OnRamp 1.8.0") }) - // ROUTER UPGRADE + // ROUTER SIMPLE UPGRADE s.T().Run("Upgrade CCIPRouter through MCMS", func(t *testing.T) { s.RunUpgradeRouterProposal("Router 1.7.0") }) @@ -94,6 +82,107 @@ func (s *CCIPMCMSTestSuite) Test_CCIP_MCMS() { }) } +// RunLatestUpgradedCCIPProposal verifies the InternalLatestPackageIDs feature: a single MCMS bypass +// proposal batches two operations targeting two independently-upgraded packages (CCIP and Offramp). +// Both breaking upgrades made their respective add_package_id a no-op, so executing via MCMS with +// OriginalPackageId→LatestPackageId routing must run upgraded bytecode (no-op) while passing +// on-chain registry validation against the original package's proof type. +func (s *CCIPMCMSTestSuite) RunLatestUpgradedCCIPProposal() { + // 1. Record the current package_ids lists before the proposal. + ccipStateObjectBefore, err := s.client.SuiGetObject(s.T().Context(), models.SuiGetObjectRequest{ + ObjectId: s.ccipObjects.CCIPObjectRefObjectId, + Options: models.SuiObjectDataOptions{ShowContent: true}, + }) + s.Require().NoError(err) + ccipPkgIdsBefore := ccipStateObjectBefore.Data.Content.Fields["package_ids"].([]any) + + offrampStateObjectBefore, err := s.client.SuiGetObject(s.T().Context(), models.SuiGetObjectRequest{ + ObjectId: s.ccipOfframpObjects.StateObjectId, + Options: models.SuiObjectDataOptions{ShowContent: true}, + }) + s.Require().NoError(err) + offrampPkgIdsBefore := offrampStateObjectBefore.Data.Content.Fields["package_ids"].([]any) + + // 2. Build a single bypass proposal batching: + // a) CCIP state_object::add_package_id — upgraded package has push_back removed (no-op) + // b) Offramp::add_package_id — upgraded package has push_back removed (no-op) + // The InternalLatestPackageIDs mechanism routes each PTB MoveCall to the corresponding latest + // (upgraded) package while keeping the original package IDs as on-chain MCMS targets so the + // registry validation passes. + input := mcmsops.ProposalGenerateInput{ + Defs: []cld_ops.Definition{ + ccipops.AddPackageIdStateObjectOp.Def(), + ccipops.AddAllowedModulesStateObjectOp.Def(), + offrampops.AddPackageIdOffRampOp.Def(), + }, + Inputs: []any{ + ccipops.AddPackageIdStateObjectInput{ + PackageId: s.ccipPackageId, // original (MCMS registry identity) + LatestPackageId: s.latestCcipPackageId, // upgraded binary (no-op add_package_id) + NewPackageId: s.latestCcipPackageId, // ID to register in CCIP state + OwnerCapObjectId: s.ccipObjects.OwnerCapObjectId, + CCIPObjectRefObjectId: s.ccipObjects.CCIPObjectRefObjectId, + }, + ccipops.AddAllowedModulesStateObjectInput{ + PackageId: s.ccipPackageId, // original (MCMS registry identity) + LatestPackageId: s.latestCcipPackageId, // upgraded binary (no-op add_allowed_modules) + MCMSRegistryObjId: s.registryObj, + AllowedModules: []string{"dummy"}, + }, + offrampops.AddPackageIdOffRampInput{ + PackageId: s.ccipOfframpPackageId, // original (MCMS registry identity) + LatestPackageId: s.latestCcipOfframpPackageId, // upgraded binary (no-op add_package_id) + NewPackageId: s.latestCcipOfframpPackageId, // ID to register in OffRamp state + OwnerCapObjectId: s.ccipOfframpObjects.OwnerCapId, + StateObjectId: s.ccipOfframpObjects.StateObjectId, + }, + }, + MmcsPackageID: s.mcmsPackageID, + McmsStateObjID: s.mcmsObj, + TimelockObjID: s.timelockObj, + AccountObjID: s.accountObj, + RegistryObjID: s.registryObj, + DeployerStateObjID: s.deployerStateObj, + ChainSelector: uint64(s.chainSelector), + TimelockConfig: utils.TimelockConfig{ + MCMSAction: types.TimelockActionBypass, + MinDelay: 0, + OverrideRoot: false, + }, + } + report, err := cld_ops.ExecuteSequence(s.bundle, mcmsops.MCMSDynamicProposalGenerateSeq, s.deps, input) + s.Require().NoError(err, "building batch proposal via upgraded CCIP and Offramp packages") + + // 3. Execute proposal via bypasser — no timelock delay needed. + s.ExecuteProposalE2e(&report.Output, s.bypasserConfig, 0) + + // 4a. Verify CCIP package_ids did NOT grow: the upgraded add_package_id is a no-op. + ccipStateObjectAfter, err := s.client.SuiGetObject(s.T().Context(), models.SuiGetObjectRequest{ + ObjectId: s.ccipObjects.CCIPObjectRefObjectId, + Options: models.SuiObjectDataOptions{ShowContent: true}, + }) + s.Require().NoError(err) + ccipPkgIdsAfter := ccipStateObjectAfter.Data.Content.Fields["package_ids"].([]any) + + s.Require().Equal(len(ccipPkgIdsBefore), len(ccipPkgIdsAfter), + "CCIP package_ids list should not grow: upgraded add_package_id is a no-op") + s.Require().NotContains(ccipPkgIdsAfter, s.latestCcipPackageId, + "latestCcipPackageId should not be in CCIP package_ids since upgraded add_package_id is a no-op") + + // 4b. Verify Offramp package_ids did NOT grow: the upgraded add_package_id is also a no-op. + offrampStateObjectAfter, err := s.client.SuiGetObject(s.T().Context(), models.SuiGetObjectRequest{ + ObjectId: s.ccipOfframpObjects.StateObjectId, + Options: models.SuiObjectDataOptions{ShowContent: true}, + }) + s.Require().NoError(err) + offrampPkgIdsAfter := offrampStateObjectAfter.Data.Content.Fields["package_ids"].([]any) + + s.Require().Equal(len(offrampPkgIdsBefore), len(offrampPkgIdsAfter), + "Offramp package_ids list should not grow: upgraded add_package_id is a no-op") + s.Require().NotContains(offrampPkgIdsAfter, s.latestCcipOfframpPackageId, + "latestCcipOfframpPackageId should not be in Offramp package_ids since upgraded add_package_id is a no-op") +} + // TODO: For prod env, the initial deployment sequence should start the ownership transfer flow of every deployed contract func RunTestCCIPFeeQuoterProposal(s *CCIPMCMSTestSuite) { // 1. Build configs @@ -138,6 +227,7 @@ func RunTestCCIPFeeQuoterProposal(s *CCIPMCMSTestSuite) { ccipops.FeeQuoterApplyTokenTransferFeeConfigUpdatesOp.Def(), ccipops.FeeQuoterApplyDestChainConfigUpdatesOp.Def(), ccipops.FeeQuoterApplyPremiumMultiplierWeiPerEthUpdatesOp.Def(), + ccipops.AddPackageIdStateObjectOp.Def(), }, Inputs: []any{ ccipops.FeeQuoterApplyFeeTokenUpdatesInput{ @@ -193,6 +283,12 @@ func RunTestCCIPFeeQuoterProposal(s *CCIPMCMSTestSuite) { Tokens: []string{s.linkObjects.CoinMetadataObjectId}, PremiumMultiplierWeiPerEth: []uint64{expectedPremiumMultiplier}, }, + ccipops.AddPackageIdStateObjectInput{ + PackageId: s.ccipPackageId, // original binary (no upgrade) + NewPackageId: s.latestCcipPackageId, // ID to register in CCIP state + OwnerCapObjectId: s.ccipObjects.OwnerCapObjectId, + CCIPObjectRefObjectId: s.ccipObjects.CCIPObjectRefObjectId, + }, }, // MCMS related MmcsPackageID: s.mcmsPackageID, @@ -204,7 +300,7 @@ func RunTestCCIPFeeQuoterProposal(s *CCIPMCMSTestSuite) { ChainSelector: uint64(s.chainSelector), // Proposal TimelockConfig: utils.TimelockConfig{ - MCMSAction: types.TimelockActionBypass, + MCMSAction: types.TimelockActionSchedule, MinDelay: 0, OverrideRoot: false, }, @@ -215,7 +311,7 @@ func RunTestCCIPFeeQuoterProposal(s *CCIPMCMSTestSuite) { timelockProposal := feeQuoterReport.Output // 3. Execute proposal - s.ExecuteProposalE2e(&timelockProposal, s.bypasserConfig, 0) + s.ExecuteProposalE2e(&timelockProposal, s.proposerConfig, 1*time.Second) // 4. Verify the changes in CCIP state object fqContract, err := module_fee_quoter.NewFeeQuoter(s.ccipPackageId, s.client) @@ -243,6 +339,14 @@ func RunTestCCIPFeeQuoterProposal(s *CCIPMCMSTestSuite) { actualPremiumMultiplier, err := fqContract.DevInspect().GetPremiumMultiplierWeiPerEth(s.T().Context(), s.deps.GetCallOpts(), ccipObjRef, linkTokenID) require.NoError(s.T(), err) require.Equal(s.T(), expectedPremiumMultiplier, actualPremiumMultiplier) + + // Add check to verify latest ccip package was added to state object fetching the object directly + ccipStateObject, err := s.client.SuiGetObject(s.T().Context(), models.SuiGetObjectRequest{ + ObjectId: s.ccipObjects.CCIPObjectRefObjectId, + Options: models.SuiObjectDataOptions{ShowContent: true}, + }) + require.NoError(s.T(), err) + require.Contains(s.T(), ccipStateObject.Data.Content.Fields["package_ids"].([]any), s.latestCcipPackageId) } func RunCCIPRampsProposal(s *CCIPMCMSTestSuite) { @@ -469,7 +573,7 @@ func RunTestRouterProposal(s *CCIPMCMSTestSuite) { func (s *CCIPMCMSTestSuite) RegisterCCIPUpgradeCap() { // Register CCIP package's UpgradeCap with MCMS deployer - ccipContract, err := module_state_object.NewStateObject(s.ccipPackageId, s.client) + ccipContract, err := module_state_object.NewStateObject(s.latestCcipPackageId, s.client) require.NoError(s.T(), err, "creating CCIP state object contract") _, err = ccipContract.McmsRegisterUpgradeCap( @@ -533,11 +637,12 @@ func (s *CCIPMCMSTestSuite) RegisterRouterUpgradeCap() { s.T().Logf("✅ Registered CCIPRouter UpgradeCap with MCMS deployer") } -func (s *CCIPMCMSTestSuite) RunUpgradeCCIPProposal(newVersion string) { +func (s *CCIPMCMSTestSuite) RunUpgradeOnrampProposal(newVersion string) { + // Set test modifier to upgrade Onramp version bind.SetTestModifier(func(packageRoot string) error { - sourcePath := filepath.Join(packageRoot, "sources", "fee_quoter.move") + sourcePath := filepath.Join(packageRoot, "sources", "onramp.move") content, _ := os.ReadFile(sourcePath) - re := regexp.MustCompile(`FeeQuoter \d+\.\d+\.\d+`) + re := regexp.MustCompile(`OnRamp \d+\.\d+\.\d+`) modified := re.ReplaceAllString(string(content), newVersion) return os.WriteFile(sourcePath, []byte(modified), 0o644) }) @@ -546,16 +651,16 @@ func (s *CCIPMCMSTestSuite) RunUpgradeCCIPProposal(newVersion string) { signerAddress, err := s.signer.GetAddress() s.Require().NoError(err, "getting signer address") - // 1. Build upgrade input for CCIP package + // 1. Build upgrade input for CCIPOnramp package input := mcmsops.UpgradeCCIPInput{ // Package related - PackageName: contracts.CCIP, - TargetPackageId: s.latestCcipPackageId, + PackageName: contracts.CCIPOnramp, + TargetPackageId: s.latestCcipOnrampPackageId, NamedAddresses: map[string]string{ - "signer": signerAddress, - "mcms": s.mcmsPackageID, - "link": s.linkPackageId, - "original_ccip_pkg": s.ccipPackageId, + "signer": signerAddress, + "mcms": s.mcmsPackageID, + "ccip": s.ccipPackageId, + "original_ccip_onramp_pkg": s.ccipOnrampPackageId, }, ChainSelector: uint64(s.chainSelector), @@ -566,7 +671,7 @@ func (s *CCIPMCMSTestSuite) RunUpgradeCCIPProposal(newVersion string) { TimelockObjID: s.timelockObj, AccountObjID: s.accountObj, DeployerStateObjID: s.deployerStateObj, - OwnerCapObjID: s.ownerCapObj, // MCMS OwnerCap + OwnerCapObjID: s.ownerCapObj, // Timelock related TimelockConfig: utils.TimelockConfig{ @@ -578,11 +683,11 @@ func (s *CCIPMCMSTestSuite) RunUpgradeCCIPProposal(newVersion string) { // 2. Execute operation to generate upgrade proposal upgradeReport, err := cld_ops.ExecuteOperation(s.NewOpBundle(), mcmsops.UpgradeCCIPOp, s.deps, input) - s.Require().NoError(err, "executing CCIP upgrade operation") + s.Require().NoError(err, "executing CCIPOnramp upgrade operation") timelockProposal := upgradeReport.Output - s.T().Logf("✅ Generated CCIP upgrade proposal: %s", timelockProposal.Description) + s.T().Logf("✅ Generated CCIPOnramp upgrade proposal: %s", timelockProposal.Description) // 3. Execute the upgrade proposal through MCMS using Schedule path responses := s.ExecuteProposalE2e(&timelockProposal, s.proposerConfig, 6*time.Second) @@ -594,24 +699,24 @@ func (s *CCIPMCMSTestSuite) RunUpgradeCCIPProposal(newVersion string) { s.Require().NoError(err) s.Require().NotEmpty(newAddress) - s.T().Logf("✅ Successfully upgraded CCIP package from %s to %s", s.ccipPackageId, newAddress) + s.T().Logf("✅ Successfully upgraded CCIPOnramp package from %s to %s", s.ccipOnrampPackageId, newAddress) // 4. Verify the new package version - feequoter, err := module_fee_quoter.NewFeeQuoter(newAddress, s.client) + onramp, err := module_onramp.NewOnramp(newAddress, s.client) s.Require().NoError(err) - version, err := feequoter.DevInspect().TypeAndVersion(s.T().Context(), s.deps.GetCallOpts()) + version, err := onramp.DevInspect().TypeAndVersion(s.T().Context(), s.deps.GetCallOpts()) s.Require().NoError(err) - s.Require().Equal(newVersion, version, "fee quoter version should be upgraded to %s", newVersion) - s.latestCcipPackageId = newAddress + s.Require().Equal(newVersion, version, "onramp version should be upgraded to "+newVersion) + s.latestCcipOnrampPackageId = newAddress } -func (s *CCIPMCMSTestSuite) RunUpgradeOfframpProposal(newVersion string) { - // Set test modifier to upgrade Offramp version +func (s *CCIPMCMSTestSuite) RunUpgradeRouterProposal(newVersion string) { + // Set test modifier to upgrade Router version bind.SetTestModifier(func(packageRoot string) error { - sourcePath := filepath.Join(packageRoot, "sources", "offramp.move") + sourcePath := filepath.Join(packageRoot, "sources", "router.move") content, _ := os.ReadFile(sourcePath) - re := regexp.MustCompile(`OffRamp \d+\.\d+\.\d+`) + re := regexp.MustCompile(`Router \d+\.\d+\.\d+`) modified := re.ReplaceAllString(string(content), newVersion) return os.WriteFile(sourcePath, []byte(modified), 0o644) }) @@ -620,16 +725,15 @@ func (s *CCIPMCMSTestSuite) RunUpgradeOfframpProposal(newVersion string) { signerAddress, err := s.signer.GetAddress() s.Require().NoError(err, "getting signer address") - // 1. Build upgrade input for CCIPOfframp package + // 1. Build upgrade input for CCIPRouter package input := mcmsops.UpgradeCCIPInput{ // Package related - PackageName: contracts.CCIPOfframp, - TargetPackageId: s.latestCcipOfframpPackageId, + PackageName: contracts.CCIPRouter, + TargetPackageId: s.latestCcipRouterPackageId, NamedAddresses: map[string]string{ - "signer": signerAddress, - "mcms": s.mcmsPackageID, - "ccip": s.ccipPackageId, - "original_ccip_offramp_pkg": s.ccipOfframpPackageId, + "signer": signerAddress, + "mcms": s.mcmsPackageID, + "original_ccip_router_pkg": s.ccipRouterPackageId, }, ChainSelector: uint64(s.chainSelector), @@ -652,11 +756,11 @@ func (s *CCIPMCMSTestSuite) RunUpgradeOfframpProposal(newVersion string) { // 2. Execute operation to generate upgrade proposal upgradeReport, err := cld_ops.ExecuteOperation(s.NewOpBundle(), mcmsops.UpgradeCCIPOp, s.deps, input) - s.Require().NoError(err, "executing CCIPOfframp upgrade operation") + s.Require().NoError(err, "executing CCIPRouter upgrade operation") timelockProposal := upgradeReport.Output - s.T().Logf("✅ Generated CCIPOfframp upgrade proposal: %s", timelockProposal.Description) + s.T().Logf("✅ Generated CCIPRouter upgrade proposal: %s", timelockProposal.Description) // 3. Execute the upgrade proposal through MCMS using Schedule path responses := s.ExecuteProposalE2e(&timelockProposal, s.proposerConfig, 6*time.Second) @@ -668,46 +772,49 @@ func (s *CCIPMCMSTestSuite) RunUpgradeOfframpProposal(newVersion string) { s.Require().NoError(err) s.Require().NotEmpty(newAddress) - s.T().Logf("✅ Successfully upgraded CCIPOfframp package from %s to %s", s.ccipOfframpPackageId, newAddress) + s.T().Logf("✅ Successfully upgraded CCIPRouter package from %s to %s", s.ccipRouterPackageId, newAddress) // 4. Verify the new package version - offramp, err := module_offramp.NewOfframp(newAddress, s.client) + router, err := module_router.NewRouter(newAddress, s.client) s.Require().NoError(err) - version, err := offramp.DevInspect().TypeAndVersion(s.T().Context(), s.deps.GetCallOpts()) + version, err := router.DevInspect().TypeAndVersion(s.T().Context(), s.deps.GetCallOpts()) s.Require().NoError(err) - s.Require().Equal(newVersion, version, "offramp version should be upgraded to "+newVersion) - s.latestCcipOfframpPackageId = newAddress + s.Require().Equal(newVersion, version, "router version should be upgraded to "+newVersion) + s.latestCcipRouterPackageId = newAddress } -func (s *CCIPMCMSTestSuite) RunUpgradeOnrampProposal(newVersion string) { - // Set test modifier to upgrade Onramp version +func (s *CCIPMCMSTestSuite) RunUpgradeBreakingOfframpProposal() { bind.SetTestModifier(func(packageRoot string) error { - sourcePath := filepath.Join(packageRoot, "sources", "onramp.move") - content, _ := os.ReadFile(sourcePath) - re := regexp.MustCompile(`OnRamp \d+\.\d+\.\d+`) - modified := re.ReplaceAllString(string(content), newVersion) - return os.WriteFile(sourcePath, []byte(modified), 0o644) + // Bump OffRamp version so the upgraded package bytecode is recognizably different. + offrampPath := filepath.Join(packageRoot, "sources", "offramp.move") + offrampContent, _ := os.ReadFile(offrampPath) + reVersion := regexp.MustCompile(`OffRamp \d+\.\d+\.\d+`) + if err := os.WriteFile(offrampPath, []byte(reVersion.ReplaceAllString(string(offrampContent), "OffRamp 1.9.0")), 0o644); err != nil { + return err + } + // Remove state.package_ids.push_back(package_id) from add_package_id to simulate a + // breaking change: the upgraded contract's add_package_id becomes a no-op, so any MCMS + // proposal targeting this package will not actually store the package ID. + rePushBack := regexp.MustCompile(`(assert!\(object::id\(owner_cap\) == ownable::owner_cap_id\(&state\.ownable_state\)[^\n]+EInvalidOwnerCap\);\n)\s+state\.package_ids\.push_back\(package_id\);`) + return os.WriteFile(offrampPath, []byte(rePushBack.ReplaceAllString(string(reVersion.ReplaceAllString(string(offrampContent), "OffRamp 1.9.0")), "$1")), 0o644) }) defer bind.ClearTestModifier() signerAddress, err := s.signer.GetAddress() s.Require().NoError(err, "getting signer address") - // 1. Build upgrade input for CCIPOnramp package + // Build upgrade input for CCIPOfframp package with the breaking change input := mcmsops.UpgradeCCIPInput{ - // Package related - PackageName: contracts.CCIPOnramp, - TargetPackageId: s.latestCcipOnrampPackageId, + PackageName: contracts.CCIPOfframp, + TargetPackageId: s.latestCcipOfframpPackageId, NamedAddresses: map[string]string{ - "signer": signerAddress, - "mcms": s.mcmsPackageID, - "ccip": s.ccipPackageId, - "original_ccip_onramp_pkg": s.ccipOnrampPackageId, + "signer": signerAddress, + "mcms": s.mcmsPackageID, + "ccip": s.ccipPackageId, + "original_ccip_offramp_pkg": s.ccipOfframpPackageId, }, - - ChainSelector: uint64(s.chainSelector), - // MCMS related + ChainSelector: uint64(s.chainSelector), MmcsPackageID: s.mcmsPackageID, McmsStateObjID: s.mcmsObj, RegistryObjID: s.registryObj, @@ -715,8 +822,6 @@ func (s *CCIPMCMSTestSuite) RunUpgradeOnrampProposal(newVersion string) { AccountObjID: s.accountObj, DeployerStateObjID: s.deployerStateObj, OwnerCapObjID: s.ownerCapObj, - - // Timelock related TimelockConfig: utils.TimelockConfig{ MCMSAction: types.TimelockActionSchedule, MinDelay: 5 * time.Second, @@ -724,15 +829,12 @@ func (s *CCIPMCMSTestSuite) RunUpgradeOnrampProposal(newVersion string) { }, } - // 2. Execute operation to generate upgrade proposal upgradeReport, err := cld_ops.ExecuteOperation(s.NewOpBundle(), mcmsops.UpgradeCCIPOp, s.deps, input) - s.Require().NoError(err, "executing CCIPOnramp upgrade operation") + s.Require().NoError(err, "executing breaking Offramp upgrade operation") timelockProposal := upgradeReport.Output + s.T().Logf("✅ Generated breaking Offramp upgrade proposal: %s", timelockProposal.Description) - s.T().Logf("✅ Generated CCIPOnramp upgrade proposal: %s", timelockProposal.Description) - - // 3. Execute the upgrade proposal through MCMS using Schedule path responses := s.ExecuteProposalE2e(&timelockProposal, s.proposerConfig, 6*time.Second) tx, ok := responses[len(responses)-1].RawData.(*models.SuiTransactionBlockResponse) @@ -742,41 +844,49 @@ func (s *CCIPMCMSTestSuite) RunUpgradeOnrampProposal(newVersion string) { s.Require().NoError(err) s.Require().NotEmpty(newAddress) - s.T().Logf("✅ Successfully upgraded CCIPOnramp package from %s to %s", s.ccipOnrampPackageId, newAddress) + s.T().Logf("✅ Successfully upgraded Offramp package (breaking) from %s to %s", s.ccipOfframpPackageId, newAddress) - // 4. Verify the new package version - onramp, err := module_onramp.NewOnramp(newAddress, s.client) + // Verify the new version + offramp, err := module_offramp.NewOfframp(newAddress, s.client) s.Require().NoError(err) - - version, err := onramp.DevInspect().TypeAndVersion(s.T().Context(), s.deps.GetCallOpts()) + version, err := offramp.DevInspect().TypeAndVersion(s.T().Context(), s.deps.GetCallOpts()) s.Require().NoError(err) - s.Require().Equal(newVersion, version, "onramp version should be upgraded to "+newVersion) - s.latestCcipOnrampPackageId = newAddress + s.Require().Equal("OffRamp 1.9.0", version, "offramp version should be upgraded to OffRamp 1.9.0") + s.latestCcipOfframpPackageId = newAddress } -func (s *CCIPMCMSTestSuite) RunUpgradeRouterProposal(newVersion string) { - // Set test modifier to upgrade Router version +func (s *CCIPMCMSTestSuite) RunUpgradeBreakingCCIPProposal() { bind.SetTestModifier(func(packageRoot string) error { - sourcePath := filepath.Join(packageRoot, "sources", "router.move") - content, _ := os.ReadFile(sourcePath) - re := regexp.MustCompile(`Router \d+\.\d+\.\d+`) - modified := re.ReplaceAllString(string(content), newVersion) - return os.WriteFile(sourcePath, []byte(modified), 0o644) + // Bump fee_quoter version so the upgraded package bytecode is recognizably different. + fqPath := filepath.Join(packageRoot, "sources", "fee_quoter.move") + fqContent, _ := os.ReadFile(fqPath) + reVersion := regexp.MustCompile(`FeeQuoter \d+\.\d+\.\d+`) + if err := os.WriteFile(fqPath, []byte(reVersion.ReplaceAllString(string(fqContent), "FeeQuoter 1.9.0")), 0o644); err != nil { + return err + } + // Remove ref.package_ids.push_back(package_id) from add_package_id to simulate a + // breaking change: the upgraded contract's add_package_id becomes a no-op, so any MCMS + // proposal targeting this package will not actually store the package ID. + soPath := filepath.Join(packageRoot, "sources", "state_object.move") + soContent, _ := os.ReadFile(soPath) + rePushBack := regexp.MustCompile(`(assert!\(object::id\(owner_cap\)[^\n]+EInvalidOwnerCap\);\n)\s+ref\.package_ids\.push_back\(package_id\);`) + return os.WriteFile(soPath, []byte(rePushBack.ReplaceAllString(string(soContent), "$1")), 0o644) }) defer bind.ClearTestModifier() signerAddress, err := s.signer.GetAddress() s.Require().NoError(err, "getting signer address") - // 1. Build upgrade input for CCIPRouter package + // 1. Build upgrade input for CCIP package input := mcmsops.UpgradeCCIPInput{ // Package related - PackageName: contracts.CCIPRouter, - TargetPackageId: s.latestCcipRouterPackageId, + PackageName: contracts.CCIP, + TargetPackageId: s.latestCcipPackageId, NamedAddresses: map[string]string{ - "signer": signerAddress, - "mcms": s.mcmsPackageID, - "original_ccip_router_pkg": s.ccipRouterPackageId, + "signer": signerAddress, + "mcms": s.mcmsPackageID, + "link": s.linkPackageId, + "original_ccip_pkg": s.ccipPackageId, }, ChainSelector: uint64(s.chainSelector), @@ -787,7 +897,7 @@ func (s *CCIPMCMSTestSuite) RunUpgradeRouterProposal(newVersion string) { TimelockObjID: s.timelockObj, AccountObjID: s.accountObj, DeployerStateObjID: s.deployerStateObj, - OwnerCapObjID: s.ownerCapObj, + OwnerCapObjID: s.ownerCapObj, // MCMS OwnerCap // Timelock related TimelockConfig: utils.TimelockConfig{ @@ -799,11 +909,11 @@ func (s *CCIPMCMSTestSuite) RunUpgradeRouterProposal(newVersion string) { // 2. Execute operation to generate upgrade proposal upgradeReport, err := cld_ops.ExecuteOperation(s.NewOpBundle(), mcmsops.UpgradeCCIPOp, s.deps, input) - s.Require().NoError(err, "executing CCIPRouter upgrade operation") + s.Require().NoError(err, "executing CCIP upgrade operation") timelockProposal := upgradeReport.Output - s.T().Logf("✅ Generated CCIPRouter upgrade proposal: %s", timelockProposal.Description) + s.T().Logf("✅ Generated CCIP upgrade proposal: %s", timelockProposal.Description) // 3. Execute the upgrade proposal through MCMS using Schedule path responses := s.ExecuteProposalE2e(&timelockProposal, s.proposerConfig, 6*time.Second) @@ -815,14 +925,14 @@ func (s *CCIPMCMSTestSuite) RunUpgradeRouterProposal(newVersion string) { s.Require().NoError(err) s.Require().NotEmpty(newAddress) - s.T().Logf("✅ Successfully upgraded CCIPRouter package from %s to %s", s.ccipRouterPackageId, newAddress) + s.T().Logf("✅ Successfully upgraded CCIP package from %s to %s", s.ccipPackageId, newAddress) // 4. Verify the new package version - router, err := module_router.NewRouter(newAddress, s.client) + feequoter, err := module_fee_quoter.NewFeeQuoter(newAddress, s.client) s.Require().NoError(err) - version, err := router.DevInspect().TypeAndVersion(s.T().Context(), s.deps.GetCallOpts()) + version, err := feequoter.DevInspect().TypeAndVersion(s.T().Context(), s.deps.GetCallOpts()) s.Require().NoError(err) - s.Require().Equal(newVersion, version, "router version should be upgraded to "+newVersion) - s.latestCcipRouterPackageId = newAddress + s.Require().Equal("FeeQuoter 1.9.0", version, "fee quoter version should be upgraded to FeeQuoter 1.9.0") + s.latestCcipPackageId = newAddress } diff --git a/scripts/go.mod b/scripts/go.mod index c10af3ff..008aaf2c 100644 --- a/scripts/go.mod +++ b/scripts/go.mod @@ -130,13 +130,13 @@ require ( github.com/smartcontractkit/chainlink-protos/job-distributor v0.17.0 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b // indirect - github.com/smartcontractkit/chainlink-sui v0.0.0-20260205175622-33e65031f9a9 // indirect + github.com/smartcontractkit/chainlink-sui v0.0.0-20260408222042-6c7b4c27a8b2 // indirect github.com/smartcontractkit/chainlink-ton v0.0.0-20260219201907-054376f21418 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20250908203554-5bd9d2fe9513 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d // indirect - github.com/smartcontractkit/mcms v0.40.1 // indirect + github.com/smartcontractkit/mcms v0.41.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863 // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect diff --git a/scripts/go.sum b/scripts/go.sum index 0f735058..197e12f4 100644 --- a/scripts/go.sum +++ b/scripts/go.sum @@ -652,8 +652,8 @@ github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12i github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d h1:LokA9PoCNb8mm8mDT52c3RECPMRsGz1eCQORq+J3n74= github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d/go.mod h1:Acy3BTBxou83ooMESLO90s8PKSu7RvLCzwSTbxxfOK0= -github.com/smartcontractkit/mcms v0.40.1 h1:r9bU/2GfIf6mHHM4PklSBkfi2Lq4+EGILC81e4IqKz0= -github.com/smartcontractkit/mcms v0.40.1/go.mod h1:7YqJPR8w9GiO1L/JjjTrwlSwAZ7i3J7cgOcu88PqtvU= +github.com/smartcontractkit/mcms v0.41.0 h1:SKRR/znedyDmj11isYgUW00j9+/n3FwGPHi1L/QWFfk= +github.com/smartcontractkit/mcms v0.41.0/go.mod h1:VTg6wrglc+efxfc7YIk4AcOZlrw1i+vkj5UqzG2p7K8= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=