Skip to content

Commit 4bbd175

Browse files
authored
USDC Token Pool Liquidity migration changeset (#1603)
1 parent 347f511 commit 4bbd175

20 files changed

Lines changed: 675 additions & 23 deletions

File tree

ccv/chains/evm/deployment/go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ require (
1212
github.com/Masterminds/semver/v3 v3.4.0
1313
github.com/ethereum/go-ethereum v1.17.0
1414
github.com/smartcontractkit/chain-selectors v1.0.97
15-
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260310154354-52a02454d61e
16-
github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260310154354-52a02454d61e
17-
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260310154354-52a02454d61e
18-
github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260310154354-52a02454d61e
15+
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260310185019-fb9ea4228dd1
16+
github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260310185019-fb9ea4228dd1
17+
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260310185019-fb9ea4228dd1
18+
github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260310185019-fb9ea4228dd1
1919
github.com/smartcontractkit/chainlink-common v0.9.6-0.20260114142648-bd9e1b483e96
2020
github.com/smartcontractkit/chainlink-deployments-framework v0.80.2
2121
github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd

ccv/chains/evm/deployment/go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -729,12 +729,12 @@ github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0ku
729729
github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w=
730730
github.com/smartcontractkit/chainlink-aptos v0.0.0-20251024142440-51f2ad2652a2 h1:vGdeMwHO3ow88HvxfhA4DDPYNY0X9jmdux7L83UF/W8=
731731
github.com/smartcontractkit/chainlink-aptos v0.0.0-20251024142440-51f2ad2652a2/go.mod h1:iteU0WORHkArACVh/HoY/1bipV4TcNcJdTmom9uIT0E=
732-
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260310154354-52a02454d61e h1:4bsuBTHciVvpwBXxoLdxTh4oDf7R/9v48BhXs3H5Sf8=
733-
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260310154354-52a02454d61e/go.mod h1:hl57wj/oxsh+ieJkvTkYxyB6+Gv22QiVjjcmh7fa0AI=
734-
github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260310154354-52a02454d61e h1:inqpDKW9rPdzO1mi1YikPvcP/ZahJK2woaFdg5d9XFM=
735-
github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260310154354-52a02454d61e/go.mod h1:Zp8erzWAVrADEhbR0EjhWFbEdr98Sdz4yb0LKKMccA8=
736-
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260310154354-52a02454d61e h1:y2UF1jOhv8+p+PmGQxF9Rphv6BCJeCRSRiVnHB1GIfg=
737-
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260310154354-52a02454d61e/go.mod h1:+A8q4+NjgjbbRpBcll7wvoer3AdOa5xgH4TRtkE68XU=
732+
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260310185019-fb9ea4228dd1 h1:zciSOosLgo8Z20XVGLLv/nAcgenCb0b2uw5cwHu9zlY=
733+
github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260310185019-fb9ea4228dd1/go.mod h1:hl57wj/oxsh+ieJkvTkYxyB6+Gv22QiVjjcmh7fa0AI=
734+
github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260310185019-fb9ea4228dd1 h1:jytm+W86+ay7zfmiHu9laPjZzH+ck7++pns1xrP+QFI=
735+
github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260310185019-fb9ea4228dd1/go.mod h1:Zp8erzWAVrADEhbR0EjhWFbEdr98Sdz4yb0LKKMccA8=
736+
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260310185019-fb9ea4228dd1 h1:VteY8zjPz8eHiG0w86oSqWeyl4+4X3kydtSuXY/95P4=
737+
github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260310185019-fb9ea4228dd1/go.mod h1:v00iDRb29FXQYKiR5s/Dz3krgqKXm689w1pTNEMyC1s=
738738
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d h1:xdFpzbApEMz4Rojg2Y2OjFlrh0wu7eB10V2tSZGW5y8=
739739
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d/go.mod h1:bgmqE7x9xwmIVr8PqLbC0M5iPm4AV2DBl596lO6S5Sw=
740740
github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 h1:Z4t2ZY+ZyGWxtcXvPr11y4o3CGqhg3frJB5jXkCSvWA=

ccv/chains/evm/deployment/latest/operations/erc20_lock_box/erc20_lock_box.go

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ccv/chains/evm/deployment/latest/operations/siloed_usdc_token_pool/siloed_usdc_token_pool.go

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ccv/chains/evm/deployment/operations_gen_config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ contracts:
194194
access: owner
195195
- name: getAllAuthorizedCallers
196196
access: public
197+
- name: getAllLockBoxConfigs
198+
access: public
197199

198200
- contract_name: LombardTokenPool
199201
version: "2.0.0"
@@ -246,6 +248,8 @@ contracts:
246248
access: owner
247249
- name: getAllAuthorizedCallers
248250
access: public
251+
- name : deposit
252+
access: public
249253

250254
- contract_name: CommitteeVerifier
251255
version: "2.0.0"

ccv/chains/evm/deployment/v1_7_0/adapters/cctp_chain.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ func (c *CCTPChainAdapter) ConfigureCCTPChainForLanes() *operations.Sequence[ada
3434
return cctp.ConfigureCCTPChainForLanes
3535
}
3636

37+
// MigrateHybridLockReleaseLiquidity returns the sequence for migrating liquidity from a HybridLockReleaseUSDCTokenPool
38+
// into per-chain siloed lockboxes on the home chain.
39+
func (c *CCTPChainAdapter) MigrateHybridLockReleaseLiquidity() *operations.Sequence[adapters.MigrateHybridLockReleaseLiquidityInput, seq_core.OnChainOutput, adapters.MigrateHybridLockReleaseLiquidityDeps] {
40+
return cctp.MigrateHybridLockReleaseLiquidity
41+
}
42+
3743
// CCTPV2AllowedCallerOnDest returns the address allowed to trigger message reception on the remote domain.
3844
// On dest, the caller of CCTPV2 is the CCTPMessageTransmitterProxy 2.0.0.
3945
func (c *CCTPChainAdapter) CCTPV2AllowedCallerOnDest(d datastore.DataStore, b chain.BlockChains, chainSelector uint64) ([]byte, error) {
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package changesets
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
chain_selectors "github.com/smartcontractkit/chain-selectors"
8+
9+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
10+
cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
11+
cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations"
12+
13+
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/sequences/cctp"
14+
"github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets"
15+
mcms_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/mcms"
16+
"github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/adapters"
17+
)
18+
19+
// MigrateHybridLockReleaseLiquidityConfig is the configuration for the MigrateHybridLockReleaseLiquidity changeset.
20+
// This changeset is EVM-only and intended for Ethereum mainnet/Sepolia (home chains) where the hybrid lock-release
21+
// pool and siloed lockboxes live.
22+
type MigrateHybridLockReleaseLiquidityConfig struct {
23+
// ChainSelector is the home chain where liquidity will be migrated (must be Ethereum mainnet or Sepolia).
24+
ChainSelector uint64
25+
// HybridLockReleaseTokenPool is the address of the existing HybridLockReleaseUSDCTokenPool.
26+
HybridLockReleaseTokenPool string
27+
// SiloedUSDCTokenPool is the address of the SiloedUSDCTokenPool to migrate liquidity into.
28+
SiloedUSDCTokenPool string
29+
// USDCToken is the address of the USDC token contract.
30+
USDCToken string
31+
// LockReleaseChainSelectors specifies which remote chains' locked liquidity to migrate.
32+
LockReleaseChainSelectors []uint64
33+
// LiquidityWithdrawPercent is the percent of locked liquidity to migrate (1-100).
34+
LiquidityWithdrawPercent uint8
35+
// MCMS configures the resulting proposal. Required because this migration
36+
// operates on timelock-owned contracts and all operations must execute
37+
// atomically within a single MCMS proposal batch.
38+
MCMS mcms_utils.Input
39+
}
40+
41+
// MigrateHybridLockReleaseLiquidity returns an EVM-only changeset that migrates liquidity from a
42+
// HybridLockReleaseUSDCTokenPool into per-chain siloed lockboxes on the home chain.
43+
func MigrateHybridLockReleaseLiquidity(mcmsRegistry *changesets.MCMSReaderRegistry) cldf_deployment.ChangeSetV2[MigrateHybridLockReleaseLiquidityConfig] {
44+
return cldf_deployment.CreateChangeSet(
45+
makeApplyMigrateHybridLockReleaseLiquidity(mcmsRegistry),
46+
makeVerifyMigrateHybridLockReleaseLiquidity(),
47+
)
48+
}
49+
50+
func makeVerifyMigrateHybridLockReleaseLiquidity() func(cldf_deployment.Environment, MigrateHybridLockReleaseLiquidityConfig) error {
51+
return func(e cldf_deployment.Environment, cfg MigrateHybridLockReleaseLiquidityConfig) error {
52+
if err := cfg.MCMS.Validate(); err != nil {
53+
return fmt.Errorf("MCMS config is required and must be valid: %w", err)
54+
}
55+
if _, err := chain_selectors.GetSelectorFamily(cfg.ChainSelector); err != nil {
56+
return fmt.Errorf("invalid chain selector %d: %w", cfg.ChainSelector, err)
57+
}
58+
// Liquidity migration is only supported on Ethereum home chains.
59+
if cfg.ChainSelector != chain_selectors.ETHEREUM_MAINNET.Selector && cfg.ChainSelector != chain_selectors.ETHEREUM_TESTNET_SEPOLIA.Selector {
60+
return fmt.Errorf("liquidity migration is only supported on Ethereum mainnet or Sepolia, got chain selector %d", cfg.ChainSelector)
61+
}
62+
if !common.IsHexAddress(cfg.HybridLockReleaseTokenPool) {
63+
return fmt.Errorf("invalid HybridLockReleaseTokenPool address for chain %d", cfg.ChainSelector)
64+
}
65+
if !common.IsHexAddress(cfg.SiloedUSDCTokenPool) {
66+
return fmt.Errorf("invalid SiloedUSDCTokenPool address for chain %d", cfg.ChainSelector)
67+
}
68+
if !common.IsHexAddress(cfg.USDCToken) {
69+
return fmt.Errorf("invalid USDCToken address for chain %d", cfg.ChainSelector)
70+
}
71+
if len(cfg.LockReleaseChainSelectors) == 0 {
72+
return fmt.Errorf("at least one lock release chain selector must be provided")
73+
}
74+
if cfg.LiquidityWithdrawPercent == 0 || cfg.LiquidityWithdrawPercent > 100 {
75+
return fmt.Errorf("liquidity withdraw percent must be between 1 and 100")
76+
}
77+
for _, sel := range cfg.LockReleaseChainSelectors {
78+
if _, err := chain_selectors.GetSelectorFamily(sel); err != nil {
79+
return fmt.Errorf("invalid lock release chain selector %d: %w", sel, err)
80+
}
81+
}
82+
return nil
83+
}
84+
}
85+
86+
func makeApplyMigrateHybridLockReleaseLiquidity(
87+
mcmsRegistry *changesets.MCMSReaderRegistry,
88+
) func(cldf_deployment.Environment, MigrateHybridLockReleaseLiquidityConfig) (cldf_deployment.ChangesetOutput, error) {
89+
return func(e cldf_deployment.Environment, cfg MigrateHybridLockReleaseLiquidityConfig) (cldf_deployment.ChangesetOutput, error) {
90+
reader, ok := mcmsRegistry.GetMCMSReader(chain_selectors.FamilyEVM)
91+
if !ok {
92+
return cldf_deployment.ChangesetOutput{}, fmt.Errorf("no MCMS reader registered for chain family 'evm'")
93+
}
94+
timelockRef, err := reader.GetTimelockRef(e, cfg.ChainSelector, cfg.MCMS)
95+
if err != nil {
96+
return cldf_deployment.ChangesetOutput{}, fmt.Errorf("failed to resolve timelock address on chain %d: %w", cfg.ChainSelector, err)
97+
}
98+
99+
deps := adapters.MigrateHybridLockReleaseLiquidityDeps{
100+
BlockChains: e.BlockChains,
101+
}
102+
in := adapters.MigrateHybridLockReleaseLiquidityInput{
103+
ChainSelector: cfg.ChainSelector,
104+
HybridLockReleaseTokenPool: cfg.HybridLockReleaseTokenPool,
105+
SiloedUSDCTokenPool: cfg.SiloedUSDCTokenPool,
106+
USDCToken: cfg.USDCToken,
107+
LockReleaseChainSelectors: cfg.LockReleaseChainSelectors,
108+
LiquidityWithdrawPercent: cfg.LiquidityWithdrawPercent,
109+
MCMSTimelockAddress: timelockRef.Address,
110+
}
111+
112+
report, err := cldf_ops.ExecuteSequence(e.OperationsBundle, cctp.MigrateHybridLockReleaseLiquidity, deps, in)
113+
if err != nil {
114+
return cldf_deployment.ChangesetOutput{}, fmt.Errorf("failed to migrate hybrid lock-release liquidity on chain %d: %w", cfg.ChainSelector, err)
115+
}
116+
117+
batchOps := report.Output.BatchOps
118+
reports := report.ExecutionReports
119+
120+
newDS := datastore.NewMemoryDataStore()
121+
for _, addr := range report.Output.Addresses {
122+
if err := newDS.Addresses().Add(addr); err != nil {
123+
return cldf_deployment.ChangesetOutput{}, fmt.Errorf("failed to add address %s to datastore: %w", addr.Address, err)
124+
}
125+
}
126+
127+
return changesets.NewOutputBuilder(e, mcmsRegistry).
128+
WithReports(reports).
129+
WithBatchOps(batchOps).
130+
WithDataStore(newDS).
131+
Build(cfg.MCMS)
132+
}
133+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package erc20
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
7+
"github.com/Masterminds/semver/v3"
8+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/core/types"
11+
12+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract"
13+
cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
14+
erc20_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/latest/erc20"
15+
)
16+
17+
var ContractType cldf_deployment.ContractType = "ERC20"
18+
19+
var Version = semver.MustParse("1.0.0")
20+
21+
type ApproveArgs struct {
22+
Spender common.Address
23+
Amount *big.Int
24+
}
25+
26+
var Approve = contract.NewWrite(contract.WriteParams[ApproveArgs, *erc20_bindings.ERC20]{
27+
Name: "erc20:approve",
28+
Version: Version,
29+
Description: "Approves a spender for ERC20 transfers",
30+
ContractType: ContractType,
31+
ContractABI: erc20_bindings.ERC20ABI,
32+
NewContract: erc20_bindings.NewERC20,
33+
IsAllowedCaller: contract.AllCallersAllowed[*erc20_bindings.ERC20, ApproveArgs],
34+
Validate: validateApproveArgs,
35+
CallContract: func(token *erc20_bindings.ERC20, opts *bind.TransactOpts, args ApproveArgs) (*types.Transaction, error) {
36+
return token.Approve(opts, args.Spender, args.Amount)
37+
},
38+
})
39+
40+
// ApproveProposalOnly is identical to Approve but forces the operation into a proposal
41+
// rather than executing directly. Use when the approve must be called by a timelock
42+
// as part of an atomic MCMS batch (e.g., withdraw → approve → deposit flows).
43+
var ApproveProposalOnly = contract.NewWrite(contract.WriteParams[ApproveArgs, *erc20_bindings.ERC20]{
44+
Name: "erc20:approve-proposal-only",
45+
Version: Version,
46+
Description: "Approves a spender for ERC20 transfers (proposal-only, never executed directly)",
47+
ContractType: ContractType,
48+
ContractABI: erc20_bindings.ERC20ABI,
49+
NewContract: erc20_bindings.NewERC20,
50+
IsAllowedCaller: contract.NoCallersAllowed[*erc20_bindings.ERC20, ApproveArgs],
51+
Validate: validateApproveArgs,
52+
CallContract: func(token *erc20_bindings.ERC20, opts *bind.TransactOpts, args ApproveArgs) (*types.Transaction, error) {
53+
return token.Approve(opts, args.Spender, args.Amount)
54+
},
55+
})
56+
57+
func validateApproveArgs(args ApproveArgs) error {
58+
if args.Spender == (common.Address{}) {
59+
return fmt.Errorf("spender address must be set")
60+
}
61+
if args.Amount == nil || args.Amount.Sign() <= 0 {
62+
return fmt.Errorf("amount must be greater than zero")
63+
}
64+
return nil
65+
}

0 commit comments

Comments
 (0)