Skip to content
Draft
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
5 changes: 2 additions & 3 deletions deployment/adapters/configure_lanes_token_prices.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"

"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1"
"github.com/smartcontractkit/chainlink-canton/deployment/cciptokens"
feequoterop "github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/fee_quoter"
)

Expand All @@ -28,8 +29,6 @@ import (
// Canton FeeQuoter stores usdPerToken as DAML Decimal (see FeeQuoter.daml tests: 20.0 = $20/LINK).
const CantonRemoteTokenPricesFamilyExtraKey = "cantonRemoteTokenPrices"

const defaultLinkTokenInstrumentID = "link-token"

// defaultLinkUsdPerTokenDollars is the nominal LINK/USD spot used when FamilyExtras omit a price.
const defaultLinkUsdPerTokenDollars int64 = 10

Expand All @@ -51,7 +50,7 @@ func ResolveTokenPricesForRemoteDest(
}

prices := map[string]*big.Int{
fmt.Sprintf("%s:%s", ccipOwner, defaultLinkTokenInstrumentID): usdPerTokenToScaled(defaultLinkUsdPerTokenDollars),
fmt.Sprintf("%s:%s", ccipOwner, cciptokens.LinkTokenInstrumentID): usdPerTokenToScaled(defaultLinkUsdPerTokenDollars),
}
if nativeInstrument != nil && nativeInstrument.Admin != "" && nativeInstrument.Id != "" {
key := instrumentPriceKey(nativeInstrument.Admin, nativeInstrument.Id)
Expand Down
6 changes: 6 additions & 0 deletions deployment/adapters/deploy_chain_contracts_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/committeeverifier"
"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/core"
"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/executor"
"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1"
"github.com/smartcontractkit/chainlink-canton/deployment/cciptokens"
"github.com/smartcontractkit/chainlink-canton/deployment/sequences"
dsutils "github.com/smartcontractkit/chainlink-canton/deployment/utils/datastore"
)
Expand Down Expand Up @@ -130,6 +132,10 @@ func DeployCantonChainContracts(ctx context.Context, bundle cldf_ops.Bundle, cha
FeeQuoterConfig: sequences.FeeQuoterParams{
Template: core.FeeQuoter{
PriceUpdaters: []types.PARTY{types.PARTY(ownerParty)},
LinkTokenInstrumentId: splice_api_token_holding_v1.InstrumentId{
Admin: types.PARTY(ownerParty),
Id: types.TEXT(cciptokens.LinkTokenInstrumentID),
},
},
},
RMNRemote: sequences.RMNRemoteParams{
Expand Down
7 changes: 7 additions & 0 deletions deployment/cciptokens/instruments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Package cciptokens holds Canton instrument ID strings shared by devenv and deployment.
package cciptokens

const (
LinkTokenInstrumentID = "link-token"
AmuletInstrumentID = "Amulet"
)
179 changes: 179 additions & 0 deletions deployment/operations/ccip/factory/factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package factory

import (
"encoding/hex"
"testing"

"github.com/smartcontractkit/go-daml/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ccipcodec"
executorbindings "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/executor"
factorybindings "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/factory"
"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/ratelimiter"
)

// Byte-for-byte the DeployExecutor operationData from the prod_testnet proposal
// 1781126292119391259-...-from-factory_mcms_timelock_proposal_0.json, which was produced
// by the previous hand-rolled encoder:
// instanceId "executor-wwbhq" + owner + int64(10) + finality tag 0x00 + bool false.
const deployExecutorProposalHex = "0e6578656375746f722d77776268714f636369704f776e65723a3a3132323065333832663465353762303831356536626537333730303665333831653662376465343438653036626430333365636536646634393830313738373966353531000000000000000a0000"

// Wire format reference: encodeRequestedFinality/decodeRequestedFinalityAt in CCIP/Codec.daml
// (finality variant = uint8 tag 0x00/0x01, or 0x02 + int64 block depth).
func TestEncodeDeployExecutor(t *testing.T) {
t.Parallel()

blockDepth := types.INT64(12)
prefix := deployExecutorProposalHex[:len(deployExecutorProposalHex)-4]

tests := []struct {
name string
finality ccipcodec.FinalityConfig
enabled types.BOOL
wantHex string
}{
{
name: "wait for finality matches previously emitted proposal bytes",
finality: ccipcodec.FinalityConfig{WaitForFinality: &types.UNIT{}},
enabled: false,
wantHex: deployExecutorProposalHex,
},
{
name: "wait for safe",
finality: ccipcodec.FinalityConfig{WaitForSafe: &types.UNIT{}},
enabled: true,
wantHex: prefix + "0101",
},
{
name: "block depth carries int64 payload",
finality: ccipcodec.FinalityConfig{BlockDepth: &blockDepth},
enabled: false,
wantHex: prefix + "02000000000000000c00",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

encoded, err := encodeDeployExecutor(factorybindings.DeployExecutor{
Contract: executorbindings.Executor{
InstanceId: "executor-wwbhq",
Owner: "ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551",
MaxCCVsPerMsg: 10,
DynamicConfig: executorbindings.DynamicConfig{
AllowedFinalityConfig: tt.finality,
CcvAllowlistEnabled: tt.enabled,
},
},
})
require.NoError(t, err)
assert.Equal(t, "DeployExecutor", encoded.Choice)
assert.Equal(t, tt.wantHex, encoded.OperationData)
})
}
}

// The generated params struct must round-trip its own encoding so MCMS proposal
// decoding (mcms/sdk/canton) can verify candidates by re-encoding.
func TestDeployExecutorParamsHexRoundTrip(t *testing.T) {
t.Parallel()

original := factorybindings.DeployExecutorParams{
InstanceId: "executor-wwbhq",
Owner: "ccipOwner::1220e382f4e57b0815e6be737006e381e6b7de448e06bd033ece6df498017879f551",
MaxCCVsPerMsg: 10,
AllowedFinalityConfig: ccipcodec.FinalityConfig{WaitForFinality: &types.UNIT{}},
CcvAllowlistEnabled: false,
}

encoded, err := encodeDeployExecutor(factorybindings.DeployExecutor{
Contract: executorbindings.Executor{
InstanceId: original.InstanceId,
Owner: original.Owner,
MaxCCVsPerMsg: original.MaxCCVsPerMsg,
DynamicConfig: executorbindings.DynamicConfig{
AllowedFinalityConfig: original.AllowedFinalityConfig,
CcvAllowlistEnabled: original.CcvAllowlistEnabled,
},
},
})
require.NoError(t, err)

var decoded factorybindings.DeployExecutorParams
require.NoError(t, decoded.UnmarshalHex(encoded.OperationData))
assert.Equal(t, original, decoded)
}

// TestDeployRateLimiterParams_EnumEncoding verifies that RateLimitDirection and RateLimitMode
// are encoded as single ordinal bytes (0x00/0x01), matching the Daml MCMS codec wire format
// (decodeRateLimitDirectionAt / decodeRateLimitModeAt in contracts/ccip/factory/daml/CCIP/Factory.daml).
// Before the fix, go-daml encoded enums as length-prefixed constructor-name strings (e.g. 28 bytes
// for "RateLimitDirection_Outbound"), which the ledger's decodeUint8 step rejects.
func TestDeployRateLimiterParams_EnumEncoding(t *testing.T) {
t.Parallel()

params := factorybindings.DeployRateLimiterParams{
InstanceId: "rl-out-default",
PoolInstanceId: "pool-1",
PoolOwner: "owner::abc123",
RemoteChainSelector: "16015286601757825753",
Direction: ratelimiter.RateLimitDirectionRateLimitDirection_Outbound,
Mode: ratelimiter.RateLimitModeRateLimitMode_DefaultFinality,
IsEnabled: true,
Capacity: "1000",
Rate: "10",
}

encoded, err := params.MarshalHex()
require.NoError(t, err)

raw := []byte(encoded)
// Walk the encoded bytes to find Direction and Mode positions:
// TEXT instanceId: 1 + 14 = 15
// TEXT poolInstanceId: 1 + 6 = 7 → offset 22
// PARTY poolOwner: 1 + 13 = 14 → offset 36
// NUMERIC selector: 1 + 20 = 21 → offset 57
// Direction: offset 57 must be 0x00 (Outbound)
// Mode: offset 58 must be 0x00 (DefaultFinality)
// Byte layout: 1+14 instanceId | 1+6 poolInstanceId | 1+13 poolOwner | 1+20 chainSelector | dir | mode | ...
// Direction at offset 57 (15+7+14+21), Mode at offset 58.
require.Greater(t, len(raw), 59, "encoded too short")
assert.Equal(t, byte(0x00), raw[57], "Direction Outbound must be byte 0x00")
assert.Equal(t, byte(0x00), raw[58], "Mode DefaultFinality must be byte 0x00")

// Inbound / CustomFinality → 0x01
params.Direction = ratelimiter.RateLimitDirectionRateLimitDirection_Inbound
params.Mode = ratelimiter.RateLimitModeRateLimitMode_CustomFinality
encoded, err = params.MarshalHex()
require.NoError(t, err)
raw = []byte(encoded)
assert.Equal(t, byte(0x01), raw[57], "Direction Inbound must be byte 0x01")
assert.Equal(t, byte(0x01), raw[58], "Mode CustomFinality must be byte 0x01")
}

func TestDeployRateLimiterParams_HexRoundTrip(t *testing.T) {
t.Parallel()

original := factorybindings.DeployRateLimiterParams{
InstanceId: "rl-inbound",
PoolInstanceId: "pool-2",
PoolOwner: "owner::def456",
RemoteChainSelector: "16015286601757825753",
Direction: ratelimiter.RateLimitDirectionRateLimitDirection_Inbound,
Mode: ratelimiter.RateLimitModeRateLimitMode_CustomFinality,
IsEnabled: false,
Capacity: "500",
Rate: "5",
}

// MarshalHex returns raw bytes (not a hex string); hex-encode for UnmarshalHex (same as executor test).
encoded, err := original.MarshalHex()
require.NoError(t, err)

var decoded factorybindings.DeployRateLimiterParams
require.NoError(t, decoded.UnmarshalHex(hex.EncodeToString([]byte(encoded))))
assert.Equal(t, original, decoded)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package token_admin_registry

import (
"encoding/hex"
"testing"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/go-daml/pkg/types"

"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/ccip/core"
splice "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_holding_v1"
)

func TestTokenAdminRegistryMCMSEncodingUsesParamsTypes(t *testing.T) {
t.Parallel()

inst := splice.InstrumentId{
Admin: types.PARTY("party::alice"),
Id: types.TEXT("TOKEN"),
}
admin := types.PARTY("party::bob")
pool := &core.PoolRegistration2{
PoolOwner: types.PARTY("party::pool"),
PoolInstanceId: types.TEXT("pool-1"),
}

proposeParams := core.ProposeAdminParams{InstrumentId: inst, NewAdmin: admin}
proposeEncoded, err := tarEncoder.ProposeAdministrator(proposeParams)
require.NoError(t, err)
proposeWire, err := proposeParams.MarshalHex()
require.NoError(t, err)
require.Equal(t, hex.EncodeToString([]byte(proposeWire)), proposeEncoded.OperationData)
require.Equal(t, "ProposeAdministrator", proposeEncoded.Choice)

acceptParams := core.AcceptAdminParams{InstrumentId: inst}
acceptEncoded, err := tarEncoder.AcceptAdminRole(acceptParams)
require.NoError(t, err)
acceptWire, err := acceptParams.MarshalHex()
require.NoError(t, err)
require.Equal(t, hex.EncodeToString([]byte(acceptWire)), acceptEncoded.OperationData)
require.Equal(t, "AcceptAdminRole", acceptEncoded.Choice)

setPoolParams := core.SetPoolParams{InstrumentId: inst, TokenPool: pool}
setPoolEncoded, err := tarEncoder.SetPoolParams(setPoolParams)
require.NoError(t, err)
setPoolWire, err := setPoolParams.MarshalHex()
require.NoError(t, err)
require.Equal(t, hex.EncodeToString([]byte(setPoolWire)), setPoolEncoded.OperationData)
require.Equal(t, "SetPool", setPoolEncoded.Choice)
}
21 changes: 19 additions & 2 deletions deployment/operations/linkregistry/linkregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package linkregistry

import (
"github.com/Masterminds/semver/v3"
"github.com/smartcontractkit/go-daml/pkg/model"

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

Expand Down Expand Up @@ -34,7 +35,7 @@ var BurnMintFactory_BurnMint = contract.NewExercise(contract.ExerciseParams[spli
Description: "Burns/Mints Holdings on Canton",
ContractType: ContractType,
Template: link.LinkRegistry{},
Method: link.LinkRegistry{}.BurnMintFactoryBurnMint,
Method: exerciseLinkRegistryBurnMint,
})

var TransferFactory_Transfer = contract.NewExercise(contract.ExerciseParams[splice_api_token_transfer_instruction_v1.TransferFactoryTransfer]{
Expand All @@ -43,7 +44,7 @@ var TransferFactory_Transfer = contract.NewExercise(contract.ExerciseParams[spli
Description: "Transfers Holdings on Canton",
ContractType: ContractType,
Template: link.LinkRegistry{},
Method: link.LinkRegistry{}.TransferFactoryTransfer,
Method: exerciseLinkRegistryTransfer,
})

var CreateTransferPreapproval = contract.NewExercise(contract.ExerciseParams[link.CreateTransferPreapproval]{
Expand All @@ -54,3 +55,19 @@ var CreateTransferPreapproval = contract.NewExercise(contract.ExerciseParams[lin
Template: link.LinkRegistry{},
Method: link.LinkRegistry{}.CreateTransferPreapproval,
})

// Splice token interface choices must be exercised via the interface package on the ledger
// (see integration-tests/ccip/ccip_send_with_token_bnm_test.go), not Link.Token's local view.
func exerciseLinkRegistryBurnMint(contractID string, args splice_api_token_burn_mint_v1.BurnMintFactoryBurnMint) *model.ExerciseCommand {
cmd := link.LinkRegistry{}.BurnMintFactoryBurnMint(contractID, args)
cmd.TemplateID = splice_api_token_burn_mint_v1.IBurnMintFactoryInterfaceID()

return cmd
}

func exerciseLinkRegistryTransfer(contractID string, args splice_api_token_transfer_instruction_v1.TransferFactoryTransfer) *model.ExerciseCommand {
cmd := link.LinkRegistry{}.TransferFactoryTransfer(contractID, args)
cmd.TemplateID = splice_api_token_transfer_instruction_v1.ITransferFactoryInterfaceID()

return cmd
}
22 changes: 22 additions & 0 deletions deployment/operations/linkregistry/linkregistry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package linkregistry

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_burn_mint_v1"
"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/splice/splice_api_token_transfer_instruction_v1"
)

func TestExerciseLinkRegistryUsesSpliceInterfaceTemplateIDs(t *testing.T) {
t.Parallel()

burnMintCmd := exerciseLinkRegistryBurnMint("cid", splice_api_token_burn_mint_v1.BurnMintFactoryBurnMint{})
require.Equal(t, splice_api_token_burn_mint_v1.IBurnMintFactoryInterfaceID(), burnMintCmd.TemplateID)
require.Equal(t, "BurnMintFactory_BurnMint", burnMintCmd.Choice)

transferCmd := exerciseLinkRegistryTransfer("cid", splice_api_token_transfer_instruction_v1.TransferFactoryTransfer{})
require.Equal(t, splice_api_token_transfer_instruction_v1.ITransferFactoryInterfaceID(), transferCmd.TemplateID)
require.Equal(t, "TransferFactory_Transfer", transferCmd.Choice)
}
23 changes: 23 additions & 0 deletions deployment/operations/services/eds/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
"github.com/smartcontractkit/chainlink-deployments-framework/operations"

"github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/link"
"github.com/smartcontractkit/chainlink-canton/commonconfig"
"github.com/smartcontractkit/chainlink-canton/contracts"
"github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/burn_mint_token_pool"
Expand All @@ -22,6 +23,7 @@ import (
"github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/per_party_router_factory"
"github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/rmn_remote"
"github.com/smartcontractkit/chainlink-canton/deployment/operations/ccip/token_admin_registry"
"github.com/smartcontractkit/chainlink-canton/deployment/operations/linkregistry"
edsConfig "github.com/smartcontractkit/chainlink-canton/eds/config"
)

Expand Down Expand Up @@ -183,6 +185,27 @@ var BuildConfig = operations.NewOperation(
pool.TransferPreapproval = input.LockReleaseTransferPreapproval
}
}
// BnM send needs burn-mint-factory in pool choice context; LinkRegistry implements BurnMintFactory.
if tokenPoolType == edsConfig.TokenPoolTypeBurnMint {
linkRegistryRef, err := env.DataStore.Addresses().Get(datastore.NewAddressRefKey(
input.ChainSelector,
datastore.ContractType(linkregistry.ContractType),
linkregistry.Version,
"",
))
if err != nil {
return GenerateEDSConfigOutput{}, fmt.Errorf("failed to get LinkRegistry for burn-mint pool EDS config: %w", err)
}
templateID := link.LinkRegistry{}.GetTemplateID()
partyID := participant.PartyID
linkRegistryAddress := contracts.HexToInstanceAddress(linkRegistryRef.Address)
pool.BurnMintFactory = &edsConfig.BurnMintFactory{
Type: edsConfig.FactoryTypeAddress,
TemplateId: &templateID,
Party: &partyID,
InstanceAddress: &linkRegistryAddress,
}
}
tokenPools[instanceAddress.Hex()] = pool
}

Expand Down
Loading
Loading