Skip to content
Merged
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
8 changes: 6 additions & 2 deletions deployment/ccip/config/deploy_aptos_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ import (

"github.com/aptos-labs/aptos-go-sdk"

cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
"github.com/smartcontractkit/chainlink-aptos/deployment/ccip/shared"
"github.com/smartcontractkit/chainlink-aptos/deployment/types"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
)

// DeployAptosChainConfig is a configuration for deploying CCIP Package for Aptos chains
type DeployAptosChainConfig struct {
MCMSDeployConfigPerChain map[uint64]types.MCMSWithTimelockConfigV2
MCMSTimelockConfigPerChain map[uint64]cldfproposalutils.TimelockConfig
ContractParamsPerChain map[uint64]ChainContractParams
// ReplaceExisting allows this changeset to take datastore keys that are already
// recorded, as an intentional redeploy of a chain does. Without it, an occupied key is
// an error raised before anything is deployed.
ReplaceExisting bool
}

func (c DeployAptosChainConfig) Validate() error {
Expand Down
4 changes: 4 additions & 0 deletions deployment/ccip/config/deploy_curse_mcms.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import (
type DeployCurseMCMSConfig struct {
CurseMCMSConfigPerChain map[uint64]cldfproposalutils.MCMSWithTimelockConfig
MCMSTimelockConfigPerChain map[uint64]cldfproposalutils.TimelockConfig
// ReplaceExisting allows this changeset to take datastore keys that are already
// recorded, as an intentional redeploy of CurseMCMS does. Without it, an occupied key
// is an error raised before anything is deployed.
ReplaceExisting bool
}

func (c DeployCurseMCMSConfig) Validate() error {
Expand Down
4 changes: 4 additions & 0 deletions deployment/ccip/config/deploy_regulated_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ type DeployRegulatedTokenConfig struct {
// RegistrarPreregister is passed to DeployMCMSRegistrarToExistingObject (default true).
RegistrarPreregister *bool
MCMSConfig *cldfproposalutils.TimelockConfig
// ReplaceExisting allows this changeset to take datastore keys that are already
// recorded, as an intentional redeploy of this token does. Without it, an occupied key
// is an error raised before anything is deployed.
ReplaceExisting bool
}

func (c DeployRegulatedTokenConfig) Validate() error {
Expand Down
15 changes: 15 additions & 0 deletions deployment/ccip/config/token_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/ethereum/go-ethereum/common"

fee_quoter "github.com/smartcontractkit/chainlink-aptos/bindings/ccip/fee_quoter"
"github.com/smartcontractkit/chainlink-aptos/deployment/ccip/shared"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
)
Expand All @@ -23,6 +24,20 @@ type AddTokenPoolConfig struct {
TokenParams TokenParams
MCMSConfig *cldfproposalutils.TimelockConfig
TokenMint *TokenMint
// ReplaceExisting allows this changeset to take datastore keys that are already
// recorded, as an intentional redeploy of this token or pool does. Without it, an
// occupied key is an error raised before anything is deployed.
ReplaceExisting bool
}

// Qualifier returns the datastore qualifier applied to every address this changeset
// records. Token-scoped types are qualified by token symbol, so the symbol is required
// even when the token already exists: pool refs are keyed by
// (chain, PoolType, version, qualifier) with the address excluded, and an empty qualifier
// would collide between two pools of the same type on one chain. The symbol is normalised
// to the convention's canonical form so Aptos rows key identically to other families'.
func (c AddTokenPoolConfig) Qualifier() shared.TokenSymbol {
return shared.TokenSymbol(shared.TokenQualifier(c.TokenParams.Symbol.String()))
}

type EVMRemoteConfig struct {
Expand Down
116 changes: 100 additions & 16 deletions deployment/ccip/cs_add_token_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import (
"github.com/smartcontractkit/mcms"
mcmstypes "github.com/smartcontractkit/mcms/types"

aptosHelpers "github.com/smartcontractkit/chainlink-aptos/bindings/helpers"
mcmsbind "github.com/smartcontractkit/chainlink-aptos/bindings/mcms"
"github.com/smartcontractkit/chainlink-aptos/deployment/ccip/config"
"github.com/smartcontractkit/chainlink-aptos/deployment/ccip/dependency"
seq "github.com/smartcontractkit/chainlink-aptos/deployment/ccip/sequence"
"github.com/smartcontractkit/chainlink-aptos/deployment/ccip/utils"
"github.com/smartcontractkit/chainlink-aptos/deployment/ccip/shared"
"github.com/smartcontractkit/chainlink-aptos/deployment/ccip/utils"
"github.com/smartcontractkit/chainlink-aptos/deployment/stateview"

"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
)
Expand Down Expand Up @@ -46,19 +48,34 @@ func (cs AddTokenPool) VerifyPreconditions(env cldf.Environment, cfg config.AddT
if cfg.MCMSConfig == nil {
errs = append(errs, errors.New("MCMS config is required for AddTokenPool changeset"))
}
// Validate config.TokenParams
// Validate config.TokenParams. The token symbol is required in both paths because it is
// the datastore qualifier for every ref this changeset records (see Qualifier()); the
// remaining params only describe a token this changeset would deploy itself.
if cfg.TokenCodeObjAddress == (aptos.AccountAddress{}) {
err = cfg.TokenParams.Validate()
if err != nil {
errs = append(errs, fmt.Errorf("invalid token parameters: %w", err))
}
} else {
if cfg.Qualifier() == "" {
errs = append(errs, errors.New("TokenParams.Symbol is required: it is the datastore qualifier for the token pool"))
}
if cfg.TokenAddress == (aptos.AccountAddress{}) {
errs = append(errs, errors.New("TokenAddress must be provided when TokenCodeObjAddress is set"))
}
if err := verifyTokenSymbol(env, cfg, state); err != nil {
errs = append(errs, err)
}
}
// Validate config.EVMRemoteConfigs
for chainSelector, remoteConfig := range cfg.EVMRemoteConfigs {
if err := remoteConfig.Validate(); err != nil {
errs = append(errs, fmt.Errorf("invalid EVM remote config for chain %d: %w", chainSelector, err))
}
}
// Validate that the datastore keys this changeset will write are free, before any
// transaction is signed or staged.
errs = append(errs, shared.ValidatePlannedRefs(env, cfg.ReplaceExisting, plannedTokenPoolRefs(cfg)))
// Validate if token address is provided if pool address is specified
if cfg.TokenCodeObjAddress == (aptos.AccountAddress{}) && cfg.TokenPoolAddress != (aptos.AccountAddress{}) {
errs = append(errs, errors.New("token object address must be provided if token pool address is specified"))
Expand Down Expand Up @@ -116,6 +133,79 @@ func (cs AddTokenPool) VerifyPreconditions(env cldf.Environment, cfg config.AddT
return errors.Join(errs...)
}

// plannedTokenPoolRefs declares the datastore refs Apply will record, mirroring its
// conditionals: a caller-supplied token or pool address means that ref is not deployed here
// and therefore not planned. Every type this changeset records is token-scoped, so all of
// them are multi-instance and qualified by symbol.
func plannedTokenPoolRefs(cfg config.AddTokenPoolConfig) []shared.PlannedRef {
qualifier := cfg.Qualifier().String()
var refs []shared.PlannedRef
if cfg.TokenCodeObjAddress == (aptos.AccountAddress{}) {
refs = append(refs,
shared.PlannedRef{
ChainSelector: cfg.ChainSelector,
Type: shared.AptosManagedTokenType,
Version: Version1_6_0,
Qualifier: qualifier,
MultiInstance: true,
},
shared.PlannedRef{
ChainSelector: cfg.ChainSelector,
Type: cldf.ContractType(cfg.TokenParams.Symbol),
Version: Version1_6_0,
Qualifier: qualifier,
MultiInstance: true,
},
)
}
if cfg.TokenPoolAddress == (aptos.AccountAddress{}) {
refs = append(refs, shared.PlannedRef{
ChainSelector: cfg.ChainSelector,
Type: cfg.PoolType,
Version: Version1_6_0,
Qualifier: qualifier,
MultiInstance: true,
})
}

return refs
}

// verifyTokenSymbol checks the configured token symbol against the symbol of the token the
// pool will serve, so a wrong qualifier cannot reach the datastore. Tokens this repo deployed
// are recorded by symbol in state (ManagedTokens is keyed by symbol); anything else is read on
// chain with the same helper the state loader uses. The comparison uses the raw configured
// symbol the datastore key is derived from it separately, normalized by TokenQualifier.
func verifyTokenSymbol(env cldf.Environment, cfg config.AddTokenPoolConfig, state stateview.CCIPOnChainState) error {
if cfg.TokenParams.Symbol == "" || cfg.TokenAddress == (aptos.AccountAddress{}) {
// Both are reported separately; nothing to compare against.
return nil
}
for symbol, codeObjAddress := range state.AptosChains[cfg.ChainSelector].ManagedTokens {
if codeObjAddress == cfg.TokenCodeObjAddress {
if symbol != cfg.TokenParams.Symbol {
return fmt.Errorf("TokenParams.Symbol %q does not match the recorded symbol %q of token object %s",
cfg.TokenParams.Symbol, symbol, cfg.TokenCodeObjAddress.StringLong())
}
return nil
}
}
aptosChain, ok := env.BlockChains.AptosChains()[cfg.ChainSelector]
if !ok {
// Unsupported chain is reported separately; there is no client to read the token with.
return nil
}
metadata, err := aptosHelpers.GetFungibleAssetMetadata(aptosChain.Client, cfg.TokenAddress)
if err != nil {
return fmt.Errorf("failed to read fungible asset metadata for token %s: %w", cfg.TokenAddress.StringLong(), err)
}
if metadata.Symbol != cfg.TokenParams.Symbol.String() {
return fmt.Errorf("TokenParams.Symbol %q does not match the on-chain symbol %q of token %s",
cfg.TokenParams.Symbol, metadata.Symbol, cfg.TokenAddress.StringLong())
}
return nil
}

func (cs AddTokenPool) Apply(env cldf.Environment, cfg config.AddTokenPoolConfig) (cldf.ChangesetOutput, error) {
state, err := stateview.LoadOnchainState(env)
if err != nil {
Expand All @@ -124,6 +214,7 @@ func (cs AddTokenPool) Apply(env cldf.Environment, cfg config.AddTokenPoolConfig

aptosChain := env.BlockChains.AptosChains()[cfg.ChainSelector]
ab := cldf.NewMemoryAddressBook()
ds := datastore.NewMemoryDataStore()
seqReports := make([]operations.Report[any, any], 0)
proposals := make([]mcms.TimelockProposal, 0)
var mcmsOperations []mcmstypes.BatchOperation
Expand Down Expand Up @@ -167,17 +258,15 @@ func (cs AddTokenPool) Apply(env cldf.Environment, cfg config.AddTokenPoolConfig
tokenAddress = deploySeq.Output.TokenAddress
seqReports = append(seqReports, deploySeq.ExecutionReports...)
mcmsOperations = append(mcmsOperations, deploySeq.Output.MCMSOperations...)
// Save token object address in address book
// Save token object address token-scoped, qualified by symbol
typeAndVersion := cldf.NewTypeAndVersion(shared.AptosManagedTokenType, Version1_6_0)
typeAndVersion.AddLabel(string(cfg.TokenParams.Symbol))
err = deps.AB.Save(deps.AptosChain.Selector, deploySeq.Output.TokenCodeObjAddress.StringLong(), typeAndVersion)
if err != nil {
if err := shared.RecordAddress(deps.AB, ds, cfg.ChainSelector, deploySeq.Output.TokenCodeObjAddress.StringLong(), typeAndVersion, cfg.Qualifier().String()); err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to save token object address %s: %w", deploySeq.Output.TokenCodeObjAddress, err)
}
// Save token address in address book
// Save token address — token-scoped, qualified by symbol
typeAndVersion = cldf.NewTypeAndVersion(cldf.ContractType(cfg.TokenParams.Symbol), Version1_6_0)
err = deps.AB.Save(deps.AptosChain.Selector, deploySeq.Output.TokenAddress.StringLong(), typeAndVersion)
if err != nil {
if err := shared.RecordAddress(deps.AB, ds, cfg.ChainSelector, deploySeq.Output.TokenAddress.StringLong(), typeAndVersion, cfg.Qualifier().String()); err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to save token address %s: %w", deploySeq.Output.TokenAddress, err)
}
}
Expand All @@ -202,11 +291,11 @@ func (cs AddTokenPool) Apply(env cldf.Environment, cfg config.AddTokenPoolConfig
seqReports = append(seqReports, deploySeq.ExecutionReports...)
mcmsOperations = append(mcmsOperations, deploySeq.Output.MCMSOps...)
tokenPoolAddress = deploySeq.Output.TokenPoolAddress
// Save token pool address in address book
// Save token pool address token-scoped, qualified by symbol; the label keeps the
// token address so the state loader can reconstruct the pool without RPC
typeAndVersion := cldf.NewTypeAndVersion(cfg.PoolType, Version1_6_0)
typeAndVersion.AddLabel(tokenAddress.StringLong())
err = deps.AB.Save(deps.AptosChain.Selector, deploySeq.Output.TokenPoolAddress.StringLong(), typeAndVersion)
if err != nil {
if err := shared.RecordAddress(deps.AB, ds, cfg.ChainSelector, deploySeq.Output.TokenPoolAddress.StringLong(), typeAndVersion, cfg.Qualifier().String()); err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to save token pool address %s: %w", deploySeq.Output.TokenPoolAddress, err)
}
}
Expand Down Expand Up @@ -240,11 +329,6 @@ func (cs AddTokenPool) Apply(env cldf.Environment, cfg config.AddTokenPoolConfig
}
proposals = append(proposals, *proposal)

ds, err := shared.PopulateDataStore(ab)
if err != nil {
return cldf.ChangesetOutput{}, fmt.Errorf("failed to populate in-memory DataStore: %w", err)
}

return cldf.ChangesetOutput{
AddressBook: ab,
DataStore: ds,
Expand Down
Loading
Loading