Skip to content

Commit 89e3497

Browse files
committed
Add withdrawFeeTokens changeset using auto-generated operations
All operations use auto-generated latest/operations/ packages. Includes OnRamp, CommitteeVerifier, and all 11 TokenPool variant types.
1 parent 4bbd175 commit 89e3497

3 files changed

Lines changed: 539 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package changesets
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
evm_datastore_utils "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/datastore"
8+
evm_sequences "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/sequences"
9+
"github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets"
10+
datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore"
11+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
12+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
13+
cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
14+
15+
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/sequences"
16+
)
17+
18+
// WithdrawFeeTokensCfg is the YAML/pipeline input for the WithdrawFeeTokens changeset.
19+
type WithdrawFeeTokensCfg struct {
20+
ChainSel uint64
21+
// ContractRefs identifies the contracts to withdraw from.
22+
ContractRefs []datastore.AddressRef
23+
// FeeTokens is the list of ERC-20 token addresses to withdraw from each contract.
24+
FeeTokens []common.Address
25+
// Recipient is required when any ref is a TokenPool. Ignored for OnRamp/CommitteeVerifier.
26+
Recipient common.Address
27+
}
28+
29+
// ChainSelector implements the single-chain config interface required by
30+
// ResolveEVMChainDep, which looks up the evm.Chain from the environment.
31+
func (c WithdrawFeeTokensCfg) ChainSelector() uint64 {
32+
return c.ChainSel
33+
}
34+
35+
// WithdrawFeeTokens wraps the withdraw-fee-tokens sequence into a changeset.
36+
// It resolves each user-supplied AddressRef against the datastore to obtain on-chain
37+
// addresses, validates that every ref is a known FeeTokenHandler, and delegates
38+
// execution to the sequence. The result is an MCMS proposal containing all withdrawals.
39+
var WithdrawFeeTokens = changesets.NewFromOnChainSequence(changesets.NewFromOnChainSequenceParams[
40+
sequences.WithdrawFeeTokensInput,
41+
evm.Chain,
42+
WithdrawFeeTokensCfg,
43+
]{
44+
Sequence: sequences.WithdrawFeeTokens,
45+
46+
// ResolveInput converts the user-facing config into the sequence's input by:
47+
// 1. Validating each ref is a supported FeeTokenHandler type.
48+
// 2. Looking up the deployed address in the environment's datastore.
49+
// 3. Building fully-resolved AddressRefs with the on-chain address populated.
50+
ResolveInput: func(e cldf_deployment.Environment, cfg WithdrawFeeTokensCfg) (sequences.WithdrawFeeTokensInput, error) {
51+
resolvedRefs := make([]datastore.AddressRef, 0, len(cfg.ContractRefs))
52+
for _, ref := range cfg.ContractRefs {
53+
// Reject unknown contract types early, before hitting the datastore.
54+
if !sequences.IsFeeTokenHandler(ref.Type) {
55+
return sequences.WithdrawFeeTokensInput{}, fmt.Errorf(
56+
"contract type %q is not a supported FeeTokenHandler", ref.Type,
57+
)
58+
}
59+
// Look up the contract's deployed address from the datastore.
60+
resolvedAddr, err := datastore_utils.FindAndFormatRef(e.DataStore, ref, cfg.ChainSel, evm_datastore_utils.ToEVMAddress)
61+
if err != nil {
62+
return sequences.WithdrawFeeTokensInput{}, fmt.Errorf(
63+
"failed to resolve contract ref (type=%s, version=%v, qualifier=%s) on chain %d: %w",
64+
ref.Type, ref.Version, ref.Qualifier, cfg.ChainSel, err,
65+
)
66+
}
67+
resolvedRefs = append(resolvedRefs, datastore.AddressRef{
68+
Address: resolvedAddr.Hex(),
69+
ChainSelector: cfg.ChainSel,
70+
Type: ref.Type,
71+
Version: ref.Version,
72+
Qualifier: ref.Qualifier,
73+
})
74+
}
75+
76+
return sequences.WithdrawFeeTokensInput{
77+
ChainSelector: cfg.ChainSel,
78+
ContractRefs: resolvedRefs,
79+
FeeTokens: cfg.FeeTokens,
80+
Recipient: cfg.Recipient,
81+
}, nil
82+
},
83+
84+
// ResolveDep looks up the evm.Chain object from the environment using ChainSel.
85+
ResolveDep: evm_sequences.ResolveEVMChainDep[WithdrawFeeTokensCfg],
86+
})
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package changesets_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/Masterminds/semver/v3"
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/latest/operations/burn_mint_token_pool"
11+
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/latest/operations/committee_verifier"
12+
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/latest/operations/onramp"
13+
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/latest/operations/token_pool"
14+
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/changesets"
15+
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/create2_factory"
16+
"github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/testsetup"
17+
contract_utils "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract"
18+
cs_core "github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets"
19+
"github.com/smartcontractkit/chainlink-ccip/deployment/utils/mcms"
20+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
21+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
22+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment"
23+
)
24+
25+
const testChainSelector = 5009297550715157269
26+
27+
// TestWithdrawFeeTokens_VerifyPreconditions tests that the changeset rejects
28+
// invalid configurations during precondition validation (before any on-chain work).
29+
func TestWithdrawFeeTokens_VerifyPreconditions(t *testing.T) {
30+
e, err := environment.New(t.Context(),
31+
environment.WithEVMSimulated(t, []uint64{testChainSelector}),
32+
)
33+
require.NoError(t, err)
34+
35+
tests := []struct {
36+
desc string
37+
input cs_core.WithMCMS[changesets.WithdrawFeeTokensCfg]
38+
expectedErr string
39+
}{
40+
{
41+
desc: "valid input with OnRamp ref",
42+
input: cs_core.WithMCMS[changesets.WithdrawFeeTokensCfg]{
43+
MCMS: mcms.Input{},
44+
Cfg: changesets.WithdrawFeeTokensCfg{
45+
ChainSel: testChainSelector,
46+
ContractRefs: []datastore.AddressRef{
47+
{
48+
Type: datastore.ContractType(onramp.ContractType),
49+
Version: onramp.Version,
50+
},
51+
},
52+
FeeTokens: []common.Address{common.HexToAddress("0x01")},
53+
},
54+
},
55+
expectedErr: "expected to find exactly 1 ref",
56+
},
57+
{
58+
desc: "invalid chain selector",
59+
input: cs_core.WithMCMS[changesets.WithdrawFeeTokensCfg]{
60+
MCMS: mcms.Input{},
61+
Cfg: changesets.WithdrawFeeTokensCfg{
62+
ChainSel: 12345,
63+
ContractRefs: []datastore.AddressRef{
64+
{
65+
Type: datastore.ContractType(onramp.ContractType),
66+
Version: onramp.Version,
67+
},
68+
},
69+
FeeTokens: []common.Address{common.HexToAddress("0x01")},
70+
},
71+
},
72+
expectedErr: "failed to resolve contract ref",
73+
},
74+
{
75+
desc: "unsupported contract type",
76+
input: cs_core.WithMCMS[changesets.WithdrawFeeTokensCfg]{
77+
MCMS: mcms.Input{},
78+
Cfg: changesets.WithdrawFeeTokensCfg{
79+
ChainSel: testChainSelector,
80+
ContractRefs: []datastore.AddressRef{
81+
{
82+
Type: "UnsupportedContract",
83+
Version: semver.MustParse("1.0.0"),
84+
},
85+
},
86+
FeeTokens: []common.Address{common.HexToAddress("0x01")},
87+
},
88+
},
89+
expectedErr: "not a supported FeeTokenHandler",
90+
},
91+
}
92+
93+
for _, test := range tests {
94+
t.Run(test.desc, func(t *testing.T) {
95+
mcmsRegistry := cs_core.GetRegistry()
96+
err := changesets.WithdrawFeeTokens(mcmsRegistry).VerifyPreconditions(*e, test.input)
97+
if test.expectedErr != "" {
98+
require.ErrorContains(t, err, test.expectedErr)
99+
} else {
100+
require.NoError(t, err)
101+
}
102+
})
103+
}
104+
}
105+
106+
// TestWithdrawFeeTokens_Apply deploys a full set of chain contracts on a simulated
107+
// chain, then verifies the changeset can successfully withdraw fee tokens from
108+
// OnRamp, CommitteeVerifier, and multiple contracts at once.
109+
func TestWithdrawFeeTokens_Apply(t *testing.T) {
110+
e, err := environment.New(t.Context(),
111+
environment.WithEVMSimulated(t, []uint64{testChainSelector}),
112+
)
113+
require.NoError(t, err)
114+
115+
mcmsRegistry := cs_core.GetRegistry()
116+
117+
create2FactoryRef, err := contract_utils.MaybeDeployContract(
118+
e.OperationsBundle, create2_factory.Deploy,
119+
e.BlockChains.EVMChains()[testChainSelector],
120+
contract_utils.DeployInput[create2_factory.ConstructorArgs]{
121+
TypeAndVersion: deployment.NewTypeAndVersion(create2_factory.ContractType, *semver.MustParse("1.7.0")),
122+
ChainSelector: testChainSelector,
123+
Args: create2_factory.ConstructorArgs{
124+
AllowList: []common.Address{e.BlockChains.EVMChains()[testChainSelector].DeployerKey.From},
125+
},
126+
}, nil,
127+
)
128+
require.NoError(t, err, "Failed to deploy CREATE2Factory")
129+
130+
deployOut, err := changesets.DeployChainContracts(mcmsRegistry).Apply(*e, cs_core.WithMCMS[changesets.DeployChainContractsCfg]{
131+
MCMS: mcms.Input{},
132+
Cfg: changesets.DeployChainContractsCfg{
133+
ChainSel: testChainSelector,
134+
CREATE2Factory: common.HexToAddress(create2FactoryRef.Address),
135+
Params: testsetup.CreateBasicContractParams(),
136+
},
137+
})
138+
require.NoError(t, err, "Failed to deploy chain contracts")
139+
140+
deployedAddrs, err := deployOut.DataStore.Addresses().Fetch()
141+
require.NoError(t, err)
142+
143+
var wethAddr common.Address
144+
for _, ref := range deployedAddrs {
145+
if ref.Type == "WETH9" {
146+
wethAddr = common.HexToAddress(ref.Address)
147+
break
148+
}
149+
}
150+
require.NotEqual(t, common.Address{}, wethAddr, "WETH should be deployed")
151+
152+
ds := datastore.NewMemoryDataStore()
153+
for _, ref := range deployedAddrs {
154+
require.NoError(t, ds.Addresses().Add(ref))
155+
}
156+
e.DataStore = ds.Seal()
157+
158+
tests := []struct {
159+
desc string
160+
input cs_core.WithMCMS[changesets.WithdrawFeeTokensCfg]
161+
}{
162+
{
163+
desc: "withdraw from OnRamp",
164+
input: cs_core.WithMCMS[changesets.WithdrawFeeTokensCfg]{
165+
MCMS: mcms.Input{},
166+
Cfg: changesets.WithdrawFeeTokensCfg{
167+
ChainSel: testChainSelector,
168+
ContractRefs: []datastore.AddressRef{
169+
{
170+
Type: datastore.ContractType(onramp.ContractType),
171+
Version: onramp.Version,
172+
},
173+
},
174+
FeeTokens: []common.Address{wethAddr},
175+
},
176+
},
177+
},
178+
{
179+
desc: "withdraw from CommitteeVerifier",
180+
input: cs_core.WithMCMS[changesets.WithdrawFeeTokensCfg]{
181+
MCMS: mcms.Input{},
182+
Cfg: changesets.WithdrawFeeTokensCfg{
183+
ChainSel: testChainSelector,
184+
ContractRefs: []datastore.AddressRef{
185+
{
186+
Type: datastore.ContractType(committee_verifier.ContractType),
187+
Version: committee_verifier.Version,
188+
Qualifier: "alpha",
189+
},
190+
},
191+
FeeTokens: []common.Address{wethAddr},
192+
},
193+
},
194+
},
195+
{
196+
desc: "withdraw from multiple contracts",
197+
input: cs_core.WithMCMS[changesets.WithdrawFeeTokensCfg]{
198+
MCMS: mcms.Input{},
199+
Cfg: changesets.WithdrawFeeTokensCfg{
200+
ChainSel: testChainSelector,
201+
ContractRefs: []datastore.AddressRef{
202+
{
203+
Type: datastore.ContractType(onramp.ContractType),
204+
Version: onramp.Version,
205+
},
206+
{
207+
Type: datastore.ContractType(committee_verifier.ContractType),
208+
Version: committee_verifier.Version,
209+
Qualifier: "alpha",
210+
},
211+
},
212+
FeeTokens: []common.Address{wethAddr},
213+
},
214+
},
215+
},
216+
}
217+
218+
for _, test := range tests {
219+
t.Run(test.desc, func(t *testing.T) {
220+
_, err := changesets.WithdrawFeeTokens(mcmsRegistry).Apply(*e, test.input)
221+
require.NoError(t, err)
222+
})
223+
}
224+
}
225+
226+
// TestWithdrawFeeTokens_TokenPoolRequiresRecipient verifies that the sequence rejects
227+
// TokenPool withdrawals when no recipient is specified. Covers both the generic
228+
// TokenPool type and a concrete subtype (BurnMintTokenPool) to ensure all pool
229+
// variants are routed through the same validation path.
230+
func TestWithdrawFeeTokens_TokenPoolRequiresRecipient(t *testing.T) {
231+
e, err := environment.New(t.Context(),
232+
environment.WithEVMSimulated(t, []uint64{testChainSelector}),
233+
)
234+
require.NoError(t, err)
235+
236+
mcmsRegistry := cs_core.GetRegistry()
237+
238+
tests := []struct {
239+
desc string
240+
contractType datastore.ContractType
241+
version *semver.Version
242+
}{
243+
{
244+
desc: "generic TokenPool",
245+
contractType: datastore.ContractType(token_pool.ContractType),
246+
version: token_pool.Version,
247+
},
248+
{
249+
desc: "BurnMintTokenPool subtype",
250+
contractType: datastore.ContractType(burn_mint_token_pool.ContractType),
251+
version: burn_mint_token_pool.Version,
252+
},
253+
}
254+
255+
for _, tt := range tests {
256+
t.Run(tt.desc, func(t *testing.T) {
257+
ds := datastore.NewMemoryDataStore()
258+
err := ds.Addresses().Add(datastore.AddressRef{
259+
ChainSelector: testChainSelector,
260+
Type: tt.contractType,
261+
Version: tt.version,
262+
Address: common.HexToAddress("0xDEAD").Hex(),
263+
})
264+
require.NoError(t, err)
265+
e.DataStore = ds.Seal()
266+
267+
_, err = changesets.WithdrawFeeTokens(mcmsRegistry).Apply(*e, cs_core.WithMCMS[changesets.WithdrawFeeTokensCfg]{
268+
MCMS: mcms.Input{},
269+
Cfg: changesets.WithdrawFeeTokensCfg{
270+
ChainSel: testChainSelector,
271+
ContractRefs: []datastore.AddressRef{
272+
{
273+
Type: tt.contractType,
274+
Version: tt.version,
275+
},
276+
},
277+
FeeTokens: []common.Address{common.HexToAddress("0x01")},
278+
},
279+
})
280+
require.ErrorContains(t, err, "recipient is required")
281+
})
282+
}
283+
}

0 commit comments

Comments
 (0)