Skip to content

Commit 1fc8bbc

Browse files
authored
Register Token Changeset (#1470)
* Register Token Changeset * Fix
1 parent 2d8cbcf commit 1fc8bbc

File tree

4 files changed

+380
-0
lines changed

4 files changed

+380
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
Token datastore.AddressRef
22+
ExternalAdmin common.Address
23+
}
24+
25+
func (c RegisterTokenCfg) ChainSelector() uint64 {
26+
return c.ChainSel
27+
}
28+
29+
var RegisterToken = changesets.NewFromOnChainSequence(changesets.NewFromOnChainSequenceParams[
30+
sequences.RegisterTokenInput,
31+
evm.Chain,
32+
RegisterTokenCfg,
33+
]{
34+
Sequence: sequences.RegisterToken,
35+
ResolveInput: func(e cldf_deployment.Environment, cfg RegisterTokenCfg) (sequences.RegisterTokenInput, error) {
36+
tokenAdminRegistryRef := datastore.AddressRef{
37+
Type: datastore.ContractType(token_admin_registry.ContractType),
38+
Version: token_admin_registry.Version,
39+
}
40+
tokenAdminRegistryAddress, err := datastore_utils.FindAndFormatRef(e.DataStore, tokenAdminRegistryRef, cfg.ChainSel, evm_datastore_utils.ToEVMAddress)
41+
if err != nil {
42+
return sequences.RegisterTokenInput{}, fmt.Errorf("failed to find token admin registry address: %w", err)
43+
}
44+
tokenAddress, err := datastore_utils.FindAndFormatRef(e.DataStore, cfg.Token, cfg.ChainSel, evm_datastore_utils.ToEVMAddress)
45+
if err != nil {
46+
return sequences.RegisterTokenInput{}, fmt.Errorf("failed to find token address: %w", err)
47+
}
48+
tokenPoolAddress, err := datastore_utils.FindAndFormatRef(e.DataStore, cfg.TokenPool, cfg.ChainSel, evm_datastore_utils.ToEVMAddress)
49+
if err != nil {
50+
return sequences.RegisterTokenInput{}, fmt.Errorf("failed to find token pool address: %w", err)
51+
}
52+
return sequences.RegisterTokenInput{
53+
ChainSelector: cfg.ChainSel,
54+
TokenPoolAddress: tokenPoolAddress,
55+
TokenAdminRegistryAddress: tokenAdminRegistryAddress,
56+
TokenAddress: tokenAddress,
57+
ExternalAdmin: cfg.ExternalAdmin,
58+
}, nil
59+
},
60+
ResolveDep: evm_sequences.ResolveEVMChainDep[RegisterTokenCfg],
61+
})
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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package sequences
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/Masterminds/semver/v3"
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/operations/token_pool"
11+
"github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences"
12+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
13+
cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations"
14+
mcms_types "github.com/smartcontractkit/mcms/types"
15+
)
16+
17+
// RegisterTokenInput is the input for the RegisterToken sequence.
18+
type RegisterTokenInput struct {
19+
// ChainSelector is the chain selector for the chain being configured.
20+
ChainSelector uint64
21+
// TokenAddress is the address of the token to register.
22+
TokenAddress common.Address
23+
// TokenPoolAddress is the address of the token pool.
24+
TokenPoolAddress common.Address
25+
// ExternalAdmin is specified when we want to propose an admin that we don't control.
26+
// In this case, only proposeAdministrator will be called.
27+
ExternalAdmin common.Address
28+
// TokenAdminRegistryAddress is the address of the TokenAdminRegistry contract.
29+
TokenAdminRegistryAddress common.Address
30+
}
31+
32+
func (c RegisterTokenInput) Validate(chain evm.Chain) error {
33+
if c.ChainSelector != chain.Selector {
34+
return fmt.Errorf("chain selector %d does not match chain %s", c.ChainSelector, chain)
35+
}
36+
return nil
37+
}
38+
39+
var RegisterToken = cldf_ops.NewSequence(
40+
"register-token",
41+
semver.MustParse("1.5.0"),
42+
"Registers a token with CCIP via the token admin registry",
43+
func(b cldf_ops.Bundle, chain evm.Chain, input RegisterTokenInput) (output sequences.OnChainOutput, err error) {
44+
if err := input.Validate(chain); err != nil {
45+
return sequences.OnChainOutput{}, fmt.Errorf("invalid input: %w", err)
46+
}
47+
writes := make([]evm_contract.WriteOutput, 0)
48+
49+
tokenAddress := input.TokenAddress
50+
if tokenAddress == (common.Address{}) {
51+
getTokenReport, err := cldf_ops.ExecuteOperation(b, token_pool.GetToken, chain, evm_contract.FunctionInput[any]{
52+
ChainSelector: input.ChainSelector,
53+
Address: input.TokenPoolAddress,
54+
})
55+
if err != nil {
56+
return sequences.OnChainOutput{}, fmt.Errorf("failed to get token address: %w", err)
57+
}
58+
tokenAddress = getTokenReport.Output
59+
}
60+
61+
// Get the current token config from the token admin registry.
62+
tokenConfigReport, err := cldf_ops.ExecuteOperation(b, token_admin_registry.GetTokenConfig, chain, evm_contract.FunctionInput[common.Address]{
63+
ChainSelector: input.ChainSelector,
64+
Address: input.TokenAdminRegistryAddress,
65+
Args: tokenAddress,
66+
})
67+
if err != nil {
68+
return sequences.OnChainOutput{}, fmt.Errorf("failed to get token config: %w", err)
69+
}
70+
admin := tokenConfigReport.Output.Administrator
71+
pendingAdmin := tokenConfigReport.Output.PendingAdministrator
72+
tokenPoolSet := tokenConfigReport.Output.TokenPool
73+
74+
// Get the owner of the token admin registry.
75+
ownerReport, err := cldf_ops.ExecuteOperation(b, token_admin_registry.Owner, chain, evm_contract.FunctionInput[any]{
76+
ChainSelector: input.ChainSelector,
77+
Address: input.TokenAdminRegistryAddress,
78+
})
79+
if err != nil {
80+
return sequences.OnChainOutput{}, fmt.Errorf("failed to get owner of token admin registry: %w", err)
81+
}
82+
registryOwner := ownerReport.Output
83+
84+
// Propose the admin for the token if no admin exists yet.
85+
if admin == (common.Address{}) {
86+
desiredAdmin := input.ExternalAdmin
87+
if desiredAdmin == (common.Address{}) {
88+
// If no external admin is specified, we set the desired admin to the registry owner.
89+
desiredAdmin = registryOwner
90+
}
91+
proposeAdminReport, err := cldf_ops.ExecuteOperation(b, token_admin_registry.ProposeAdministrator, chain, evm_contract.FunctionInput[token_admin_registry.ProposeAdministratorArgs]{
92+
ChainSelector: input.ChainSelector,
93+
Address: input.TokenAdminRegistryAddress,
94+
Args: token_admin_registry.ProposeAdministratorArgs{
95+
TokenAddress: tokenAddress,
96+
Administrator: desiredAdmin,
97+
},
98+
})
99+
if err != nil {
100+
return sequences.OnChainOutput{}, fmt.Errorf("failed to propose admin: %w", err)
101+
}
102+
writes = append(writes, proposeAdminReport.Output)
103+
pendingAdmin = desiredAdmin
104+
}
105+
106+
// Accept the admin role if the registry owner is the pending admin.
107+
if pendingAdmin == registryOwner {
108+
acceptAdminReport, err := cldf_ops.ExecuteOperation(b, token_admin_registry.AcceptAdminRole, chain, evm_contract.FunctionInput[token_admin_registry.AcceptAdminRoleArgs]{
109+
ChainSelector: input.ChainSelector,
110+
Address: input.TokenAdminRegistryAddress,
111+
Args: token_admin_registry.AcceptAdminRoleArgs{
112+
TokenAddress: tokenAddress,
113+
},
114+
})
115+
if err != nil {
116+
return sequences.OnChainOutput{}, fmt.Errorf("failed to accept admin role: %w", err)
117+
}
118+
writes = append(writes, acceptAdminReport.Output)
119+
admin = registryOwner
120+
}
121+
122+
// Set the token pool on the token admin registry if the registry owner is the admin and the pool is not set.
123+
if admin == registryOwner && tokenPoolSet != input.TokenPoolAddress {
124+
setPoolReport, err := cldf_ops.ExecuteOperation(b, token_admin_registry.SetPool, chain, evm_contract.FunctionInput[token_admin_registry.SetPoolArgs]{
125+
ChainSelector: input.ChainSelector,
126+
Address: input.TokenAdminRegistryAddress,
127+
Args: token_admin_registry.SetPoolArgs{
128+
TokenAddress: tokenAddress,
129+
TokenPoolAddress: input.TokenPoolAddress,
130+
},
131+
})
132+
if err != nil {
133+
return sequences.OnChainOutput{}, fmt.Errorf("failed to set token pool: %w", err)
134+
}
135+
writes = append(writes, setPoolReport.Output)
136+
}
137+
138+
batchOp, err := evm_contract.NewBatchOperationFromWrites(writes)
139+
if err != nil {
140+
return sequences.OnChainOutput{}, fmt.Errorf("failed to create batch operation from writes: %w", err)
141+
}
142+
143+
return sequences.OnChainOutput{BatchOps: []mcms_types.BatchOperation{batchOp}}, nil
144+
},
145+
)
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)