Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions deployment/changesets/cs_curse_uncurse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package changesets

import (
"encoding/binary"
"errors"
"fmt"

cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
"github.com/smartcontractkit/chainlink-deployments-framework/operations"

"github.com/smartcontractkit/chainlink-sui/bindings/bind"
"github.com/smartcontractkit/chainlink-sui/deployment"
sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops"
rmn_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops/rmn"
)

type CurseUncurseOperationType string

const (
CurseOperationType CurseUncurseOperationType = "curse"
UncurseOperationType CurseUncurseOperationType = "uncurse"
)

type CurseUncurseChainsConfig struct {
SuiChainSelector uint64 `yaml:"suiChainSelector"`
OperationType string `yaml:"operationType"`
IsGlobalCurse bool `yaml:"isGlobalCurse"`
DestChainSelectors []uint64 `yaml:"destChainSelectors"`
}

var _ cldf.ChangeSetV2[CurseUncurseChainsConfig] = CurseUncurseChains{}

type CurseUncurseChains struct{}

var globalCurseSubjectBytes = [16]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}

func (c CurseUncurseChains) VerifyPreconditions(e cldf.Environment, cfg CurseUncurseChainsConfig) error {
if cfg.OperationType != string(CurseOperationType) && cfg.OperationType != string(UncurseOperationType) {
return fmt.Errorf("invalid operation type %s", cfg.OperationType)
}
if cfg.IsGlobalCurse {
if len(cfg.DestChainSelectors) > 0 {
return errors.New("global curse config must not include destination selectors")
}
return nil
}
if len(cfg.DestChainSelectors) == 0 {
return errors.New("no destination chain selectors provided")
}
return nil
}

func (c CurseUncurseChains) Apply(e cldf.Environment, cfg CurseUncurseChainsConfig) (cldf.ChangesetOutput, error) {
state, err := deployment.LoadOnchainStatesui(e)
if err != nil {
return cldf.ChangesetOutput{}, err
}

chainState, ok := state[cfg.SuiChainSelector]
if !ok {
return cldf.ChangesetOutput{}, fmt.Errorf("no Sui chain state for selector %d", cfg.SuiChainSelector)
}
if chainState.CCIPAddress == "" {
return cldf.ChangesetOutput{}, fmt.Errorf("missing CCIP package address for chain %d", cfg.SuiChainSelector)
}
if chainState.CCIPObjectRef == "" {
return cldf.ChangesetOutput{}, fmt.Errorf("missing CCIP object ref for chain %d", cfg.SuiChainSelector)
}
if chainState.CCIPOwnerCapObjectId == "" {
return cldf.ChangesetOutput{}, fmt.Errorf("missing CCIP owner cap object id for chain %d", cfg.SuiChainSelector)
}

suiChain, ok := e.BlockChains.SuiChains()[cfg.SuiChainSelector]
if !ok {
return cldf.ChangesetOutput{}, fmt.Errorf("no Sui chain client for selector %d", cfg.SuiChainSelector)
}

subjects, err := buildCurseSubjects(cfg)
if err != nil {
return cldf.ChangesetOutput{}, err
}

deps := sui_ops.OpTxDeps{
Client: suiChain.Client,
Signer: suiChain.Signer,
GetCallOpts: func() *bind.CallOpts {
gasBudget := uint64(400_000_000)
return &bind.CallOpts{WaitForExecution: true, GasBudget: &gasBudget}
},
SuiRPC: suiChain.URL,
}

input := rmn_ops.CurseUncurseChainInput{
CCIPPackageId: chainState.CCIPAddress,
StateObjectId: chainState.CCIPObjectRef,
OwnerCapObjectId: chainState.CCIPOwnerCapObjectId,
Subjects: subjects,
}

var genericReport operations.Report[any, any]
if cfg.OperationType == string(UncurseOperationType) {
report, execErr := operations.ExecuteOperation(e.OperationsBundle, rmn_ops.UncurseChainOp, deps, input)
if execErr != nil {
return cldf.ChangesetOutput{}, execErr
}
genericReport = report.ToGenericReport()
} else {
report, execErr := operations.ExecuteOperation(e.OperationsBundle, rmn_ops.CurseChainOp, deps, input)
if execErr != nil {
return cldf.ChangesetOutput{}, execErr
}
genericReport = report.ToGenericReport()
}

return cldf.ChangesetOutput{Reports: []operations.Report[any, any]{genericReport}}, nil
}

func buildCurseSubjects(cfg CurseUncurseChainsConfig) ([][]byte, error) {
if cfg.IsGlobalCurse {
subject := make([]byte, len(globalCurseSubjectBytes))
copy(subject, globalCurseSubjectBytes[:])
return [][]byte{subject}, nil
}
subjects := make([][]byte, 0, len(cfg.DestChainSelectors))
for _, selector := range cfg.DestChainSelectors {
subjects = append(subjects, selectorToSubject(selector))
}
return subjects, nil
}

func selectorToSubject(selector uint64) []byte {
subject := make([]byte, 16)
binary.BigEndian.PutUint64(subject[8:], selector)
return subject
}
146 changes: 146 additions & 0 deletions deployment/ops/rmn/op_curse_uncurse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package rmn

import (
"fmt"

"github.com/Masterminds/semver/v3"

cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations"

"github.com/smartcontractkit/chainlink-sui/bindings/bind"
module_rmn_remote "github.com/smartcontractkit/chainlink-sui/bindings/generated/ccip/ccip/rmn_remote"
sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops"
)

type NoObjects struct{}

type CurseUncurseChainInput struct {
CCIPPackageId string
StateObjectId string
OwnerCapObjectId string
Subjects [][]byte
}

var CurseChainOp = cld_ops.NewOperation(
sui_ops.NewSuiOperationName("ccip", "rmn_remote", "curse_chain"),
semver.MustParse("0.1.0"),
"Curse a chain selector in the CCIP RMN Remote contract",
curseChainHandler,
)

func curseChainHandler(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input CurseUncurseChainInput) (output sui_ops.OpTxResult[NoObjects], err error) {
if len(input.Subjects) == 0 {
return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("at least one subject is required to curse")
}

contract, err := module_rmn_remote.NewRmnRemote(input.CCIPPackageId, deps.Client)
if err != nil {
return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to create RMN Remote contract: %w", err)
}

encodedCall, err := contract.Encoder().CurseMultiple(
bind.Object{Id: input.StateObjectId},
bind.Object{Id: input.OwnerCapObjectId},
input.Subjects,
)
if err != nil {
return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to encode curse call: %w", err)
}

call, err := sui_ops.ToTransactionCall(encodedCall, input.StateObjectId)
if err != nil {
return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to build transaction call for curse: %w", err)
}

if deps.Signer == nil {
b.Logger.Infow("Skipping execution of curse_chain on RMN Remote as no signer provided")
return sui_ops.OpTxResult[NoObjects]{
Digest: "",
PackageId: input.CCIPPackageId,
Objects: NoObjects{},
Call: call,
}, nil
}

opts := deps.GetCallOpts()
opts.Signer = deps.Signer
tx, err := contract.Bound().ExecuteTransaction(
b.GetContext(),
opts,
encodedCall,
)
if err != nil {
return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to execute curse_chain on RMN Remote: %w", err)
}

b.Logger.Infow("Chains cursed on RMN Remote", "digest", tx.Digest, "count", len(input.Subjects))

return sui_ops.OpTxResult[NoObjects]{
Digest: tx.Digest,
PackageId: input.CCIPPackageId,
Objects: NoObjects{},
Call: call,
}, nil
}

var UncurseChainOp = cld_ops.NewOperation(
sui_ops.NewSuiOperationName("ccip", "rmn_remote", "uncurse_chain"),
semver.MustParse("0.1.0"),
"Uncurse a chain selector in the CCIP RMN Remote contract",
uncurseChainHandler,
)

func uncurseChainHandler(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input CurseUncurseChainInput) (output sui_ops.OpTxResult[NoObjects], err error) {
if len(input.Subjects) == 0 {
return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("at least one subject is required to uncurse")
}

contract, err := module_rmn_remote.NewRmnRemote(input.CCIPPackageId, deps.Client)
if err != nil {
return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to create RMN Remote contract: %w", err)
}

encodedCall, err := contract.Encoder().UncurseMultiple(
bind.Object{Id: input.StateObjectId},
bind.Object{Id: input.OwnerCapObjectId},
input.Subjects,
)
if err != nil {
return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to encode uncurse call: %w", err)
}

call, err := sui_ops.ToTransactionCall(encodedCall, input.StateObjectId)
if err != nil {
return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to build transaction call for uncurse: %w", err)
}

if deps.Signer == nil {
b.Logger.Infow("Skipping execution of uncurse_chain on RMN Remote as no signer provided")
return sui_ops.OpTxResult[NoObjects]{
Digest: "",
PackageId: input.CCIPPackageId,
Objects: NoObjects{},
Call: call,
}, nil
}

opts := deps.GetCallOpts()
opts.Signer = deps.Signer
tx, err := contract.Bound().ExecuteTransaction(
b.GetContext(),
opts,
encodedCall,
)
if err != nil {
return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to execute uncurse_chain on RMN Remote: %w", err)
}

b.Logger.Infow("Chains uncursed on RMN Remote", "digest", tx.Digest, "count", len(input.Subjects))

return sui_ops.OpTxResult[NoObjects]{
Digest: tx.Digest,
PackageId: input.CCIPPackageId,
Objects: NoObjects{},
Call: call,
}, nil
}
57 changes: 57 additions & 0 deletions integration-tests/deploy/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package deploy

import (
"bytes"
"encoding/binary"
"fmt"

"testing"
Expand All @@ -11,6 +13,8 @@ import (

cselectors "github.com/smartcontractkit/chain-selectors"

"github.com/smartcontractkit/chainlink-sui/bindings/bind"
module_rmn_remote "github.com/smartcontractkit/chainlink-sui/bindings/generated/ccip/ccip/rmn_remote"
"github.com/smartcontractkit/chainlink-sui/deployment"
"github.com/smartcontractkit/chainlink-sui/deployment/changesets"
burnminttokenpoolops "github.com/smartcontractkit/chainlink-sui/deployment/ops/ccip_burn_mint_token_pool"
Expand Down Expand Up @@ -48,6 +52,28 @@ func (s *DeployTestSuite) TestDeployAndConfigureSuiChain() {
// Phase 10: Deploy Managed Token Pool
s.DeployManagedTokenPool()

// Phase 11: Test curse/uncurse RMN subjects
// this test is placed here since we have a full deployment to test against
curseCfg := changesets.CurseUncurseChainsConfig{
SuiChainSelector: SuiChainSelector,
OperationType: string(changesets.CurseOperationType),
IsGlobalCurse: false,
DestChainSelectors: []uint64{EVMChainSelector},
}
curseOut, err := changesets.CurseUncurseChains{}.Apply(s.env, curseCfg)
s.Require().NoError(err, "failed to curse RMN subjects")
s.Require().Len(curseOut.Reports, 1, "expected single curse report")

s.assertRMNCurseSubjects(EVMChainSelector, true)

uncurseCfg := curseCfg
uncurseCfg.OperationType = string(changesets.UncurseOperationType)
uncurseOut, err := changesets.CurseUncurseChains{}.Apply(s.env, uncurseCfg)
s.Require().NoError(err, "failed to uncurse RMN subjects")
s.Require().Len(uncurseOut.Reports, 1, "expected single uncurse report")

s.assertRMNCurseSubjects(EVMChainSelector, false)

// Load view and check deployments
states, err := deployment.LoadOnchainStatesui(s.env)
state := states[cselectors.SUI_LOCALNET.Selector]
Expand Down Expand Up @@ -459,3 +485,34 @@ func (s *DeployTestSuite) DeployManagedTokenPool() {
err = s.env.ExistingAddresses.Merge(tokenPoolOut.AddressBook)
s.Require().NoError(err, "failed to merge managed token pool addresses")
}

func (s *DeployTestSuite) assertRMNCurseSubjects(selector uint64, expectCursed bool) {
s.T().Helper()

s.Require().NotEmpty(s.ccipPackageID, "CCIP package ID not set for RMN assertions")
s.Require().NotEmpty(s.ccipObjectRef, "CCIP object ref not set for RMN assertions")

contract, err := module_rmn_remote.NewRmnRemote(s.ccipPackageID, s.client)
s.Require().NoError(err, "failed to create RMN remote binding")

callOpts := &bind.CallOpts{Signer: s.signer}
subjects, err := contract.DevInspect().GetCursedSubjects(s.T().Context(), callOpts, bind.Object{Id: s.ccipObjectRef})
s.Require().NoError(err, "failed to fetch cursed subjects")

target := make([]byte, 16)
binary.BigEndian.PutUint64(target[8:], selector)
found := false
for _, subj := range subjects {
if bytes.Equal(subj, target) {
found = true
break
}
}

if expectCursed {
s.Require().True(found, "expected selector to be cursed")
return
}

s.Require().False(found, "expected selector to be uncursed")
}
Loading