Skip to content

Commit 7a1eee9

Browse files
committed
Register Token Changeset
1 parent 5137e80 commit 7a1eee9

File tree

4 files changed

+376
-0
lines changed

4 files changed

+376
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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/chains/evm/deployment/v1_5_0/operations/token_admin_registry"
10+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/sequences"
11+
"github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets"
12+
datastore_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore"
13+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
14+
"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
15+
cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
16+
)
17+
18+
type RegisterTokenCfg struct {
19+
ChainSel uint64
20+
TokenPool datastore.AddressRef
21+
ExternalAdmin common.Address
22+
}
23+
24+
func (c RegisterTokenCfg) ChainSelector() uint64 {
25+
return c.ChainSel
26+
}
27+
28+
var RegisterToken = changesets.NewFromOnChainSequence(changesets.NewFromOnChainSequenceParams[
29+
sequences.RegisterTokenInput,
30+
evm.Chain,
31+
RegisterTokenCfg,
32+
]{
33+
Sequence: sequences.RegisterToken,
34+
ResolveInput: func(e cldf_deployment.Environment, cfg RegisterTokenCfg) (sequences.RegisterTokenInput, error) {
35+
tokenAdminRegistryRef := datastore.AddressRef{
36+
Type: datastore.ContractType(token_admin_registry.ContractType),
37+
Version: token_admin_registry.Version,
38+
}
39+
tokenAdminRegistryAddress, err := datastore_utils.FindAndFormatRef(e.DataStore, tokenAdminRegistryRef, cfg.ChainSel, evm_datastore_utils.ToEVMAddress)
40+
if err != nil {
41+
return sequences.RegisterTokenInput{}, fmt.Errorf("failed to find token admin registry address: %w", err)
42+
}
43+
tokenPoolAddress, err := datastore_utils.FindAndFormatRef(e.DataStore, cfg.TokenPool, cfg.ChainSel, evm_datastore_utils.ToEVMAddress)
44+
if err != nil {
45+
return sequences.RegisterTokenInput{}, fmt.Errorf("failed to find token pool address: %w", err)
46+
}
47+
return sequences.RegisterTokenInput{
48+
ChainSelector: cfg.ChainSel,
49+
TokenPoolAddress: tokenPoolAddress,
50+
TokenAdminRegistryAddress: tokenAdminRegistryAddress,
51+
ExternalAdmin: cfg.ExternalAdmin,
52+
}, nil
53+
},
54+
ResolveDep: evm_sequences.ResolveEVMChainDep[RegisterTokenCfg],
55+
})
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package token_pool
2+
3+
import (
4+
"github.com/Masterminds/semver/v3"
5+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract"
8+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_1/token_pool"
9+
cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
10+
)
11+
12+
var ContractType cldf_deployment.ContractType = "TokenPool"
13+
var Version *semver.Version = semver.MustParse("1.5.0")
14+
15+
var GetToken = contract.NewRead(contract.ReadParams[any, common.Address, *token_pool.TokenPool]{
16+
Name: "token-pool:get-token",
17+
Version: Version,
18+
Description: "Gets the local token address for a TokenPool",
19+
ContractType: ContractType,
20+
NewContract: token_pool.NewTokenPool,
21+
CallContract: func(tokenPool *token_pool.TokenPool, opts *bind.CallOpts, args any) (common.Address, error) {
22+
return tokenPool.GetToken(opts)
23+
},
24+
})
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package sequences
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/Masterminds/semver/v3"
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract"
9+
evm_contract "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract"
10+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/token_admin_registry"
11+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/token_pool"
12+
"github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences"
13+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
14+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
15+
cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations"
16+
mcms_types "github.com/smartcontractkit/mcms/types"
17+
)
18+
19+
// RegisterTokenInput is the input for the RegisterToken sequence.
20+
type RegisterTokenInput struct {
21+
// ChainSelector is the chain selector for the chain being configured.
22+
ChainSelector uint64
23+
// TokenAddress is the address of the token to register.
24+
TokenAddress common.Address
25+
// TokenPoolAddress is the address of the token pool.
26+
TokenPoolAddress common.Address
27+
// ExternalAdmin is specified when we want to propose an admin that we don't control.
28+
// In this case, only proposeAdministrator will be called.
29+
ExternalAdmin common.Address
30+
// TokenAdminRegistryAddress is the address of the TokenAdminRegistry contract.
31+
TokenAdminRegistryAddress common.Address
32+
}
33+
34+
func (c RegisterTokenInput) Validate(chain evm.Chain) error {
35+
if c.ChainSelector != chain.Selector {
36+
return fmt.Errorf("chain selector %d does not match chain %s", c.ChainSelector, chain)
37+
}
38+
return nil
39+
}
40+
41+
var RegisterToken = cldf_ops.NewSequence(
42+
"register-token",
43+
semver.MustParse("1.5.0"),
44+
"Registers a token with CCIP via the token admin registry",
45+
func(b operations.Bundle, chain evm.Chain, input RegisterTokenInput) (output sequences.OnChainOutput, err error) {
46+
if err := input.Validate(chain); err != nil {
47+
return sequences.OnChainOutput{}, fmt.Errorf("invalid input: %w", err)
48+
}
49+
writes := make([]contract.WriteOutput, 0)
50+
51+
tokenAddress := input.TokenAddress
52+
if tokenAddress == (common.Address{}) {
53+
getTokenReport, err := cldf_ops.ExecuteOperation(b, token_pool.GetToken, chain, evm_contract.FunctionInput[any]{
54+
ChainSelector: input.ChainSelector,
55+
Address: input.TokenPoolAddress,
56+
})
57+
if err != nil {
58+
return sequences.OnChainOutput{}, fmt.Errorf("failed to get token address: %w", err)
59+
}
60+
tokenAddress = getTokenReport.Output
61+
}
62+
63+
// Get the current token config from the token admin registry.
64+
tokenConfigReport, err := cldf_ops.ExecuteOperation(b, token_admin_registry.GetTokenConfig, chain, evm_contract.FunctionInput[common.Address]{
65+
ChainSelector: input.ChainSelector,
66+
Address: input.TokenAdminRegistryAddress,
67+
Args: tokenAddress,
68+
})
69+
if err != nil {
70+
return sequences.OnChainOutput{}, fmt.Errorf("failed to get token config: %w", err)
71+
}
72+
admin := tokenConfigReport.Output.Administrator
73+
pendingAdmin := tokenConfigReport.Output.PendingAdministrator
74+
tokenPoolSet := tokenConfigReport.Output.TokenPool
75+
76+
// Get the owner of the token admin registry.
77+
ownerReport, err := cldf_ops.ExecuteOperation(b, token_admin_registry.Owner, chain, evm_contract.FunctionInput[any]{
78+
ChainSelector: input.ChainSelector,
79+
Address: input.TokenAdminRegistryAddress,
80+
})
81+
if err != nil {
82+
return sequences.OnChainOutput{}, fmt.Errorf("failed to get owner of token admin registry: %w", err)
83+
}
84+
registryOwner := ownerReport.Output
85+
86+
// Propose the admin for the token if no admin exists yet.
87+
if admin == (common.Address{}) {
88+
desiredAdmin := input.ExternalAdmin
89+
if desiredAdmin == (common.Address{}) {
90+
// If no external admin is specified, we set the desired admin to the registry owner.
91+
desiredAdmin = registryOwner
92+
}
93+
proposeAdminReport, err := cldf_ops.ExecuteOperation(b, token_admin_registry.ProposeAdministrator, chain, evm_contract.FunctionInput[token_admin_registry.ProposeAdministratorArgs]{
94+
ChainSelector: input.ChainSelector,
95+
Address: input.TokenAdminRegistryAddress,
96+
Args: token_admin_registry.ProposeAdministratorArgs{
97+
TokenAddress: tokenAddress,
98+
Administrator: desiredAdmin,
99+
},
100+
})
101+
if err != nil {
102+
return sequences.OnChainOutput{}, fmt.Errorf("failed to propose admin: %w", err)
103+
}
104+
writes = append(writes, proposeAdminReport.Output)
105+
pendingAdmin = desiredAdmin
106+
}
107+
108+
// Accept the admin role if the registry owner is the pending admin.
109+
if pendingAdmin == registryOwner {
110+
acceptAdminReport, err := cldf_ops.ExecuteOperation(b, token_admin_registry.AcceptAdminRole, chain, evm_contract.FunctionInput[token_admin_registry.AcceptAdminRoleArgs]{
111+
ChainSelector: input.ChainSelector,
112+
Address: input.TokenAdminRegistryAddress,
113+
Args: token_admin_registry.AcceptAdminRoleArgs{
114+
TokenAddress: tokenAddress,
115+
},
116+
})
117+
if err != nil {
118+
return sequences.OnChainOutput{}, fmt.Errorf("failed to accept admin role: %w", err)
119+
}
120+
writes = append(writes, acceptAdminReport.Output)
121+
admin = registryOwner
122+
}
123+
124+
// Set the token pool on the token admin registry if the registry owner is the admin and the pool is not set.
125+
if admin == registryOwner && tokenPoolSet != input.TokenPoolAddress {
126+
setPoolReport, err := cldf_ops.ExecuteOperation(b, token_admin_registry.SetPool, chain, evm_contract.FunctionInput[token_admin_registry.SetPoolArgs]{
127+
ChainSelector: input.ChainSelector,
128+
Address: input.TokenAdminRegistryAddress,
129+
Args: token_admin_registry.SetPoolArgs{
130+
TokenAddress: tokenAddress,
131+
TokenPoolAddress: input.TokenPoolAddress,
132+
},
133+
})
134+
if err != nil {
135+
return sequences.OnChainOutput{}, fmt.Errorf("failed to set token pool: %w", err)
136+
}
137+
writes = append(writes, setPoolReport.Output)
138+
}
139+
140+
batchOp, err := contract.NewBatchOperationFromWrites(writes)
141+
if err != nil {
142+
return sequences.OnChainOutput{}, fmt.Errorf("failed to create batch operation from writes: %w", err)
143+
}
144+
145+
return sequences.OnChainOutput{BatchOps: []mcms_types.BatchOperation{batchOp}}, nil
146+
},
147+
)
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package sequences_test
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
evm_contract "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract"
9+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/token_admin_registry"
10+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/sequences"
11+
"github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_1/burn_mint_token_pool"
12+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
13+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment"
14+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
15+
"github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc20"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestRegisterToken(t *testing.T) {
20+
tests := []struct {
21+
desc string
22+
makeInput func(chainSel uint64, tokenAddress, tokenPoolAddress, tokenAdminRegistryAddress common.Address) sequences.RegisterTokenInput
23+
expectedErr string
24+
}{
25+
{
26+
desc: "happy path - no external admin",
27+
makeInput: func(chainSel uint64, tokenAddress, tokenPoolAddress, tokenAdminRegistryAddress common.Address) sequences.RegisterTokenInput {
28+
return sequences.RegisterTokenInput{
29+
ChainSelector: chainSel,
30+
TokenAddress: tokenAddress,
31+
TokenPoolAddress: tokenPoolAddress,
32+
ExternalAdmin: common.Address{},
33+
TokenAdminRegistryAddress: tokenAdminRegistryAddress,
34+
}
35+
},
36+
expectedErr: "",
37+
},
38+
{
39+
desc: "happy path - external admin",
40+
makeInput: func(chainSel uint64, tokenAddress, tokenPoolAddress, tokenAdminRegistryAddress common.Address) sequences.RegisterTokenInput {
41+
return sequences.RegisterTokenInput{
42+
ChainSelector: chainSel,
43+
TokenAddress: tokenAddress,
44+
TokenPoolAddress: tokenPoolAddress,
45+
ExternalAdmin: common.HexToAddress("0x07"),
46+
TokenAdminRegistryAddress: tokenAdminRegistryAddress,
47+
}
48+
},
49+
expectedErr: "",
50+
},
51+
}
52+
53+
for _, test := range tests {
54+
t.Run(test.desc, func(t *testing.T) {
55+
chainSel := uint64(5009297550715157269)
56+
e, err := environment.New(t.Context(),
57+
environment.WithEVMSimulated(t, []uint64{chainSel}),
58+
)
59+
require.NoError(t, err, "Failed to create environment")
60+
require.NotNil(t, e, "Environment should be created")
61+
62+
// Deploy token admin registry
63+
tokenAdminRegistryReport, err := operations.ExecuteOperation(
64+
e.OperationsBundle,
65+
token_admin_registry.Deploy,
66+
e.BlockChains.EVMChains()[chainSel],
67+
evm_contract.DeployInput[token_admin_registry.ConstructorArgs]{
68+
ChainSelector: chainSel,
69+
TypeAndVersion: deployment.NewTypeAndVersion(token_admin_registry.ContractType, *token_admin_registry.Version),
70+
},
71+
)
72+
require.NoError(t, err, "ExecuteOperation should not error")
73+
tokenAdminRegistryAddress := common.HexToAddress(tokenAdminRegistryReport.Output.Address)
74+
75+
// Deploy token
76+
tokenAddress, tx, _, err := burn_mint_erc20.DeployBurnMintERC20(
77+
e.BlockChains.EVMChains()[chainSel].DeployerKey,
78+
e.BlockChains.EVMChains()[chainSel].Client,
79+
"Test Token",
80+
"TEST",
81+
18,
82+
big.NewInt(1000000000000000000),
83+
big.NewInt(0),
84+
)
85+
require.NoError(t, err, "DeployBurnMintERC20 should not error")
86+
_, err = e.BlockChains.EVMChains()[chainSel].Confirm(tx)
87+
require.NoError(t, err, "Confirm should not error")
88+
89+
// Deploy token pool
90+
tokenPoolAddress, _, _, err := burn_mint_token_pool.DeployBurnMintTokenPool(
91+
e.BlockChains.EVMChains()[chainSel].DeployerKey,
92+
e.BlockChains.EVMChains()[chainSel].Client,
93+
tokenAddress,
94+
18,
95+
[]common.Address{},
96+
common.HexToAddress("0x01"),
97+
common.HexToAddress("0x02"),
98+
)
99+
require.NoError(t, err, "DeployBurnMintTokenPool should not error")
100+
101+
input := test.makeInput(
102+
chainSel,
103+
tokenAddress,
104+
tokenPoolAddress,
105+
tokenAdminRegistryAddress,
106+
)
107+
_, err = operations.ExecuteSequence(
108+
e.OperationsBundle,
109+
sequences.RegisterToken,
110+
e.BlockChains.EVMChains()[chainSel],
111+
input,
112+
)
113+
if test.expectedErr != "" {
114+
require.Error(t, err, "ExecuteSequence should error")
115+
require.Contains(t, err.Error(), test.expectedErr)
116+
return
117+
}
118+
require.NoError(t, err, "ExecuteSequence should not error")
119+
120+
// Checks
121+
tokenConfigReport, err := operations.ExecuteOperation(
122+
operations.NewBundle(
123+
e.OperationsBundle.GetContext,
124+
e.OperationsBundle.Logger,
125+
operations.NewMemoryReporter(),
126+
),
127+
token_admin_registry.GetTokenConfig,
128+
e.BlockChains.EVMChains()[chainSel],
129+
evm_contract.FunctionInput[common.Address]{
130+
ChainSelector: chainSel,
131+
Address: tokenAdminRegistryAddress,
132+
Args: tokenAddress,
133+
},
134+
)
135+
require.NoError(t, err, "ExecuteOperation should not error")
136+
if input.ExternalAdmin != (common.Address{}) {
137+
// We can propose an external admin, but we can't accept ownership or set the pool address since we don't control the admin.
138+
require.Equal(t, input.ExternalAdmin, tokenConfigReport.Output.PendingAdministrator)
139+
require.Equal(t, (common.Address{}), tokenConfigReport.Output.Administrator)
140+
require.Equal(t, (common.Address{}), tokenConfigReport.Output.TokenPool)
141+
} else {
142+
// No external admin means that the owner of the token admin registry will be proposed as the admin.
143+
// Since the deployer key is the owner of the token admin registry, it can accept admin rights and set the pool address.
144+
require.Equal(t, (common.Address{}), tokenConfigReport.Output.PendingAdministrator)
145+
require.Equal(t, e.BlockChains.EVMChains()[chainSel].DeployerKey.From, tokenConfigReport.Output.Administrator)
146+
require.Equal(t, input.TokenPoolAddress, tokenConfigReport.Output.TokenPool)
147+
}
148+
})
149+
}
150+
}

0 commit comments

Comments
 (0)