Repository: smartcontractkit/chainlink
Pull request #17124: NONEVM-1563: CCIP SendMessage CLD Support
Author: login: chris-de-leon-cll
Created: 2025-04-06T21:13:09Z | Merged: 2025-04-11T05:17:43Z

# CCIP SendMessage CLD Support

## Primary Changes
- [feat]: remove testing references from `SendRequest` function in CCIP `test_helpers.go`
- [fix]: allow updates to be performed on Solana token pool configs


Diff:
diff --git a/deployment/ccip/changeset/solana/cs_billing.go b/deployment/ccip/changeset/solana/cs_billing.go
index e13a30af047..3e8b51c8ec0 100644
--- a/deployment/ccip/changeset/solana/cs_billing.go
+++ b/deployment/ccip/changeset/solana/cs_billing.go
@@ -190,15 +190,7 @@ func (cfg TokenTransferFeeForRemoteChainConfig) Validate(e deployment.Environmen
 	if err := validateFeeQuoterConfig(chain, chainState); err != nil {
 		return fmt.Errorf("fee quoter validation failed: %w", err)
 	}
-	// check if desired state already exists
-	remoteBillingPDA, _, err := solState.FindFqPerChainPerTokenConfigPDA(cfg.RemoteChainSelector, tokenPubKey, chainState.FeeQuoter)
-	if err != nil {
-		return fmt.Errorf("failed to find remote billing token config pda for (remoteSelector: %d, mint: %s, feeQuoter: %s): %w", cfg.RemoteChainSelector, tokenPubKey.String(), chainState.FeeQuoter.String(), err)
-	}
-	var remoteBillingAccount solFeeQuoter.PerChainPerTokenConfig
-	if err := chain.GetAccountDataBorshInto(context.Background(), remoteBillingPDA, &remoteBillingAccount); err == nil {
-		return fmt.Errorf("billing token config already exists for (remoteSelector: %d, mint: %s, feeQuoter: %s)", cfg.RemoteChainSelector, tokenPubKey.String(), chainState.FeeQuoter.String())
-	}
+
 	return nil
 }
 
diff --git a/deployment/ccip/changeset/testhelpers/messagelimitationstest/helpers.go b/deployment/ccip/changeset/testhelpers/messagelimitationstest/helpers.go
index a13d39b1a6b..04771f007db 100644
--- a/deployment/ccip/changeset/testhelpers/messagelimitationstest/helpers.go
+++ b/deployment/ccip/changeset/testhelpers/messagelimitationstest/helpers.go
@@ -98,7 +98,7 @@ func Run(tc TestCase) TestCaseOutput {
 	}
 
 	msgSentEvent, err := testhelpers.SendRequest(
-		tc.T, tc.Env, tc.OnchainState,
+		tc.Env, tc.OnchainState,
 		testhelpers.WithSourceChain(tc.SrcChain),
 		testhelpers.WithDestChain(tc.DestChain),
 		testhelpers.WithTestRouter(tc.TestRouter),
diff --git a/deployment/ccip/changeset/testhelpers/test_helpers.go b/deployment/ccip/changeset/testhelpers/test_helpers.go
index 90badce2f25..5bf0de4e4c8 100644
--- a/deployment/ccip/changeset/testhelpers/test_helpers.go
+++ b/deployment/ccip/changeset/testhelpers/test_helpers.go
@@ -63,7 +63,6 @@ import (
 	solTestTokenPool "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/test_token_pool"
 
 	solconfig "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config"
-	soltestutils "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/testutils"
 	solccip "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/ccip"
 	solcommon "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common"
 	solstate "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/state"
@@ -393,7 +392,7 @@ func TestSendRequest(
 	}
 	baseOpts = append(baseOpts, opts...)
 
-	msgSentEvent, err := SendRequest(t, e, state, baseOpts...)
+	msgSentEvent, err := SendRequest(e, state, baseOpts...)
 	require.NoError(t, err)
 	return msgSentEvent
 }
@@ -455,7 +454,6 @@ func WithDestChain(destChain uint64) SendReqOpts {
 
 // SendRequest similar to TestSendRequest but returns an error.
 func SendRequest(
-	t *testing.T,
 	e deployment.Environment,
 	state changeset.CCIPOnChainState,
 	opts ...SendReqOpts,
@@ -465,21 +463,21 @@ func SendRequest(
 		opt(cfg)
 	}
 	family, err := chainsel.GetSelectorFamily(cfg.SourceChain)
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
 
 	switch family {
 	case chainsel.FamilyEVM:
-		return SendRequestEVM(t, e, state, cfg)
+		return SendRequestEVM(e, state, cfg)
 	case chainsel.FamilySolana:
-		return SendRequestSol(t, e, state, cfg)
+		return SendRequestSol(e, state, cfg)
 	default:
-		t.Errorf("send request: unsupported chain family: %v", family)
-		return nil, nil
+		return nil, fmt.Errorf("send request: unsupported chain family: %v", family)
 	}
 }
 
 func SendRequestEVM(
-	t *testing.T,
 	e deployment.Environment,
 	state changeset.CCIPOnChainState,
 	cfg *CCIPSendReqConfig,
@@ -488,8 +486,10 @@ func SendRequestEVM(
 	if cfg.Sender == nil {
 		cfg.Sender = e.Chains[cfg.SourceChain].DeployerKey
 	}
-	t.Logf("Sending CCIP request from chain selector %d to chain selector %d from sender %s",
+
+	e.Logger.Infof("Sending CCIP request from chain selector %d to chain selector %d from sender %s",
 		cfg.SourceChain, cfg.DestChain, cfg.Sender.From.String())
+
 	tx, blockNum, err := CCIPSendRequest(e, state, cfg)
 	if err != nil {
 		return nil, err
@@ -504,9 +504,12 @@ func SendRequestEVM(
 		return nil, err
 	}
 
-	require.True(t, it.Next())
-	t.Logf("CCIP message (id %x) sent from chain selector %d to chain selector %d tx %s seqNum %d nonce %d sender %s testRouterEnabled %t",
-		it.Event.Message.Header.MessageId[:],
+	if !it.Next() {
+		return nil, errors.New("no CCIP message sent event found")
+	}
+
+	e.Logger.Infof("CCIP message (id %s) sent from chain selector %d to chain selector %d tx %s seqNum %d nonce %d sender %s testRouterEnabled %t",
+		common.Bytes2Hex(it.Event.Message.Header.MessageId[:]),
 		cfg.SourceChain,
 		cfg.DestChain,
 		tx.Hash().String(),
@@ -519,7 +522,6 @@ func SendRequestEVM(
 }
 
 func SendRequestSol(
-	t *testing.T,
 	e deployment.Environment,
 	state changeset.CCIPOnChainState,
 	cfg *CCIPSendReqConfig,
@@ -537,39 +539,60 @@ func SendRequestSol(
 		message.FeeToken = s.WSOL
 	}
 
-	t.Logf("Sending CCIP request from chain selector %d to chain selector %d from sender %s",
+	e.Logger.Infof("Sending CCIP request from chain selector %d to chain selector %d from sender %s",
 		cfg.SourceChain, cfg.DestChain, sender.String())
 
 	client := e.SolChains[cfg.SourceChain].Client
-	ctx := t.Context()
+	ctx := e.GetContext()
 
 	destinationChainSelector := cfg.DestChain
 
 	destinationChainStatePDA, err := solstate.FindDestChainStatePDA(destinationChainSelector, s.Router)
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
 
 	noncePDA, err := solstate.FindNoncePDA(cfg.DestChain, sender.PublicKey(), s.Router)
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
 
 	feeToken := message.FeeToken
 
 	linkFqBillingConfigPDA, _, err := solstate.FindFqBillingTokenConfigPDA(s.LinkToken, s.FeeQuoter)
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
+
 	feeTokenFqBillingConfigPDA, _, err := solstate.FindFqBillingTokenConfigPDA(feeToken, s.FeeQuoter)
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
 
 	billingSignerPDA, _, err := solstate.FindFeeBillingSignerPDA(s.Router)
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
 
 	feeTokenUserATA, _, err := soltokens.FindAssociatedTokenAddress(solana.TokenProgramID, feeToken, sender.PublicKey())
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
+
 	feeTokenReceiverATA, _, err := soltokens.FindAssociatedTokenAddress(solana.TokenProgramID, feeToken, billingSignerPDA)
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
+
 	fqDestChainPDA, _, err := solstate.FindFqDestChainPDA(cfg.DestChain, s.FeeQuoter)
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
 
 	rmnRemoteCursesPDA, _, err := solstate.FindRMNRemoteCursesPDA(s.RMNRemote)
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
 
 	base := ccip_router.NewCcipSendInstruction(
 		destinationChainSelector,
@@ -609,73 +632,133 @@ func SendRequestSol(
 	for _, tokenAmount := range message.TokenAmounts {
 		token := tokenAmount.Token
 		tokenPool, err := soltokens.NewTokenPool(solana.Token2022ProgramID, s.BurnMintTokenPool, token)
-		require.NoError(t, err)
+		if err != nil {
+			return nil, err
+		}
 
 		// Set the token pool's lookup table address
 		var tokenAdminRegistry solCommon.TokenAdminRegistry
 		err = solcommon.GetAccountDataBorshInto(ctx, client, tokenPool.AdminRegistryPDA, solconfig.DefaultCommitment, &tokenAdminRegistry)
-		require.NoError(t, err)
+		if err != nil {
+			return nil, err
+		}
+
 		tokenPool.PoolLookupTable = tokenAdminRegistry.LookupTable
 
 		// invalid config account, maybe this billing stuff isn't right
 
 		chainPDA, _, err := soltokens.TokenPoolChainConfigPDA(cfg.DestChain, token, s.BurnMintTokenPool)
-		require.NoError(t, err)
+		if err != nil {
+			return nil, err
+		}
+
 		tokenPool.Chain[cfg.DestChain] = chainPDA
 
 		billingPDA, _, err := solstate.FindFqPerChainPerTokenConfigPDA(cfg.DestChain, token, s.FeeQuoter)
-		require.NoError(t, err)
+		if err != nil {
+			return nil, err
+		}
+
 		tokenPool.Billing[cfg.DestChain] = billingPDA
 
 		userTokenAccount, _, err := soltokens.FindAssociatedTokenAddress(solana.Token2022ProgramID, token, sender.PublicKey())
-		require.NoError(t, err)
+		if err != nil {
+			return nil, err
+		}
 
 		tokenMetas, tokenAddressTables, err := soltokens.ParseTokenLookupTableWithChain(ctx, client, tokenPool, userTokenAccount, cfg.DestChain)
-		require.NoError(t, err)
+		if err != nil {
+			return nil, err
+		}
+
 		tokenIndexes = append(tokenIndexes, byte(len(base.AccountMetaSlice)-requiredAccounts))
 		base.AccountMetaSlice = append(base.AccountMetaSlice, tokenMetas...)
 		maps.Copy(addressTables, tokenAddressTables)
 	}
+
 	base.SetTokenIndexes(tokenIndexes)
 
 	ix, err := base.ValidateAndBuild()
-	require.NoError(t, err)
+	if err != nil {
+		return nil, err
+	}
 
 	// for some reason onchain doesn't see extraAccounts
 
 	ixs := []solana.Instruction{ix}
-	result := soltestutils.SendAndConfirmWithLookupTables(ctx, t, client, ixs, *sender, solconfig.DefaultCommitment, addressTables, solcommon.AddComputeUnitLimit(300_000))
-	require.NotNil(t, result)
+	result, err := solcommon.SendAndConfirmWithLookupTables(ctx, client, ixs, *sender, solconfig.DefaultCommitment, addressTables, solcommon.AddComputeUnitLimit(300_000))
+	if err != nil {
+		return nil, err
+	}
 
 	// check CCIP event
 	ccipMessageSentEvent := solccip.EventCCIPMessageSent{}
 	printEvents := true
-	require.NoError(t, solcommon.ParseEvent(result.Meta.LogMessages, "CCIPMessageSent", &ccipMessageSentEvent, printEvents))
-	require.Equal(t, len(message.TokenAmounts), len(ccipMessageSentEvent.Message.TokenAmounts))
+	err = solcommon.ParseEvent(result.Meta.LogMessages, "CCIPMessageSent", &ccipMessageSentEvent, printEvents)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(message.TokenAmounts) != len(ccipMessageSentEvent.Message.TokenAmounts) {
+		return nil, errors.New("token amounts mismatch")
+	}
 
 	// TODO: fee bumping?
 
-	// ---
+	transactionID := "N/A"
+	if tx, err := result.Transaction.GetTransaction(); err != nil {
+		e.Logger.Warnf("could not obtain transaction details (err = %s)", err.Error())
+	} else if len(tx.Signatures) == 0 {
+		e.Logger.Warnf("transaction has no signatures: %v", tx)
+	} else {
+		transactionID = tx.Signatures[0].String()
+	}
 
-	// t.Logf("CCIP message (id %x) sent from chain selector %d to chain selector %d tx %s seqNum %d nonce %d sender %s testRouterEnabled %t",
-	// 	it.Event.Message.Header.MessageId[:],
-	// 	cfg.SourceChain,
-	// 	cfg.DestChain,
-	// 	tx.Hash().String(),
-	// 	it.Event.SequenceNumber,
-	// 	it.Event.Message.Header.Nonce,
-	// 	it.Event.Message.Sender.String(),
-	// 	cfg.IsTestRouter,
-	// )
+	e.Logger.Infof("CCIP message (id %s) sent from chain selector %d to chain selector %d tx %s seqNum %d nonce %d sender %s testRouterEnabled %t",
+		common.Bytes2Hex(ccipMessageSentEvent.Message.Header.MessageId[:]),
+		cfg.SourceChain,
+		cfg.DestChain,
+		transactionID,
+		ccipMessageSentEvent.SequenceNumber,
+		ccipMessageSentEvent.Message.Header.Nonce,
+		ccipMessageSentEvent.Message.Sender.String(),
+		cfg.IsTestRouter,
+	)
 
 	return &onramp.OnRampCCIPMessageSent{
 		DestChainSelector: ccipMessageSentEvent.DestinationChainSelector,
 		SequenceNumber:    ccipMessageSentEvent.SequenceNumber,
-		Message:           onramp.InternalEVM2AnyRampMessage{}, // TODO:
-		Raw:               types.Log{},
+		Message: onramp.InternalEVM2AnyRampMessage{
+			Header: onramp.InternalRampMessageHeader{
+				SourceChainSelector: ccipMessageSentEvent.Message.Header.SourceChainSelector,
+				DestChainSelector:   ccipMessageSentEvent.Message.Header.DestChainSelector,
+				MessageId:           ccipMessageSentEvent.Message.Header.MessageId,
+				SequenceNumber:      ccipMessageSentEvent.SequenceNumber,
+				Nonce:               ccipMessageSentEvent.Message.Header.Nonce,
+			},
+			FeeTokenAmount: ConvertSolanaCrossChainAmountToBigInt(ccipMessageSentEvent.Message.FeeTokenAmount),
+			FeeValueJuels:  ConvertSolanaCrossChainAmountToBigInt(ccipMessageSentEvent.Message.FeeValueJuels),
+			ExtraArgs:      ccipMessageSentEvent.Message.ExtraArgs,
+			Receiver:       ccipMessageSentEvent.Message.Receiver,
+			Data:           ccipMessageSentEvent.Message.Data,
+
+			// TODO: these fields are EVM specific - need to revisit for Solana
+			FeeToken:     common.Address{}, // ccipMessageSentEvent.Message.FeeToken
+			Sender:       common.Address{}, // ccipMessageSentEvent.Message.Sender
+			TokenAmounts: []onramp.InternalEVM2AnyTokenTransfer{},
+		},
+
+		// TODO: EVM specific - need to revisit for Solana
+		Raw: types.Log{},
 	}, nil
 }
 
+func ConvertSolanaCrossChainAmountToBigInt(amount ccip_router.CrossChainAmount) *big.Int {
+	bytes := amount.LeBytes[:]
+	slices.Reverse(bytes) // convert to big-endian
+	return big.NewInt(0).SetBytes(bytes)
+}
+
 // MakeEVMExtraArgsV2 creates the extra args for the EVM2Any message that is destined
 // for an EVM chain. The extra args contain the gas limit and allow out of order flag.
 func MakeEVMExtraArgsV2(gasLimit uint64, allowOOO bool) []byte {
diff --git a/integration-tests/smoke/ccip/ccip_disable_lane_test.go b/integration-tests/smoke/ccip/ccip_disable_lane_test.go
index 2ebd8f14ccc..dc26de51813 100644
--- a/integration-tests/smoke/ccip/ccip_disable_lane_test.go
+++ b/integration-tests/smoke/ccip/ccip_disable_lane_test.go
@@ -47,7 +47,6 @@ func TestDisableLane(t *testing.T) {
 		noOfRequests           = 3
 		sendmessage            = func(src, dest uint64, deployer *bind.TransactOpts) (*onramp.OnRampCCIPMessageSent, error) {
 			return testhelpers.SendRequest(
-				t,
 				e,
 				state,
 				testhelpers.WithSender(deployer),
diff --git a/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go b/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go
index ef00332458a..840c549f46e 100644
--- a/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go
+++ b/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go
@@ -696,7 +696,7 @@ func TestMigrateFromV1_5ToV1_6(t *testing.T) {
 	expectedSeqNumExec := make(map[testhelpers.SourceDestPair][]uint64)
 	expectedSeqNums := make(map[testhelpers.SourceDestPair]uint64)
 	msgSentEvent, err := testhelpers.SendRequest(
-		t, e.Env, state,
+		e.Env, state,
 		testhelpers.WithSourceChain(src1),
 		testhelpers.WithDestChain(dest),
 		testhelpers.WithTestRouter(true),
diff --git a/integration-tests/smoke/ccip/ccip_ooo_execution_test.go b/integration-tests/smoke/ccip/ccip_ooo_execution_test.go
index 82bfdbc1363..244d3c1a8ca 100644
--- a/integration-tests/smoke/ccip/ccip_ooo_execution_test.go
+++ b/integration-tests/smoke/ccip/ccip_ooo_execution_test.go
@@ -196,7 +196,7 @@ func Test_OutOfOrderExecution(t *testing.T) {
 
 	// Ordered token transfer, but using different sender, should be executed
 	fifthReceiver := utils.RandomAddress()
-	fifthMessage, err := testhelpers.SendRequest(t, e, state,
+	fifthMessage, err := testhelpers.SendRequest(e, state,
 		testhelpers.WithSender(anotherSender),
 		testhelpers.WithSourceChain(sourceChain),
 		testhelpers.WithDestChain(destChain),


Reviews:
user: login: chris-de-leon-cll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T21:33:22Z
user: login: jlaveracll
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T04:30:22Z
user: login: archseer
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T04:55:52Z

Review comments:
diff_hunk: @@ -190,15 +190,7 @@ func (cfg TokenTransferFeeForRemoteChainConfig) Validate(e deployment.Environmen
 	if err := validateFeeQuoterConfig(chain, chainState); err != nil {
 		return fmt.Errorf("fee quoter validation failed: %w", err)
 	}
-	// check if desired state already exists
-	remoteBillingPDA, _, err := solState.FindFqPerChainPerTokenConfigPDA(cfg.RemoteChainSelector, tokenPubKey, chainState.FeeQuoter)
-	if err != nil {
-		return fmt.Errorf("failed to find remote billing token config pda for (remoteSelector: %d, mint: %s, feeQuoter: %s): %w", cfg.RemoteChainSelector, tokenPubKey.String(), chainState.FeeQuoter.String(), err)
-	}
-	var remoteBillingAccount solFeeQuoter.PerChainPerTokenConfig
-	if err := chain.GetAccountDataBorshInto(context.Background(), remoteBillingPDA, &remoteBillingAccount); err == nil {
-		return fmt.Errorf("billing token config already exists for (remoteSelector: %d, mint: %s, feeQuoter: %s)", cfg.RemoteChainSelector, tokenPubKey.String(), chainState.FeeQuoter.String())
-	}
path: deployment/ccip/changeset/solana/cs_billing.go
user: login: chris-de-leon-cll
body: NOTE: the Solana on-chain team confirmed that this logic is not needed - it prevents the Solana token pool configurations from being updated
created_at: 2025-04-06T21:33:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: cl-sonarqube-production[bot]
created_at: 2025-04-11T04:04:27Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarqube.main.prod.cldev.sh/dashboard?id=smartcontractkit_chainlink&pullRequest=17124) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_chainlink&pullRequest=17124&resolved=false) [2 New issues](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_chainlink&pullRequest=17124&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_chainlink&fixedInPullRequest=17124) [1 Fixed issue](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_chainlink&fixedInPullRequest=17124)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_chainlink&pullRequest=17124&issueStatuses=ACCEPTED) [0 Accepted issues](https://sonarqube.main.prod.cldev.sh/project/issues?id=smartcontractkit_chainlink&pullRequest=17124&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.main.prod.cldev.sh/security_hotspots?id=smartcontractkit_chainlink&pullRequest=17124) [0 Security Hotspots](https://sonarqube.main.prod.cldev.sh/security_hotspots?id=smartcontractkit_chainlink&pullRequest=17124)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_chainlink&pullRequest=17124&metric=new_coverage&view=list) [0.0% Coverage on New Code](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_chainlink&pullRequest=17124&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_chainlink&pullRequest=17124&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://sonarqube.main.prod.cldev.sh/component_measures?id=smartcontractkit_chainlink&pullRequest=17124&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://sonarqube.main.prod.cldev.sh/dashboard?id=smartcontractkit_chainlink&pullRequest=17124)


author_association: NONE