Skip to content
Open
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
4 changes: 2 additions & 2 deletions chains/evm/deployment/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ require (
github.com/aws/smithy-go v1.22.5
github.com/ethereum/go-ethereum v1.16.2
github.com/smartcontractkit/chain-selectors v1.0.72
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20251029160438-93e0a575186e
github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20251029160438-93e0a575186e
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20251030175938-269363720b3b
github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20251030175938-269363720b3b
github.com/smartcontractkit/chainlink-common v0.9.6-0.20250929154511-1f5fbda7ae76
github.com/smartcontractkit/chainlink-deployments-framework v0.56.0
github.com/smartcontractkit/chainlink-evm v0.3.3
Expand Down
4 changes: 2 additions & 2 deletions chains/evm/deployment/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -666,8 +666,8 @@ github.com/smartcontractkit/chain-selectors v1.0.72 h1:AExF2H3mABdLCN0QZd+IjU8Ck
github.com/smartcontractkit/chain-selectors v1.0.72/go.mod h1:xsKM0aN3YGcQKTPRPDDtPx2l4mlTN1Djmg0VVXV40b8=
github.com/smartcontractkit/chainlink-aptos v0.0.0-20250915164817-46a35eda083d h1:bcfnHPXAhrhUw95X60Y/lDhQAb4SxSyTrqyVCHqfXPI=
github.com/smartcontractkit/chainlink-aptos v0.0.0-20250915164817-46a35eda083d/go.mod h1:tEjqontct1/5cKHm4q75nopZa1rwzaQZwd9U9wn0uZE=
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20251029160438-93e0a575186e h1:A5lV4Qdm7Pf5jAADGhBvAIErkWLgFZuvh8+DgYHfTzQ=
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20251029160438-93e0a575186e/go.mod h1:pETrvAF8uvkZgtDgI/oRllZZaC4IpPO26tMxh1u9LC4=
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20251030175938-269363720b3b h1:9l3kWXEliJbJ1sTF/JSE6jFgC/pk5lX/9oBKz87s2Z8=
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20251030175938-269363720b3b/go.mod h1:pETrvAF8uvkZgtDgI/oRllZZaC4IpPO26tMxh1u9LC4=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250908144012-8184001834b5 h1:GmJQqNrWn5pNc8YTei6l2TOSYjK2fRd4+edFZIifCrU=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250908144012-8184001834b5/go.mod h1:Ve1xD71bl193YIZQEoJMmBqLGQJdNs29bwbuObwvbhQ=
github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250908144012-8184001834b5 h1:QhcYGEhRLInr1/qh/3RJiVdvJ0nxBHKhPe65WLbSBnU=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ type ApplyCustomBlockConfirmationConfigArgs struct {
RateLimitConfigArgs []CustomBlockConfirmationRateLimitConfigArg
}

type TokenTransferFeeConfigArg struct {
DestChainSelector uint64
TokenTransferFeeConfig tokens.TokenTransferFeeConfig
}

type ApplyTokenTransferFeeConfigUpdatesArgs struct {
TokenTransferFeeConfigArgs []TokenTransferFeeConfigArg
DestToUseDefaultFeeConfigs []uint64
}

type GetTokenTransferFeeConfigArgs struct {
LocalToken common.Address
DestChainSelector uint64
}

var Deploy = contract.NewDeploy(contract.DeployParams[ConstructorArgs]{
Name: "token-pool:deploy",
Version: semver.MustParse("1.7.0"),
Expand Down Expand Up @@ -288,6 +303,34 @@ var SetCustomBlockConfirmationRateLimitConfig = contract.NewWrite(contract.Write
},
})

var ApplyTokenTransferFeeConfigUpdates = contract.NewWrite(contract.WriteParams[ApplyTokenTransferFeeConfigUpdatesArgs, *token_pool.TokenPool]{
Name: "token-pool:apply-token-transfer-fee-config-updates",
Version: semver.MustParse("1.7.0"),
Description: "Applies token transfer fee overrides or resets defaults for destination chains on a TokenPool",
ContractType: ContractType,
ContractABI: token_pool.TokenPoolABI,
NewContract: token_pool.NewTokenPool,
IsAllowedCaller: contract.OnlyOwner[*token_pool.TokenPool, ApplyTokenTransferFeeConfigUpdatesArgs],
Validate: func(ApplyTokenTransferFeeConfigUpdatesArgs) error { return nil },
CallContract: func(tokenPool *token_pool.TokenPool, opts *bind.TransactOpts, args ApplyTokenTransferFeeConfigUpdatesArgs) (*types.Transaction, error) {
configs := make([]token_pool.TokenPoolTokenTransferFeeConfigArgs, 0, len(args.TokenTransferFeeConfigArgs))
for _, cfg := range args.TokenTransferFeeConfigArgs {
configs = append(configs, token_pool.TokenPoolTokenTransferFeeConfigArgs{
DestChainSelector: cfg.DestChainSelector,
TokenTransferFeeConfig: token_pool.IPoolV2TokenTransferFeeConfig{
DestGasOverhead: cfg.TokenTransferFeeConfig.DestGasOverhead,
DestBytesOverhead: cfg.TokenTransferFeeConfig.DestBytesOverhead,
DefaultBlockConfirmationFeeUSDCents: cfg.TokenTransferFeeConfig.DefaultBlockConfirmationFeeUSDCents,
CustomBlockConfirmationFeeUSDCents: cfg.TokenTransferFeeConfig.CustomBlockConfirmationFeeUSDCents,
DefaultBlockConfirmationTransferFeeBps: cfg.TokenTransferFeeConfig.DefaultBlockConfirmationTransferFeeBps,
CustomBlockConfirmationTransferFeeBps: cfg.TokenTransferFeeConfig.CustomBlockConfirmationTransferFeeBps,
},
})
}
return tokenPool.ApplyTokenTransferFeeConfigUpdates(opts, configs, args.DestToUseDefaultFeeConfigs)
},
})

var ApplyAllowListUpdates = contract.NewWrite(contract.WriteParams[ApplyAllowListUpdatesArgs, *token_pool.TokenPool]{
Name: "token-pool:apply-allowlist-updates",
Version: semver.MustParse("1.7.0"),
Expand Down Expand Up @@ -394,6 +437,17 @@ var GetCurrentCustomBlockConfirmationRateLimiterState = contract.NewRead(contrac
},
})

var GetTokenTransferFeeConfig = contract.NewRead(contract.ReadParams[GetTokenTransferFeeConfigArgs, token_pool.IPoolV2TokenTransferFeeConfig, *token_pool.TokenPool]{
Name: "token-pool:get-token-transfer-fee-config",
Version: semver.MustParse("1.7.0"),
Description: "Gets the token transfer fee configuration for a destination chain on a TokenPool",
ContractType: ContractType,
NewContract: token_pool.NewTokenPool,
CallContract: func(tokenPool *token_pool.TokenPool, opts *bind.CallOpts, args GetTokenTransferFeeConfigArgs) (token_pool.IPoolV2TokenTransferFeeConfig, error) {
return tokenPool.GetTokenTransferFeeConfig(opts, args.LocalToken, args.DestChainSelector, token_pool.ClientEVM2AnyMessage{}, 0, nil)
},
})

var GetSupportedChains = contract.NewRead(contract.ReadParams[any, []uint64, *token_pool.TokenPool]{
Name: "token-pool:supported-chains",
Version: semver.MustParse("1.7.0"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package tokens

import (
"fmt"
"strings"

"github.com/Masterminds/semver/v3"
"github.com/ethereum/go-ethereum/common"
evm_contract "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract"
v1_5_0 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/sequences"
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_7_0/operations/token_pool"
tp_bindings "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/latest/token_pool"
"github.com/smartcontractkit/chainlink-ccip/deployment/tokens"
"github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences"
"github.com/smartcontractkit/chainlink-deployments-framework/chain"
Expand All @@ -26,15 +28,28 @@ var ConfigureTokenForTransfers = cldf_ops.NewSequence(
if !ok {
return sequences.OnChainOutput{}, fmt.Errorf("chain with selector %d not found", input.ChainSelector)
}
tokenPoolAddress := common.HexToAddress(input.TokenPoolAddress)

// Fetch the local token address once to reuse for token transfer fee lookups and registration.
tokenAddressReport, err := cldf_ops.ExecuteOperation(b, token_pool.GetToken, chain, evm_contract.FunctionInput[any]{
ChainSelector: input.ChainSelector,
Address: tokenPoolAddress,
})
if err != nil {
return sequences.OnChainOutput{}, fmt.Errorf("failed to get token address from token pool with address %s on %s: %w", input.TokenPoolAddress, chain, err)
}
localTokenAddress := tokenAddressReport.Output

// Configure remote chains on the token pool as specified
// This means adding any remote chains not already added, removing any remote chains that are no longer desired,
// or modifying rate limiters on any existing remote chains.
customBlockConfirmationArgs := make([]token_pool.CustomBlockConfirmationRateLimitConfigArg, 0, len(input.RemoteChains))
tokenTransferFeeConfigArgs := make([]token_pool.TokenTransferFeeConfigArg, 0, len(input.RemoteChains))
destToUseDefaultFeeConfigs := make([]uint64, 0)
for destChainSelector, remoteChainConfig := range input.RemoteChains {
configureTokenPoolForRemoteChainReport, err := cldf_ops.ExecuteSequence(b, ConfigureTokenPoolForRemoteChain, chain, ConfigureTokenPoolForRemoteChainInput{
ChainSelector: input.ChainSelector,
TokenPoolAddress: common.HexToAddress(input.TokenPoolAddress),
TokenPoolAddress: tokenPoolAddress,
RemoteChainSelector: destChainSelector,
RemoteChainConfig: remoteChainConfig,
})
Expand All @@ -43,6 +58,43 @@ var ConfigureTokenForTransfers = cldf_ops.NewSequence(
}
ops = append(ops, configureTokenPoolForRemoteChainReport.Output.BatchOps...)

supportsTokenPoolFeeConfig := true
var currentFeeConfig tp_bindings.IPoolV2TokenTransferFeeConfig
currentFeeConfigReport, err := cldf_ops.ExecuteOperation(b, token_pool.GetTokenTransferFeeConfig, chain, evm_contract.FunctionInput[token_pool.GetTokenTransferFeeConfigArgs]{
ChainSelector: input.ChainSelector,
Address: tokenPoolAddress,
Args: token_pool.GetTokenTransferFeeConfigArgs{
LocalToken: localTokenAddress,
DestChainSelector: destChainSelector,
},
})
if err != nil {
if strings.Contains(err.Error(), "execution reverted") {
supportsTokenPoolFeeConfig = false
} else {
return sequences.OnChainOutput{}, fmt.Errorf("failed to get token transfer fee config for token pool %s on %s for remote chain %d: %w", input.TokenPoolAddress, chain, destChainSelector, err)
}
} else {
currentFeeConfig = currentFeeConfigReport.Output
}

if supportsTokenPoolFeeConfig {
if remoteChainConfig.TokenTransferFeeConfig == nil {
if !tokenTransferFeeConfigIsZero(currentFeeConfig) {
destToUseDefaultFeeConfigs = append(destToUseDefaultFeeConfigs, destChainSelector)
tokenTransferFeeConfigArgs = append(tokenTransferFeeConfigArgs, token_pool.TokenTransferFeeConfigArg{
DestChainSelector: destChainSelector,
TokenTransferFeeConfig: tokens.TokenTransferFeeConfig{},
})
}
} else if !tokenTransferFeeConfigsEqual(currentFeeConfig, *remoteChainConfig.TokenTransferFeeConfig) {
tokenTransferFeeConfigArgs = append(tokenTransferFeeConfigArgs, token_pool.TokenTransferFeeConfigArg{
DestChainSelector: destChainSelector,
TokenTransferFeeConfig: *remoteChainConfig.TokenTransferFeeConfig,
})
}
}

if remoteChainConfig.CustomBlockConfirmationConfig != nil {
cfg := remoteChainConfig.CustomBlockConfirmationConfig
customBlockConfirmationArgs = append(customBlockConfirmationArgs, token_pool.CustomBlockConfirmationRateLimitConfigArg{
Expand All @@ -53,11 +105,34 @@ var ConfigureTokenForTransfers = cldf_ops.NewSequence(
}
}

tokenTransferFeeWrites := make([]evm_contract.WriteOutput, 0, 1)
if len(tokenTransferFeeConfigArgs) > 0 || len(destToUseDefaultFeeConfigs) > 0 {
applyTokenTransferFeesReport, err := cldf_ops.ExecuteOperation(b, token_pool.ApplyTokenTransferFeeConfigUpdates, chain, evm_contract.FunctionInput[token_pool.ApplyTokenTransferFeeConfigUpdatesArgs]{
ChainSelector: input.ChainSelector,
Address: tokenPoolAddress,
Args: token_pool.ApplyTokenTransferFeeConfigUpdatesArgs{
TokenTransferFeeConfigArgs: tokenTransferFeeConfigArgs,
DestToUseDefaultFeeConfigs: destToUseDefaultFeeConfigs,
},
})
if err != nil {
return sequences.OnChainOutput{}, fmt.Errorf("failed to apply token transfer fee config updates on token pool %s: %w", input.TokenPoolAddress, err)
}
tokenTransferFeeWrites = append(tokenTransferFeeWrites, applyTokenTransferFeesReport.Output)
}
if len(tokenTransferFeeWrites) > 0 {
tokenTransferFeeBatch, err := evm_contract.NewBatchOperationFromWrites(tokenTransferFeeWrites)
if err != nil {
return sequences.OnChainOutput{}, fmt.Errorf("failed to create batch operation for token transfer fee configuration: %w", err)
}
ops = append(ops, tokenTransferFeeBatch)
}

blockConfirmationWrites := make([]evm_contract.WriteOutput, 0, 1)
if input.CustomBlockConfirmationConfig != nil {
applyConfigReport, err := cldf_ops.ExecuteOperation(b, token_pool.ApplyCustomBlockConfirmationConfigUpdates, chain, evm_contract.FunctionInput[token_pool.ApplyCustomBlockConfirmationConfigArgs]{
ChainSelector: input.ChainSelector,
Address: common.HexToAddress(input.TokenPoolAddress),
Address: tokenPoolAddress,
Args: token_pool.ApplyCustomBlockConfirmationConfigArgs{
MinBlockConfirmation: input.CustomBlockConfirmationConfig.MinBlockConfirmation,
RateLimitConfigArgs: customBlockConfirmationArgs,
Expand All @@ -70,7 +145,7 @@ var ConfigureTokenForTransfers = cldf_ops.NewSequence(
} else if len(customBlockConfirmationArgs) > 0 {
setConfigReport, err := cldf_ops.ExecuteOperation(b, token_pool.SetCustomBlockConfirmationRateLimitConfig, chain, evm_contract.FunctionInput[[]token_pool.CustomBlockConfirmationRateLimitConfigArg]{
ChainSelector: input.ChainSelector,
Address: common.HexToAddress(input.TokenPoolAddress),
Address: tokenPoolAddress,
Args: customBlockConfirmationArgs,
})
if err != nil {
Expand All @@ -86,19 +161,11 @@ var ConfigureTokenForTransfers = cldf_ops.NewSequence(
ops = append(ops, blockConfirmationBatch)
}

tokenAddress, err := cldf_ops.ExecuteOperation(b, token_pool.GetToken, chain, evm_contract.FunctionInput[any]{
ChainSelector: input.ChainSelector,
Address: common.HexToAddress(input.TokenPoolAddress),
})
if err != nil {
return sequences.OnChainOutput{}, fmt.Errorf("failed to get token address from token pool with address %s on %s: %w", input.TokenPoolAddress, chain, err)
}

// Register the token with the token admin registry
registerTokenReport, err := cldf_ops.ExecuteSequence(b, v1_5_0.RegisterToken, chain, v1_5_0.RegisterTokenInput{
ChainSelector: input.ChainSelector,
TokenAddress: tokenAddress.Output,
TokenPoolAddress: common.HexToAddress(input.TokenPoolAddress),
TokenAddress: localTokenAddress,
TokenPoolAddress: tokenPoolAddress,
ExternalAdmin: common.HexToAddress(input.ExternalAdmin),
TokenAdminRegistryAddress: common.HexToAddress(input.RegistryAddress),
})
Expand All @@ -112,3 +179,21 @@ var ConfigureTokenForTransfers = cldf_ops.NewSequence(
}, nil
},
)

func tokenTransferFeeConfigIsZero(config tp_bindings.IPoolV2TokenTransferFeeConfig) bool {
return config.DestGasOverhead == 0 &&
config.DestBytesOverhead == 0 &&
config.DefaultBlockConfirmationFeeUSDCents == 0 &&
config.CustomBlockConfirmationFeeUSDCents == 0 &&
config.DefaultBlockConfirmationTransferFeeBps == 0 &&
config.CustomBlockConfirmationTransferFeeBps == 0
}

func tokenTransferFeeConfigsEqual(current tp_bindings.IPoolV2TokenTransferFeeConfig, desired tokens.TokenTransferFeeConfig) bool {
return current.DestGasOverhead == desired.DestGasOverhead &&
current.DestBytesOverhead == desired.DestBytesOverhead &&
current.DefaultBlockConfirmationFeeUSDCents == desired.DefaultBlockConfirmationFeeUSDCents &&
current.CustomBlockConfirmationFeeUSDCents == desired.CustomBlockConfirmationFeeUSDCents &&
current.DefaultBlockConfirmationTransferFeeBps == desired.DefaultBlockConfirmationTransferFeeBps &&
current.CustomBlockConfirmationTransferFeeBps == desired.CustomBlockConfirmationTransferFeeBps
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ func TestConfigureTokenForTransfers(t *testing.T) {
Rate: big.NewInt(70),
},
},
TokenTransferFeeConfig: &tokens_core.TokenTransferFeeConfig{
DestGasOverhead: 321,
DestBytesOverhead: 654,
DefaultBlockConfirmationFeeUSDCents: 777,
CustomBlockConfirmationFeeUSDCents: 888,
DefaultBlockConfirmationTransferFeeBps: 123,
CustomBlockConfirmationTransferFeeBps: 234,
},
},
remoteChainSel2: {
RemoteToken: common.LeftPadBytes(common.FromHex("0x321"), 32),
Expand Down Expand Up @@ -164,9 +172,23 @@ func TestConfigureTokenForTransfers(t *testing.T) {

// Verify configuration for first remote chain
checkRemoteChainConfiguration(t, tp, remoteChainSel1, input.RemoteChains[remoteChainSel1])
assertTokenTransferFeeConfig(
t,
tp,
common.HexToAddress(tokenAddress),
remoteChainSel1,
input.RemoteChains[remoteChainSel1].TokenTransferFeeConfig,
)

// Verify configuration for second remote chain
checkRemoteChainConfiguration(t, tp, remoteChainSel2, input.RemoteChains[remoteChainSel2])
assertTokenTransferFeeConfig(
t,
tp,
common.HexToAddress(tokenAddress),
remoteChainSel2,
input.RemoteChains[remoteChainSel2].TokenTransferFeeConfig,
)

minBlockConfirmation, err := tp.GetMinBlockConfirmation(nil)
require.NoError(t, err, "Failed to get configured min block confirmation")
Expand Down Expand Up @@ -301,6 +323,7 @@ func TestConfigureTokenForTransfers(t *testing.T) {
require.Equal(t, uint16(0), minBlockConfirmation, "Min block confirmation should remain default")
assertCustomBlockConfirmationBucket(t, tp, remoteChainSel, input.RemoteChains[remoteChainSel].CustomBlockConfirmationConfig)
})

}

// checkRemoteChainConfiguration verifies the configuration for a remote chain on the token pool
Expand Down Expand Up @@ -345,6 +368,32 @@ func checkRemoteChainConfiguration(t *testing.T, tp *tp_bindings.TokenPool, remo
}
}

func assertTokenTransferFeeConfig(
t *testing.T,
tp *tp_bindings.TokenPool,
localToken common.Address,
destChainSelector uint64,
expected *tokens_core.TokenTransferFeeConfig,
) {
config, err := tp.GetTokenTransferFeeConfig(nil, localToken, destChainSelector, tp_bindings.ClientEVM2AnyMessage{}, 0, nil)
require.NoError(t, err, "Failed to get token transfer fee config")
if expected == nil {
require.Zero(t, config.DestGasOverhead, "Dest gas overhead should be zero when no override is configured")
require.Zero(t, config.DestBytesOverhead, "Dest bytes overhead should be zero when no override is configured")
require.Zero(t, config.DefaultBlockConfirmationFeeUSDCents, "Default fee cents should be zero when no override is configured")
require.Zero(t, config.CustomBlockConfirmationFeeUSDCents, "Custom fee cents should be zero when no override is configured")
require.Zero(t, config.DefaultBlockConfirmationTransferFeeBps, "Default transfer fee bps should be zero when no override is configured")
require.Zero(t, config.CustomBlockConfirmationTransferFeeBps, "Custom transfer fee bps should be zero when no override is configured")
return
}
require.Equal(t, expected.DestGasOverhead, config.DestGasOverhead, "Dest gas overhead mismatch")
require.Equal(t, expected.DestBytesOverhead, config.DestBytesOverhead, "Dest bytes overhead mismatch")
require.Equal(t, expected.DefaultBlockConfirmationFeeUSDCents, config.DefaultBlockConfirmationFeeUSDCents, "Default fee cents mismatch")
require.Equal(t, expected.CustomBlockConfirmationFeeUSDCents, config.CustomBlockConfirmationFeeUSDCents, "Custom fee cents mismatch")
require.Equal(t, expected.DefaultBlockConfirmationTransferFeeBps, config.DefaultBlockConfirmationTransferFeeBps, "Default transfer fee bps mismatch")
require.Equal(t, expected.CustomBlockConfirmationTransferFeeBps, config.CustomBlockConfirmationTransferFeeBps, "Custom transfer fee bps mismatch")
}

func assertCustomBlockConfirmationBucket(
t *testing.T,
tp *tp_bindings.TokenPool,
Expand Down
1 change: 1 addition & 0 deletions deployment/tokens/configure_tokens_for_transfers.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func convertRemoteChainConfig(
InboundRateLimiterConfig: inCfg.InboundRateLimiterConfig,
OutboundRateLimiterConfig: inCfg.OutboundRateLimiterConfig,
CustomBlockConfirmationConfig: inCfg.CustomBlockConfirmationConfig,
TokenTransferFeeConfig: inCfg.TokenTransferFeeConfig,
}
if inCfg.RemotePool != nil {
fullRemotePoolRef, err := datastore_utils.FindAndFormatRef(e.DataStore, *inCfg.RemotePool, remoteChainSelector, datastore_utils.FullRef)
Expand Down
Loading
Loading