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
2 changes: 1 addition & 1 deletion ccv/chains/evm/deployment/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/smartcontractkit/chain-selectors v1.0.89
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260202173333-f74390e5a981
github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260202173333-f74390e5a981
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260202173333-f74390e5a981
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260202190546-951328743d01
github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260202173333-f74390e5a981
github.com/smartcontractkit/chainlink-common v0.9.6-0.20260114142648-bd9e1b483e96
github.com/smartcontractkit/chainlink-deployments-framework v0.74.2
Expand Down
4 changes: 2 additions & 2 deletions ccv/chains/evm/deployment/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -682,8 +682,8 @@ github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260202173333-f74390
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260202173333-f74390e5a981/go.mod h1:ZtZ+wtqU9JsJEmbiCsavVVEbhywpgMF7q/IpD9Eaq48=
github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260202173333-f74390e5a981 h1:oz8VagoREHfgoF+3Hh3l8Za0roHUReXgVBDk4nT+//c=
github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260202173333-f74390e5a981/go.mod h1:Gl35ExaFLinqVhp50+Yq1GnMuHb3fnDtZUFPCtcfV3M=
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260202173333-f74390e5a981 h1:le8LemZsV1ChPY0ewwDnJkhQkPGTgoul/uz4Pdr1e80=
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260202173333-f74390e5a981/go.mod h1:X9CFPZXyv9TLRL10SBFj1eP/73ZnqxtBw5T/ngovark=
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260202190546-951328743d01 h1:N+rhpdA796zr8cs82Zx7dMPhqGlfHGoVbp+xV20oDNg=
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260202190546-951328743d01/go.mod h1:xv/t/+R6UEmiUC9YRk23/MR6FW2ejnhZbW8EM+e8ljw=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d h1:xdFpzbApEMz4Rojg2Y2OjFlrh0wu7eB10V2tSZGW5y8=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d/go.mod h1:bgmqE7x9xwmIVr8PqLbC0M5iPm4AV2DBl596lO6S5Sw=
github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250908144012-8184001834b5 h1:QhcYGEhRLInr1/qh/3RJiVdvJ0nxBHKhPe65WLbSBnU=
Expand Down
46 changes: 46 additions & 0 deletions ccv/chains/evm/deployment/v1_7_0/operations/erc20/erc20.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package erc20

import (
"fmt"
"math/big"

"github.com/Masterminds/semver/v3"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"

"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract"
cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
erc20_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/latest/erc20"
)

var ContractType cldf_deployment.ContractType = "ERC20"

var Version = semver.MustParse("1.0.0")

type ApproveArgs struct {
Spender common.Address
Amount *big.Int
}

var Approve = contract.NewWrite(contract.WriteParams[ApproveArgs, *erc20_bindings.ERC20]{
Name: "erc20:approve",
Version: Version,
Description: "Approves a spender for ERC20 transfers",
ContractType: ContractType,
ContractABI: erc20_bindings.ERC20ABI,
NewContract: erc20_bindings.NewERC20,
IsAllowedCaller: contract.AllCallersAllowed[*erc20_bindings.ERC20, ApproveArgs],
Validate: func(args ApproveArgs) error {
if args.Spender == (common.Address{}) {
return fmt.Errorf("spender address must be set")
}
if args.Amount == nil || args.Amount.Sign() <= 0 {
return fmt.Errorf("amount must be greater than zero")
}
return nil
},
CallContract: func(token *erc20_bindings.ERC20, opts *bind.TransactOpts, args ApproveArgs) (*types.Transaction, error) {
return token.Approve(opts, args.Spender, args.Amount)
},
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package erc20_lock_box

import (
"fmt"
"math/big"

"github.com/Masterminds/semver/v3"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
Expand All @@ -20,6 +23,12 @@ type ConstructorArgs struct {

type AuthorizedCallerArgs = erc20_lock_box.AuthorizedCallersAuthorizedCallerArgs

type DepositArgs struct {
Token common.Address
RemoteChainSelector uint64
Amount *big.Int
}

var Deploy = contract.NewDeploy(contract.DeployParams[ConstructorArgs]{
Name: "erc20-lock-box:deploy",
Version: Version,
Expand Down Expand Up @@ -56,3 +65,36 @@ var GetAllAuthorizedCallers = contract.NewRead(contract.ReadParams[any, []common
return erc20LockBox.GetAllAuthorizedCallers(opts)
},
})

var Deposit = contract.NewWrite(contract.WriteParams[DepositArgs, *erc20_lock_box.ERC20LockBox]{
Name: "erc20-lock-box:deposit",
Version: Version,
Description: "Deposits tokens into the ERC20LockBox",
ContractType: ContractType,
ContractABI: erc20_lock_box.ERC20LockBoxABI,
NewContract: erc20_lock_box.NewERC20LockBox,
IsAllowedCaller: func(erc20LockBox *erc20_lock_box.ERC20LockBox, opts *bind.CallOpts, caller common.Address, args DepositArgs) (bool, error) {
callers, err := erc20LockBox.GetAllAuthorizedCallers(opts)
if err != nil {
return false, err
}
for _, authorized := range callers {
if authorized == caller {
return true, nil
}
}
return false, nil
},
Validate: func(args DepositArgs) error {
if args.Amount == nil || args.Amount.Sign() <= 0 {
return fmt.Errorf("amount must be greater than zero")
}
if args.Token == (common.Address{}) {
return fmt.Errorf("token address must be set")
}
return nil
},
CallContract: func(erc20LockBox *erc20_lock_box.ERC20LockBox, opts *bind.TransactOpts, args DepositArgs) (*types.Transaction, error) {
return erc20LockBox.Deposit(opts, args.Token, args.RemoteChainSelector, args.Amount)
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ var ApplyAuthorizedCallerUpdates = contract.NewWrite(contract.WriteParams[Author
},
})

var GetAllLockBoxConfigs = contract.NewRead(contract.ReadParams[any, []LockBoxConfig, *siloed_usdc_token_pool.SiloedUSDCTokenPool]{
Name: "siloed-usdc-token-pool:get-all-lock-box-configs",
Version: Version,
Description: "Gets all lock box configurations on the SiloedUSDCTokenPool",
ContractType: ContractType,
NewContract: siloed_usdc_token_pool.NewSiloedUSDCTokenPool,
CallContract: func(pool *siloed_usdc_token_pool.SiloedUSDCTokenPool, opts *bind.CallOpts, args any) ([]LockBoxConfig, error) {
return pool.GetAllLockBoxConfigs(opts)
},
})

var GetAllAuthorizedCallers = contract.NewRead(contract.ReadParams[any, []common.Address, *siloed_usdc_token_pool.SiloedUSDCTokenPool]{
Name: "siloed-usdc-token-pool:get-all-authorized-callers",
Version: Version,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package cctp

import (
"fmt"
"math/big"

"github.com/Masterminds/semver/v3"
"github.com/ethereum/go-ethereum/common"
chain_selectors "github.com/smartcontractkit/chain-selectors"
mcms_types "github.com/smartcontractkit/mcms/types"

"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/erc20"
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/erc20_lock_box"
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/siloed_usdc_token_pool"
contract_utils "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract"
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_2/operations/hybrid_lock_release_usdc_token_pool"
"github.com/smartcontractkit/chainlink-deployments-framework/chain"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations"
)

type MigrateHybridLockReleaseLiquidityInput struct {
ChainSelector uint64
HybridLockReleaseTokenPool string
SiloedUSDCTokenPool string
USDCToken string
LockReleaseChainSelectors []uint64
// LiquidityWithdrawPercent is the percent of locked liquidity to migrate (1-100).
LiquidityWithdrawPercent uint8
}

type MigrateHybridLockReleaseLiquidityOutput struct {
Addresses []datastore.AddressRef
BatchOps []mcms_types.BatchOperation
LockBoxes map[uint64]string
}

var MigrateHybridLockReleaseLiquidity = cldf_ops.NewSequence(
"migrate-hybrid-lock-release-liquidity",
semver.MustParse("1.7.0"),
"Migrates a share of liquidity from HybridLockReleaseUSDCTokenPool into per-chain Siloed lockboxes",
func(b cldf_ops.Bundle, chains chain.BlockChains, input MigrateHybridLockReleaseLiquidityInput) (output MigrateHybridLockReleaseLiquidityOutput, err error) {
chain, ok := chains.EVMChains()[input.ChainSelector]
if !ok {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("chain with selector %d not found", input.ChainSelector)
}
if len(input.LockReleaseChainSelectors) == 0 {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("lock release chain selectors must be provided")
}
if chain.Selector != chain_selectors.ETHEREUM_MAINNET.Selector && chain.Selector != chain_selectors.ETHEREUM_TESTNET_SEPOLIA.Selector {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("liquidity migration is only supported on home chains")
}
if input.LiquidityWithdrawPercent == 0 || input.LiquidityWithdrawPercent > 100 {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("liquidity withdraw percent must be between 1 and 100")
}

hybridPoolAddr, err := parseHexAddress("HybridLockReleaseUSDCTokenPool", input.HybridLockReleaseTokenPool)
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, err
}
siloedPoolAddr, err := parseHexAddress("SiloedUSDCTokenPool", input.SiloedUSDCTokenPool)
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, err
}
tokenAddr, err := parseHexAddress("USDC", input.USDCToken)
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, err
}

addresses := make([]datastore.AddressRef, 0)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not populated because the sequence doesn't deploy anything right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

writes := make([]contract_utils.WriteOutput, 0)
lockBoxes := make(map[uint64]string)

// Load lockbox mappings from the siloed pool to validate inputs.
lockBoxFromSiloedPool, err := fetchLockBoxesFromSiloedPool(b, chain, input.ChainSelector, siloedPoolAddr)
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, err
}
lockReleaseSelectors := input.LockReleaseChainSelectors
// Validate selectors are unique and configured for lock-release in the hybrid pool.
seenSelectors := make(map[uint64]struct{}, len(lockReleaseSelectors))
for _, sel := range lockReleaseSelectors {
if _, exists := seenSelectors[sel]; exists {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("duplicate lock release chain selector %d", sel)
}
seenSelectors[sel] = struct{}{}
// Validate that the hybrid pool is configured for lock-release on this chain.
shouldUseReport, err := cldf_ops.ExecuteOperation(b, hybrid_lock_release_usdc_token_pool.ShouldUseLockRelease, chain, contract_utils.FunctionInput[uint64]{
ChainSelector: input.ChainSelector,
Address: hybridPoolAddr,
Args: sel,
})
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("failed to check lock-release mechanism for chain %d: %w", sel, err)
}
if !shouldUseReport.Output {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("hybrid pool not configured for lock-release on chain %d", sel)
}
}
// Ensure each selector has a configured lockbox in the siloed pool.
for _, sel := range lockReleaseSelectors {
if lockBoxAddr, ok := lockBoxFromSiloedPool[sel]; ok && lockBoxAddr != (common.Address{}) {
lockBoxes[sel] = lockBoxAddr.Hex()
continue
}
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("lockbox not configured for chain %d", sel)
}

// Make sure the siloed pool is authorized on each lockbox before deposits.
for sel, lockBox := range lockBoxes {
lockBoxAddr := common.HexToAddress(lockBox)
callersReport, err := cldf_ops.ExecuteOperation(b, erc20_lock_box.GetAllAuthorizedCallers, chain, contract_utils.FunctionInput[any]{
ChainSelector: input.ChainSelector,
Address: lockBoxAddr,
})
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("failed to get authorized callers for lockbox %s (chain %d): %w", lockBox, sel, err)
}
if containsAddress(callersReport.Output, siloedPoolAddr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slices.Contains?

continue
}
authReport, err := cldf_ops.ExecuteOperation(b, erc20_lock_box.ApplyAuthorizedCallerUpdates, chain, contract_utils.FunctionInput[erc20_lock_box.AuthorizedCallerArgs]{
ChainSelector: input.ChainSelector,
Address: lockBoxAddr,
Args: erc20_lock_box.AuthorizedCallerArgs{
AddedCallers: []common.Address{siloedPoolAddr},
},
})
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("failed to authorize siloed pool on lockbox %s (chain %d): %w", lockBox, sel, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this already done in the initial setup of the SiloedUSDC?

}
writes = append(writes, authReport.Output)
}

// For each lock-release chain, move the requested share of liquidity into the lockbox.
for _, sel := range lockReleaseSelectors {
lockBoxAddr, ok := lockBoxes[sel]
if !ok {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("lockbox address missing for chain %d", sel)
}
lockedReport, err := cldf_ops.ExecuteOperation(b, hybrid_lock_release_usdc_token_pool.GetLockedTokensForChain, chain, contract_utils.FunctionInput[uint64]{
ChainSelector: input.ChainSelector,
Address: hybridPoolAddr,
Args: sel,
})
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("failed to get locked tokens for chain %d: %w", sel, err)
}
if lockedReport.Output == nil || lockedReport.Output.Sign() <= 0 {
continue
}
withdrawAmount := new(big.Int).Mul(lockedReport.Output, big.NewInt(int64(input.LiquidityWithdrawPercent)))
withdrawAmount.Div(withdrawAmount, big.NewInt(100))
Comment on lines +153 to +154
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have a test that confirms that this liquidity transfer works as expected?

if withdrawAmount.Sign() == 0 {
continue
}

withdrawReport, err := cldf_ops.ExecuteOperation(b, hybrid_lock_release_usdc_token_pool.WithdrawLiquidity, chain, contract_utils.FunctionInput[hybrid_lock_release_usdc_token_pool.WithdrawLiquidityArgs]{
ChainSelector: input.ChainSelector,
Address: hybridPoolAddr,
Args: hybrid_lock_release_usdc_token_pool.WithdrawLiquidityArgs{
RemoteChainSelector: sel,
Amount: withdrawAmount,
},
})
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("failed to withdraw liquidity for chain %d: %w", sel, err)
}
writes = append(writes, withdrawReport.Output)

approveReport, err := cldf_ops.ExecuteOperation(b, erc20.Approve, chain, contract_utils.FunctionInput[erc20.ApproveArgs]{
ChainSelector: input.ChainSelector,
Address: tokenAddr,
Args: erc20.ApproveArgs{
Spender: common.HexToAddress(lockBoxAddr),
Amount: withdrawAmount,
},
})
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("failed to approve lockbox for chain %d: %w", sel, err)
}
writes = append(writes, approveReport.Output)

depositReport, err := cldf_ops.ExecuteOperation(b, erc20_lock_box.Deposit, chain, contract_utils.FunctionInput[erc20_lock_box.DepositArgs]{
ChainSelector: input.ChainSelector,
Address: common.HexToAddress(lockBoxAddr),
Args: erc20_lock_box.DepositArgs{
Token: tokenAddr,
RemoteChainSelector: sel,
Amount: withdrawAmount,
},
})
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("failed to deposit into lockbox for chain %d: %w", sel, err)
}
writes = append(writes, depositReport.Output)
}

// Batch all writes into a single atomic MCMS operation.
batchOps := make([]mcms_types.BatchOperation, 0)
if len(writes) > 0 {
batchOp, err := contract_utils.NewBatchOperationFromWrites(writes)
if err != nil {
return MigrateHybridLockReleaseLiquidityOutput{}, fmt.Errorf("failed to create batch operation: %w", err)
}
batchOps = append(batchOps, batchOp)
}

return MigrateHybridLockReleaseLiquidityOutput{
Addresses: addresses,
BatchOps: batchOps,
LockBoxes: lockBoxes,
}, nil
},
)

func parseHexAddress(name, address string) (common.Address, error) {
if address == "" {
return common.Address{}, fmt.Errorf("%s address is required", name)
}
if !common.IsHexAddress(address) {
return common.Address{}, fmt.Errorf("%s address %q is not a valid hex address", name, address)
}
parsed := common.HexToAddress(address)
if parsed == (common.Address{}) {
return common.Address{}, fmt.Errorf("%s address is zero", name)
}
return parsed, nil
}

func fetchLockBoxesFromSiloedPool(b cldf_ops.Bundle, chain evm.Chain, chainSelector uint64, poolAddress common.Address) (map[uint64]common.Address, error) {
lockBoxReport, err := cldf_ops.ExecuteOperation(b, siloed_usdc_token_pool.GetAllLockBoxConfigs, chain, contract_utils.FunctionInput[any]{
ChainSelector: chainSelector,
Address: poolAddress,
})
if err != nil {
return nil, fmt.Errorf("failed to get lockbox configs: %w", err)
}

lockBoxes := make(map[uint64]common.Address, len(lockBoxReport.Output))
for _, cfg := range lockBoxReport.Output {
lockBoxes[cfg.RemoteChainSelector] = cfg.LockBox
}
return lockBoxes, nil
}
Loading
Loading