From ff285802373d3629942013b5552c597f7acb3356 Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Tue, 28 Apr 2026 13:26:45 -0700 Subject: [PATCH 01/14] POC impl for token type adaptors to encapsulate token logic --- .gitignore | 2 + STRATEGY_REFACTOR.md | 81 +++++ .../strategy/registrations/registrations.go | 330 ++++++++++++++++++ .../deployment/tokens/strategy/registry.go | 66 ++++ .../deployment/tokens/strategy/strategy.go | 74 ++++ chains/evm/deployment/v1_0_0/adapters/init.go | 3 + .../v1_0_0/adapters/pool_adapter.go | 51 +-- .../evm/deployment/v1_0_0/sequences/token.go | 195 ++--------- .../deployment/v1_0_0/sequences/token_test.go | 14 +- .../evm/deployment/v2_0_0/adapters/tokens.go | 78 +++-- 10 files changed, 647 insertions(+), 247 deletions(-) create mode 100644 STRATEGY_REFACTOR.md create mode 100644 chains/evm/deployment/tokens/strategy/registrations/registrations.go create mode 100644 chains/evm/deployment/tokens/strategy/registry.go create mode 100644 chains/evm/deployment/tokens/strategy/strategy.go diff --git a/.gitignore b/.gitignore index dcfcfbf831..86b3384ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,5 @@ devenv/env-out.toml .cursorrules .claude/ operations-gen +.agents/ +AGENTS.md diff --git a/STRATEGY_REFACTOR.md b/STRATEGY_REFACTOR.md new file mode 100644 index 0000000000..f327d17865 --- /dev/null +++ b/STRATEGY_REFACTOR.md @@ -0,0 +1,81 @@ +# Per-Token-Type Strategy Refactor + +## Objective + +Two related complaints in the EVM token-expansion code: + +1. The token-pool deployment adapter (`chains/evm/deployment/v1_0_0/adapters/pool_adapter.go`) had a switch on token type to decide which role-grant operation to call. This switch grew with each new token type. The same token-type dispatch pattern repeated in three other places — token deployment, capability predicates, and the external-admin role grant. +2. The v2.0 adapter (`chains/evm/deployment/v2_0_0/adapters/tokens.go`) reimplemented the role-grant logic inline rather than reusing v1.0.0 utilities. New v1.6 token types did not flow through to v2.0 automatically. + +The goal: encapsulate everything specific to a token contract type behind one per-token strategy, registered into a registry that is independent of pool version. The four switches collapse to registry lookups, and v2.0 picks up new BurnMint-family token types automatically. + +## Direction + +A `EVMTokenStrategy` interface lives in `chains/evm/deployment/tokens/strategy/`: + +```go +type EVMTokenStrategy interface { + ContractType() deployment.ContractType + Capabilities() Capabilities + + Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) ( + datastore.AddressRef, []evm_contract.WriteOutput, error) + + GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, + token, pool common.Address, chainSelector uint64) ([]evm_contract.WriteOutput, error) + + GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, + token, externalAdmin common.Address, chainSelector uint64) ([]evm_contract.WriteOutput, error) +} +``` + +Strategies are registered into a singleton `Registry` keyed by `ContractType` only — pool version is deliberately excluded. + +All five known EVM token types (plain ERC20, BurnMintERC20, BurnMintERC20WithDrip v1.0.0, BurnMintERC20WithDrip v1.5.0, TIP-20) are wrapped as small strategy structs in one file at `chains/evm/deployment/tokens/strategy/registrations/registrations.go`. Adapters pick them up via a single blank import. + +The four dispatch sites become registry lookups: + +| Site | Now becomes | +|---|---| +| `pool_adapter.go` role-grant switch | `strat.GrantPoolRoles(...)` | +| `sequences/token.go` deploy switch | `strat.Deploy(...)` | +| `sequences/token.go` capability predicates | `strat.Capabilities().Supports*` | +| `sequences/token.go` external-admin switch | `strat.GrantExternalAdmin(...)` | + +The v2.0 adapter's `DeployTokenPoolForToken` keeps its v2-specific pool-type dispatch but its inline role-grant block becomes the same registry lookup as v1.0.0. The previous `isBurnMintTokenType` filter is deleted. + +## Key design decisions + +**Registry key is `(chainFamily, ContractType)`, not pool version.** This is the entire point of the refactor — adding a new BurnMint-family token type to the registry makes it available on v1.5.1, v1.6.x, and v2.0 simultaneously, with one line in `registrations.go`. + +**Capabilities is a struct with four explicit flags, including `ParticipatesInPoolRoleGrant`.** Two Plan agents reviewed the design independently; both converged on the explicit flag rather than inferring non-participation from a no-op `GrantPoolRoles` return. Audit reads the flag, not a side effect. + +**TIP-20 stays v1.6-only via an explicit guard inside `v2_0_0/adapters/tokens.go`.** The strategy registry is version-independent (TIP-20 is registered globally), but the v2.0 adapter rejects TIP-20 early with a clear error message. The v1.6-only constraint is locally checkable in the v2.0 file rather than implicit in pool-type gates elsewhere. + +**Strategies live in one file, ops packages stay untouched.** All five strategy structs are colocated in `registrations/registrations.go`. Ops packages (`burn_mint_erc20`, `tip20`, etc.) are not modified — strategies are thin wrappers that compose existing operations. Adding a new token type means: add an ops package (as today), add a strategy struct, add one Register call. No edits to any pool adapter. + +**Default policies are preserved per call site.** Deploy lookup miss is fail-fast; role-grant lookup miss is warn-and-continue; capability lookup miss returns the zero-value struct (all-false). Three different policies, all matching the prior behavior verbatim. + +**Pool-type dispatch (`isBurnMintPoolType` etc.) is deliberately untouched.** Token type and pool type are orthogonal; conflating them was rejected in design review. + +## What changed + +New (3 files): `chains/evm/deployment/tokens/strategy/{strategy.go, registry.go, registrations/registrations.go}`. + +Edited: +- `chains/evm/deployment/v1_0_0/adapters/pool_adapter.go` — role-grant switch replaced with registry call; unused ops imports removed. +- `chains/evm/deployment/v1_0_0/sequences/token.go` — deploy switch, capability predicates, and external-admin switch all replaced with registry lookups; three `tokenSupports*` predicate funcs deleted. +- `chains/evm/deployment/v2_0_0/adapters/tokens.go` — TIP-20 guard added; role-grant tail replaced with registry call; `isBurnMintTokenType` deleted. +- `chains/evm/deployment/v1_0_0/adapters/init.go` — blank import of `registrations` so strategies are loaded for all transitively-importing adapters. + +`go build ./...` and `go vet ./...` are clean across all submodules of the experiments workspace. + +## What's deferred + +Tests are deferred to a follow-up pass. Once the implementation direction is confirmed, the test pass should add: +- Registry unit tests +- Golden capability-table test asserting every existing token type's capabilities match the pre-refactor `tokenSupports*` truth tables +- TIP-20 v2.0 guard test +- An end-to-end test that pre-registers a fake `ContractType` strategy and exercises both v1.0 and v2.0 dispatch backbones + +The existing token deployment test (`v1_0_0/sequences/token_test.go`) was lightly updated to call the new registry instead of the deleted predicate so the package still compiles. diff --git a/chains/evm/deployment/tokens/strategy/registrations/registrations.go b/chains/evm/deployment/tokens/strategy/registrations/registrations.go new file mode 100644 index 0000000000..ab0a52ab91 --- /dev/null +++ b/chains/evm/deployment/tokens/strategy/registrations/registrations.go @@ -0,0 +1,330 @@ +// Package registrations registers all known EVM token contract strategies +// with the singleton strategy.Registry at init time. Adapters that need +// per-token-type behavior pull in this package via a blank import; all +// known token types become available in one line. +// +// Adding a new EVM token contract type means adding one strategy struct +// and one Register call in init below. No edits to pool adapters or +// deploy sequences are required. +package registrations + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" + drip_v150 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + bnm_erc20_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc20" +) + +func init() { + r := strategy.GetRegistry() + r.RegisterEVM(erc20Strategy{}) + r.RegisterEVM(burnMintERC20Strategy{}) + r.RegisterEVM(burnMintERC20WithDripStrategy{}) + r.RegisterEVM(burnMintERC20WithDripV150Strategy{}) + r.RegisterEVM(tip20Strategy{}) +} + +// ---------- ERC20 (plain, not CCIP-aware) ---------- + +type erc20Strategy struct{} + +func (erc20Strategy) ContractType() deployment.ContractType { return erc20.ContractType } + +func (erc20Strategy) Capabilities() strategy.Capabilities { + return strategy.Capabilities{} +} + +func (erc20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + qualifier := in.Symbol + ref, err := contract.MaybeDeployContract(b, erc20.Deploy, chain, contract.DeployInput[erc20.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(erc20.ContractType, *common_utils.Version_1_0_0), + ChainSelector: chain.Selector, + Args: erc20.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + }, + Qualifier: &qualifier, + }, nil) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy ERC20 token: %w", err) + } + return ref, nil, nil +} + +func (erc20Strategy) GrantPoolRoles(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address, _ uint64) ([]contract.WriteOutput, error) { + return nil, nil +} + +func (erc20Strategy) GrantExternalAdmin(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address, _ uint64) ([]contract.WriteOutput, error) { + return nil, nil +} + +// ---------- BurnMintERC20 (v1.0.0) ---------- + +type burnMintERC20Strategy struct{} + +func (burnMintERC20Strategy) ContractType() deployment.ContractType { + return burn_mint_erc20.ContractType +} + +func (burnMintERC20Strategy) Capabilities() strategy.Capabilities { + return strategy.Capabilities{ + SupportsAdminRole: true, + SupportsCCIPAdmin: true, + SupportsPreMint: true, + ParticipatesInPoolRoleGrant: true, + } +} + +func (burnMintERC20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + qualifier := in.Symbol + maxSupply, preMint := scaledSupplyAndPreMint(in) + ref, err := contract.MaybeDeployContract(b, burn_mint_erc20.Deploy, chain, contract.DeployInput[burn_mint_erc20.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20.ContractType, *common_utils.Version_1_0_0), + ChainSelector: chain.Selector, + Args: burn_mint_erc20.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + Decimals: in.Decimals, + MaxSupply: maxSupply, + PreMint: preMint, + }, + Qualifier: &qualifier, + }, nil) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20 token: %w", err) + } + return ref, nil, nil +} + +func (burnMintERC20Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) +} + +func (burnMintERC20Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) +} + +// ---------- BurnMintERC20WithDrip (v1.0.0) ---------- + +type burnMintERC20WithDripStrategy struct{} + +func (burnMintERC20WithDripStrategy) ContractType() deployment.ContractType { + return burn_mint_erc20_with_drip.ContractType +} + +func (burnMintERC20WithDripStrategy) Capabilities() strategy.Capabilities { + return strategy.Capabilities{ + SupportsAdminRole: true, + SupportsCCIPAdmin: true, + SupportsPreMint: true, + ParticipatesInPoolRoleGrant: true, + } +} + +func (burnMintERC20WithDripStrategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + qualifier := in.Symbol + maxSupply, preMint := scaledSupplyAndPreMint(in) + ref, err := contract.MaybeDeployContract(b, burn_mint_erc20_with_drip.Deploy, chain, contract.DeployInput[burn_mint_erc20_with_drip.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20_with_drip.ContractType, *common_utils.Version_1_0_0), + ChainSelector: chain.Selector, + Args: burn_mint_erc20_with_drip.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + Decimals: in.Decimals, + MaxSupply: maxSupply, + PreMint: preMint, + }, + Qualifier: &qualifier, + }, nil) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20WithDrip token: %w", err) + } + return ref, nil, nil +} + +func (burnMintERC20WithDripStrategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) +} + +func (burnMintERC20WithDripStrategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) +} + +// ---------- BurnMintERC20WithDrip (v1.5.0) ---------- +// +// Pre-mint is unsupported because the v1.5.0 constructor takes neither +// supply nor decimals; matches the historical tokenSupportsPreMint table. + +type burnMintERC20WithDripV150Strategy struct{} + +func (burnMintERC20WithDripV150Strategy) ContractType() deployment.ContractType { + return drip_v150.ContractType +} + +func (burnMintERC20WithDripV150Strategy) Capabilities() strategy.Capabilities { + return strategy.Capabilities{ + SupportsAdminRole: true, + SupportsCCIPAdmin: true, + SupportsPreMint: false, + ParticipatesInPoolRoleGrant: true, + } +} + +func (burnMintERC20WithDripV150Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + qualifier := in.Symbol + ref, err := contract.MaybeDeployContract(b, drip_v150.Deploy, chain, contract.DeployInput[drip_v150.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(drip_v150.ContractType, *drip_v150.Version), + ChainSelector: chain.Selector, + Args: drip_v150.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + }, + Qualifier: &qualifier, + }, nil) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20WithDrip (v1.5.0) token: %w", err) + } + return ref, nil, nil +} + +func (burnMintERC20WithDripV150Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) +} + +func (burnMintERC20WithDripV150Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) +} + +// ---------- TIP-20 ---------- +// +// Tempo-only token; deployed via a factory sequence rather than +// MaybeDeployContract. CCIPAdmin and pre-mint do not apply. + +type tip20Strategy struct{} + +func (tip20Strategy) ContractType() deployment.ContractType { return tip20.ContractType } + +func (tip20Strategy) Capabilities() strategy.Capabilities { + return strategy.Capabilities{ + SupportsAdminRole: true, + SupportsCCIPAdmin: false, + SupportsPreMint: false, + ParticipatesInPoolRoleGrant: true, + } +} + +func (tip20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + // Initial admin must be the deployer so subsequent ops (e.g. GrantIssuerRole) + // pass IsAllowedCaller; ExternalAdmin receives DEFAULT_ADMIN_ROLE in a + // follow-up grant performed by the orchestrating sequence. + report, err := cldf_ops.ExecuteSequence(b, tip20.Deploy, chain, tip20.FactoryDeployArgs{ + QuoteToken: common.Address{}, + Currency: "", + Salt: [32]byte{}, + Symbol: in.Symbol, + Admin: chain.DeployerKey.From, + Name: in.Name, + }) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy TIP20 token via factory: %w", err) + } + if len(report.Output.Addresses) == 0 { + return datastore.AddressRef{}, nil, errors.New("no address returned from TIP20 factory deployment") + } + return report.Output.Addresses[0], nil, nil +} + +func (tip20Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, tip20.GrantIssuerRole, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chainSelector, + Address: token, + Args: pool, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant TIP-20 issuer role: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} + +func (tip20Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, tip20.GrantAdminRole, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chainSelector, + Address: token, + Args: externalAdmin, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant TIP-20 admin role: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} + +// ---------- shared helpers ---------- + +func scaledSupplyAndPreMint(in tokensapi.DeployTokenInput) (*big.Int, *big.Int) { + maxSupply := big.NewInt(0) + if in.Supply != nil { + maxSupply = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.Supply), in.Decimals) + } + preMint := big.NewInt(0) + if in.PreMint != nil { + preMint = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.PreMint), in.Decimals) + } + return maxSupply, preMint +} + +// grantBnMMintAndBurnRoles is shared by all BnM-family strategies. +// Historically the BnM, BnM+Drip (v1.0.0), and BnM+Drip (v1.5.0) types +// all dispatch to burn_mint_erc20.GrantMintAndBurnRoles (the v1.0.0 op); +// preserved here verbatim. +func grantBnMMintAndBurnRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantMintAndBurnRoles, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chainSelector, + Address: token, + Args: pool, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant mint and burn roles: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} + +func grantBnMDefaultAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + tokenContract, err := bnm_erc20_bindings.NewBurnMintERC20(token, chain.Client) + if err != nil { + return nil, fmt.Errorf("failed to instantiate BurnMintERC20 contract: %w", err) + } + role, err := tokenContract.DEFAULTADMINROLE(&bind.CallOpts{Context: b.GetContext()}) + if err != nil { + return nil, fmt.Errorf("failed to get default admin role constant: %w", err) + } + report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantAdminRole, chain, contract.FunctionInput[burn_mint_erc20.RoleAssignment]{ + ChainSelector: chainSelector, + Address: token, + Args: burn_mint_erc20.RoleAssignment{ + Role: role, + To: externalAdmin, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant default admin role: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} diff --git a/chains/evm/deployment/tokens/strategy/registry.go b/chains/evm/deployment/tokens/strategy/registry.go new file mode 100644 index 0000000000..18f9536d50 --- /dev/null +++ b/chains/evm/deployment/tokens/strategy/registry.go @@ -0,0 +1,66 @@ +package strategy + +import ( + "sync" + + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" +) + +// Registry holds the per-token-type strategies for each chain family. +// EVM is the only family populated today; other families would gain +// their own interface and map alongside. +type Registry struct { + mu sync.Mutex + evm map[deployment.ContractType]EVMTokenStrategy +} + +func newRegistry() *Registry { + return &Registry{evm: make(map[deployment.ContractType]EVMTokenStrategy)} +} + +// RegisterEVM registers a strategy for an EVM token contract type. +// First registration wins; subsequent registrations for the same +// ContractType are no-ops, matching the semantics of +// tokens.RegisterTokenAdapter. +func (r *Registry) RegisterEVM(s EVMTokenStrategy) { + if s == nil { + return + } + r.mu.Lock() + defer r.mu.Unlock() + if _, exists := r.evm[s.ContractType()]; !exists { + r.evm[s.ContractType()] = s + } +} + +// GetEVM returns the registered strategy for an EVM token contract type. +// The boolean is false when no strategy is registered. +func (r *Registry) GetEVM(ct deployment.ContractType) (EVMTokenStrategy, bool) { + r.mu.Lock() + defer r.mu.Unlock() + s, ok := r.evm[ct] + return s, ok +} + +// CapabilitiesEVM returns the Capabilities for an EVM token contract +// type, or the zero value if no strategy is registered. The zero value +// preserves the historical "unknown type implies all-false" predicate +// behavior at the call sites. +func (r *Registry) CapabilitiesEVM(ct deployment.ContractType) Capabilities { + if s, ok := r.GetEVM(ct); ok { + return s.Capabilities() + } + return Capabilities{} +} + +var ( + singleton *Registry + once sync.Once +) + +// GetRegistry returns the global singleton registry. The first call +// constructs the registry; subsequent calls return the same pointer. +func GetRegistry() *Registry { + once.Do(func() { singleton = newRegistry() }) + return singleton +} diff --git a/chains/evm/deployment/tokens/strategy/strategy.go b/chains/evm/deployment/tokens/strategy/strategy.go new file mode 100644 index 0000000000..32cf8fdba1 --- /dev/null +++ b/chains/evm/deployment/tokens/strategy/strategy.go @@ -0,0 +1,74 @@ +// Package strategy provides per-token-contract-type behaviors for EVM +// token deployment and pool wiring. A strategy encapsulates everything +// specific to one token contract type (e.g. BurnMintERC20, TIP-20): +// how to deploy the token, how to grant pool roles after a pool is +// deployed, how to grant an external admin role, and which capabilities +// the token contract supports. +// +// Strategies are looked up by ContractType from the singleton Registry +// and are independent of pool version, so adding a new token type makes +// it available to every pool-version adapter that consults the registry. +package strategy + +import ( + "github.com/ethereum/go-ethereum/common" + + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + evm_contract "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// Capabilities reports the optional flow steps a token contract type +// participates in. The orchestrating sequence reads these flags to +// decide whether to invoke the corresponding step. +// +// ParticipatesInPoolRoleGrant is declared explicitly rather than inferred +// from a no-op GrantPoolRoles return so that intentional non-participation +// (e.g. plain ERC20) is distinguishable from a strategy bug that returned +// no writes by accident. +type Capabilities struct { + SupportsAdminRole bool + SupportsCCIPAdmin bool + SupportsPreMint bool + ParticipatesInPoolRoleGrant bool +} + +// EVMTokenStrategy encapsulates everything specific to one EVM token +// contract type. Implementations are registered with the singleton +// Registry keyed by ContractType. +type EVMTokenStrategy interface { + // ContractType returns the deployment.ContractType used as the + // registry key. + ContractType() deployment.ContractType + + // Capabilities returns the static feature flags for this token type. + Capabilities() Capabilities + + // Deploy performs the token contract deployment, returning the + // resulting datastore reference and any token-side write outputs + // produced during deployment. Implementations wrap either an + // Operation (via contract.MaybeDeployContract) or a Sequence + // (via cldf_ops.ExecuteSequence) as appropriate for the token type. + Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) ( + datastore.AddressRef, []evm_contract.WriteOutput, error) + + // GrantPoolRoles emits the writes that authorize a freshly-deployed + // pool to mint/burn (or its TIP-20 issuer-role equivalent) against + // this token. Returns (nil, nil) for token types that do not + // participate in pool role granting; ParticipatesInPoolRoleGrant + // is the authoritative flag, callers should consult it first. + GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, + token, pool common.Address, chainSelector uint64) ( + []evm_contract.WriteOutput, error) + + // GrantExternalAdmin grants the default-admin or contract-specific + // admin role to externalAdmin. Implementations return (nil, nil) + // for token types whose Capabilities.SupportsAdminRole is false; + // callers should consult that flag first. + GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, + token, externalAdmin common.Address, chainSelector uint64) ( + []evm_contract.WriteOutput, error) +} diff --git a/chains/evm/deployment/v1_0_0/adapters/init.go b/chains/evm/deployment/v1_0_0/adapters/init.go index 1315631983..e5acdaaf78 100644 --- a/chains/evm/deployment/v1_0_0/adapters/init.go +++ b/chains/evm/deployment/v1_0_0/adapters/init.go @@ -4,6 +4,9 @@ import ( "github.com/Masterminds/semver/v3" chain_selectors "github.com/smartcontractkit/chain-selectors" + // Pull in EVM token-contract strategies so the per-token-type registry is + // populated before any consumer of strategy.GetRegistry runs. + _ "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy/registrations" deployapi "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" mcmsreaderapi "github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets" diff --git a/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go b/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go index 6848ea0dec..d0d2257bba 100644 --- a/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go +++ b/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go @@ -8,11 +8,8 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" datastore_utils_evm "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/datastore" - bnmERC20ops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" - bnmDripERC20ops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" - tip20ops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" - bnmDripOps150 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" tarops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/token_admin_registry" tarseq "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/sequences" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" @@ -345,42 +342,22 @@ func (a *EVMPoolAdapter) DeployTokenPoolForToken() *cldf_ops.Sequence[tokensapi. return sequences.OnChainOutput{}, fmt.Errorf("token address for symbol %q is zero address", input.TokenRef.Qualifier) } - writes := []evm_contract.WriteOutput{} - switch toknRef.Type.String() { - case bnmDripERC20ops.ContractType.String(), bnmERC20ops.ContractType.String(), bnmDripOps150.ContractType.String(): - report, execErr := cldf_ops.ExecuteOperation(b, - bnmERC20ops.GrantMintAndBurnRoles, chain, - evm_contract.FunctionInput[common.Address]{ - ChainSelector: input.ChainSelector, - Address: toknAddr, - Args: poolAddr, - }, - ) - if execErr != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to grant mint and burn roles to token pool %q for token %q on chain %d: %w", poolAddr.Hex(), input.TokenRef.Qualifier, input.ChainSelector, execErr) - } - writes = append(writes, report.Output) - - case tip20ops.ContractType.String(): - report, execErr := cldf_ops.ExecuteOperation(b, - tip20ops.GrantIssuerRole, chain, - evm_contract.FunctionInput[common.Address]{ - ChainSelector: input.ChainSelector, - Address: toknAddr, - Args: poolAddr, - }, - ) - if execErr != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to grant TIP-20 issuer role to token pool %q for token %q on chain %d: %w", poolAddr.Hex(), input.TokenRef.Qualifier, input.ChainSelector, execErr) - } - writes = append(writes, report.Output) - - default: - // pass through for unknown token types since we don't want to block pool deployment, but log a warning since it likely indicates a missing case in the adapter + var writes []evm_contract.WriteOutput + strat, ok := strategy.GetRegistry().GetEVM(deployment.ContractType(toknRef.Type)) + if !ok || !strat.Capabilities().ParticipatesInPoolRoleGrant { + // Pass through for unknown / non-participating token types; we don't want + // to block pool deployment, but log a warning since an unknown type likely + // indicates a missing strategy registration. b.Logger.Warnf( - "token type %q does not have a defined role granting strategy in EVMPoolAdapter, skipping grant of mint and burn roles to token pool %q for token %q on chain %d", + "token type %q has no pool role grant strategy registered, skipping grant for token pool %q on token %q on chain %d", toknRef.Type.String(), poolAddr.Hex(), input.TokenRef.Qualifier, input.ChainSelector, ) + } else { + grantWrites, grantErr := strat.GrantPoolRoles(b, chain, toknAddr, poolAddr, input.ChainSelector) + if grantErr != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to grant pool roles for token type %q (token %q, pool %q) on chain %d: %w", toknRef.Type, input.TokenRef.Qualifier, poolAddr.Hex(), input.ChainSelector, grantErr) + } + writes = append(writes, grantWrites...) } if len(writes) > 0 { diff --git a/chains/evm/deployment/v1_0_0/sequences/token.go b/chains/evm/deployment/v1_0_0/sequences/token.go index ea45a9b914..3b0061977a 100644 --- a/chains/evm/deployment/v1_0_0/sequences/token.go +++ b/chains/evm/deployment/v1_0_0/sequences/token.go @@ -5,76 +5,32 @@ import ( "fmt" "math/big" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" mcms_types "github.com/smartcontractkit/mcms/types" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" + // Defensive blank import: ensures all known EVM token strategies are + // registered when this package is imported directly (e.g. by tests) + // rather than transitively via an adapter package. + _ "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy/registrations" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" - drip_v150 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" tokenapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" - bnm_erc20_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc20" ) -func tokenSupportsAdminRole(tokenType deployment.ContractType) bool { - switch tokenType { - case burn_mint_erc20.ContractType, - burn_mint_erc20_with_drip.ContractType, - drip_v150.ContractType, - tip20.ContractType: - return true - default: - return false - } -} - -func tokenSupportsCCIPAdmin(tokenType deployment.ContractType) bool { - switch tokenType { - case burn_mint_erc20.ContractType, - burn_mint_erc20_with_drip.ContractType, - drip_v150.ContractType: - return true - default: - return false - } -} - -func tokenSupportsPreMint(tokenType deployment.ContractType) bool { - switch tokenType { - // drip_v150 has no supply/decimals in its constructor so pre-mint is not supported - case burn_mint_erc20.ContractType, burn_mint_erc20_with_drip.ContractType: - return true - default: - return false - } -} - var DeployToken = cldf_ops.NewSequence( "deploy-token", common_utils.Version_1_0_0, "Deploy given type of token contracts", func(b cldf_ops.Bundle, chains cldf_chain.BlockChains, input tokenapi.DeployTokenInput) (sequences.OnChainOutput, error) { - addresses := make([]datastore.AddressRef, 0) - writes := make([]contract.WriteOutput, 0) chain := chains.EVMChains()[input.ChainSelector] - var err error - var tokenRef datastore.AddressRef - qualifier := input.Symbol - - maxSupply := big.NewInt(0) - if input.Supply != nil { - maxSupply = tokenapi.ScaleTokenAmount(new(big.Int).SetUint64(*input.Supply), input.Decimals) - } preMint := big.NewInt(0) if input.PreMint != nil { @@ -95,96 +51,22 @@ var DeployToken = cldf_ops.NewSequence( ccipAdmin = common.HexToAddress(input.CCIPAdmin) } - switch input.Type { - case erc20.ContractType: - tokenRef, err = contract.MaybeDeployContract(b, erc20.Deploy, chain, contract.DeployInput[erc20.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(erc20.ContractType, *common_utils.Version_1_0_0), - ChainSelector: chain.Selector, - Args: erc20.ConstructorArgs{ - Name: input.Name, - Symbol: input.Symbol, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to deploy ERC20 token: %w", err) - } - - case burn_mint_erc20.ContractType: - tokenRef, err = contract.MaybeDeployContract(b, burn_mint_erc20.Deploy, chain, contract.DeployInput[burn_mint_erc20.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20.ContractType, *common_utils.Version_1_0_0), - ChainSelector: chain.Selector, - Args: burn_mint_erc20.ConstructorArgs{ - Name: input.Name, - Symbol: input.Symbol, - Decimals: input.Decimals, - MaxSupply: maxSupply, - PreMint: preMint, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to deploy BurnMintERC20 token: %w", err) - } - - case burn_mint_erc20_with_drip.ContractType: - tokenRef, err = contract.MaybeDeployContract(b, burn_mint_erc20_with_drip.Deploy, chain, contract.DeployInput[burn_mint_erc20_with_drip.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20_with_drip.ContractType, *common_utils.Version_1_0_0), - ChainSelector: chain.Selector, - Args: burn_mint_erc20_with_drip.ConstructorArgs{ - Name: input.Name, - Symbol: input.Symbol, - Decimals: input.Decimals, - MaxSupply: maxSupply, - PreMint: preMint, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to deploy BurnMintERC20WithDrip token: %w", err) - } - - case drip_v150.ContractType: - tokenRef, err = contract.MaybeDeployContract(b, drip_v150.Deploy, chain, contract.DeployInput[drip_v150.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(drip_v150.ContractType, *drip_v150.Version), - ChainSelector: chain.Selector, - Args: drip_v150.ConstructorArgs{ - Name: input.Name, - Symbol: input.Symbol, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to deploy BurnMintERC20WithDrip (v1.5.0) token: %w", err) - } - - case tip20.ContractType: - // Initial admin must be the deployer so subsequent ops (e.g. GrantIssuerRole) run as the same - // identity pass IsAllowedCaller; ExternalAdmin receives DEFAULT_ADMIN_ROLE in a follow-up grant. - report, err := cldf_ops.ExecuteSequence(b, tip20.Deploy, chain, tip20.FactoryDeployArgs{ - QuoteToken: common.Address{}, // defaults to sensible value - Currency: "", // defaults to sensible value - Salt: [32]byte{}, // defaults to random salt - Symbol: input.Symbol, - Admin: chain.DeployerKey.From, - Name: input.Name, - }) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to deploy TIP20 token via factory: %w", err) - } - if len(report.Output.Addresses) == 0 { - return sequences.OnChainOutput{}, errors.New("no address returned from TIP20 factory deployment") - } - tokenRef = report.Output.Addresses[0] - - default: + strat, ok := strategy.GetRegistry().GetEVM(input.Type) + if !ok { return sequences.OnChainOutput{}, fmt.Errorf("unsupported token type: %s", input.Type) } + caps := strat.Capabilities() + + tokenRef, deployWrites, err := strat.Deploy(b, chain, input) + if err != nil { + return sequences.OnChainOutput{}, err + } + addresses := []datastore.AddressRef{tokenRef} + writes := append([]contract.WriteOutput(nil), deployWrites...) tokenAddr := common.HexToAddress(tokenRef.Address) - addresses = append(addresses, tokenRef) - if tokenSupportsPreMint(input.Type) && preMint.Cmp(big.NewInt(0)) > 0 && len(input.Senders) > 0 { + if caps.SupportsPreMint && preMint.Cmp(big.NewInt(0)) > 0 && len(input.Senders) > 0 { firstSender := input.Senders[0] if !common.IsHexAddress(firstSender) { return sequences.OnChainOutput{}, fmt.Errorf("invalid sender address: %s", firstSender) @@ -210,7 +92,7 @@ var DeployToken = cldf_ops.NewSequence( writes = append(writes, transferReport.Output) } - if input.CCIPAdmin != "" && tokenSupportsCCIPAdmin(input.Type) { + if input.CCIPAdmin != "" && caps.SupportsCCIPAdmin { setCCIPAdminReport, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.SetCCIPAdmin, chain, contract.FunctionInput[string]{ ChainSelector: chain.Selector, Address: tokenAddr, @@ -222,45 +104,12 @@ var DeployToken = cldf_ops.NewSequence( writes = append(writes, setCCIPAdminReport.Output) } - if input.ExternalAdmin != "" && tokenSupportsAdminRole(input.Type) { - switch input.Type { - case burn_mint_erc20.ContractType, burn_mint_erc20_with_drip.ContractType, drip_v150.ContractType: - token, err := bnm_erc20_bindings.NewBurnMintERC20(tokenAddr, chain.Client) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to instantiate BurnMintERC20 contract: %w", err) - } - role, err := token.DEFAULTADMINROLE(&bind.CallOpts{Context: b.GetContext()}) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to get default admin role constant: %w", err) - } - - grantReport, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantAdminRole, chain, contract.FunctionInput[burn_mint_erc20.RoleAssignment]{ - ChainSelector: chain.Selector, - Address: tokenAddr, - Args: burn_mint_erc20.RoleAssignment{ - Role: role, - To: externalAdmin, - }, - }) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to grant admin role to %s: %w", input.ExternalAdmin, err) - } - writes = append(writes, grantReport.Output) - - case tip20.ContractType: - grantReport, err := cldf_ops.ExecuteOperation(b, tip20.GrantAdminRole, chain, contract.FunctionInput[common.Address]{ - ChainSelector: chain.Selector, - Address: tokenAddr, - Args: externalAdmin, - }) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to grant admin role to %s: %w", input.ExternalAdmin, err) - } - writes = append(writes, grantReport.Output) - - default: - return sequences.OnChainOutput{}, fmt.Errorf("unsupported token type for admin role grant: %s", input.Type) + if input.ExternalAdmin != "" && caps.SupportsAdminRole { + adminWrites, err := strat.GrantExternalAdmin(b, chain, tokenAddr, externalAdmin, chain.Selector) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to grant admin role to %s: %w", input.ExternalAdmin, err) } + writes = append(writes, adminWrites...) } batchOp, err := contract.NewBatchOperationFromWrites(writes) diff --git a/chains/evm/deployment/v1_0_0/sequences/token_test.go b/chains/evm/deployment/v1_0_0/sequences/token_test.go index 038256aa80..7d72f0ea39 100644 --- a/chains/evm/deployment/v1_0_0/sequences/token_test.go +++ b/chains/evm/deployment/v1_0_0/sequences/token_test.go @@ -14,6 +14,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" @@ -196,7 +197,7 @@ func TestEVMTokenDeployments(t *testing.T) { } } - tokenSupportsAdmin := tokenSupportsAdminRole(tc.tokenType) + tokenSupportsAdmin := strategy.GetRegistry().CapabilitiesEVM(tc.tokenType).SupportsAdminRole if tokenSupportsAdmin { // Verify CCIP Admin was set correctly t.Log(" Verifying CCIP Admin...") @@ -235,8 +236,11 @@ func TestEVMTokenDeployments(t *testing.T) { func TestTokenSupportsAdminRole(t *testing.T) { t.Parallel() - require.True(t, tokenSupportsAdminRole(burn_mint_erc20.ContractType)) - require.True(t, tokenSupportsAdminRole(burn_mint_erc20_with_drip.ContractType)) - require.True(t, tokenSupportsAdminRole(tip20.ContractType)) - require.False(t, tokenSupportsAdminRole(erc20.ContractType)) + caps := func(ct cldf.ContractType) bool { + return strategy.GetRegistry().CapabilitiesEVM(ct).SupportsAdminRole + } + require.True(t, caps(burn_mint_erc20.ContractType)) + require.True(t, caps(burn_mint_erc20_with_drip.ContractType)) + require.True(t, caps(tip20.ContractType)) + require.False(t, caps(erc20.ContractType)) } diff --git a/chains/evm/deployment/v2_0_0/adapters/tokens.go b/chains/evm/deployment/v2_0_0/adapters/tokens.go index a683663e88..f8c0c9b36a 100644 --- a/chains/evm/deployment/v2_0_0/adapters/tokens.go +++ b/chains/evm/deployment/v2_0_0/adapters/tokens.go @@ -11,17 +11,16 @@ import ( "github.com/ethereum/go-ethereum/common" mcms_types "github.com/smartcontractkit/mcms/types" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" evm1_0_0 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/adapters" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/siloed_lock_release_token_pool" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/token_pool" evm_tokens "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/sequences/tokens" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" - bnmOps "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" - bnmDripOps "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" rmnproxyops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/rmn_proxy" + tip20ops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router" - bnmDripOps150 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" "github.com/smartcontractkit/chainlink-ccip/deployment/finality" "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" @@ -130,6 +129,29 @@ func (t *TokenAdapter) DeployTokenPoolForToken() *cldf_ops.Sequence[tokens.Deplo return sequences.OnChainOutput{}, nil } + // Defensive: TIP-20 tokens are v1.6-only by product decision; reject before pool deploy + // even when the pool-type gate would otherwise allow it. Resolve the token type via the + // caller-supplied input first, then fall back to a datastore probe by address. If neither + // resolves, log a warning so a potential bypass is visible rather than silent. + guardType := datastore.ContractType("") + if input.TokenRef != nil { + guardType = input.TokenRef.Type + } + if guardType == "" { + if typeProbe, probeErr := datastore_utils.FindAndFormatRef(input.ExistingDataStore, datastore.AddressRef{ + ChainSelector: input.ChainSelector, + Address: tokenAddr, + }, input.ChainSelector, datastore_utils.FullRef); probeErr == nil { + guardType = typeProbe.Type + } else { + b.Logger.Warnf("TIP-20 v2.0 guard could not resolve token type for address %s on chain %d (probe: %v); skipping guard", + tokenAddr, input.ChainSelector, probeErr) + } + } + if string(guardType) == string(tip20ops.ContractType) { + return sequences.OnChainOutput{}, fmt.Errorf("TIP-20 tokens are not supported on CCIP v2.0 (v1.6-only by product decision)") + } + tokenContract, err := erc20.NewERC20(common.HexToAddress(tokenAddr), evmChain.Client) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to bind ERC20 at %s: %w", tokenAddr, err) @@ -208,30 +230,28 @@ func (t *TokenAdapter) DeployTokenPoolForToken() *cldf_ops.Sequence[tokens.Deplo ChainSelector: input.ChainSelector, Address: tokenAddr, }, input.ChainSelector, datastore_utils.FullRef) - if lookupErr == nil && isBurnMintTokenType(toknRef.Type) { - poolRef := deployOutput.Addresses[0] - poolAddr := common.HexToAddress(poolRef.Address) - if poolAddr == (common.Address{}) { - return sequences.OnChainOutput{}, errors.New("deployed token pool address is zero") + if lookupErr == nil { + strat, ok := strategy.GetRegistry().GetEVM(deployment.ContractType(toknRef.Type)) + if ok && strat.Capabilities().ParticipatesInPoolRoleGrant { + poolRef := deployOutput.Addresses[0] + poolAddr := common.HexToAddress(poolRef.Address) + if poolAddr == (common.Address{}) { + return sequences.OnChainOutput{}, errors.New("deployed token pool address is zero") + } + + grantWrites, grantErr := strat.GrantPoolRoles(b, evmChain, common.HexToAddress(tokenAddr), poolAddr, input.ChainSelector) + if grantErr != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to grant pool roles for token type %q (token %s, pool %s) on chain %d: %w", toknRef.Type, tokenAddr, poolAddr, input.ChainSelector, grantErr) + } + + if len(grantWrites) > 0 { + batchOp, bErr := contract.NewBatchOperationFromWrites(grantWrites) + if bErr != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to create batch operation for role grants: %w", bErr) + } + result.BatchOps = append(result.BatchOps, batchOp) + } } - - grantReport, grantErr := cldf_ops.ExecuteOperation(b, - bnmOps.GrantMintAndBurnRoles, evmChain, - contract.FunctionInput[common.Address]{ - ChainSelector: input.ChainSelector, - Address: common.HexToAddress(tokenAddr), - Args: poolAddr, - }, - ) - if grantErr != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to grant mint/burn roles to pool %s for token %s: %w", poolAddr, tokenAddr, grantErr) - } - - batchOp, bErr := contract.NewBatchOperationFromWrites([]contract.WriteOutput{grantReport.Output}) - if bErr != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to create batch operation for role grants: %w", bErr) - } - result.BatchOps = append(result.BatchOps, batchOp) } } @@ -467,9 +487,3 @@ func isLockReleasePoolType(poolType deployment.ContractType) bool { return poolType == cciputils.LockReleaseTokenPool || poolType == siloed_lock_release_token_pool.ContractType } - -func isBurnMintTokenType(typ datastore.ContractType) bool { - return typ.String() == bnmOps.ContractType.String() || - typ.String() == bnmDripOps.ContractType.String() || - typ.String() == bnmDripOps150.ContractType.String() -} From 7093ed04538f464df7ebf8f6625f23de40dd2339 Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Tue, 28 Apr 2026 15:57:10 -0700 Subject: [PATCH 02/14] break up registration files --- .../strategy/registrations/burn_mint_erc20.go | 65 ++++ .../burn_mint_erc20_with_drip.go | 65 ++++ .../burn_mint_erc20_with_drip_v150.go | 63 ++++ .../tokens/strategy/registrations/doc.go | 9 + .../tokens/strategy/registrations/erc20.go | 55 +++ .../tokens/strategy/registrations/helpers.go | 67 ++++ .../strategy/registrations/registrations.go | 330 ------------------ .../tokens/strategy/registrations/tip20.go | 82 +++++ 8 files changed, 406 insertions(+), 330 deletions(-) create mode 100644 chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20.go create mode 100644 chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip.go create mode 100644 chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip_v150.go create mode 100644 chains/evm/deployment/tokens/strategy/registrations/doc.go create mode 100644 chains/evm/deployment/tokens/strategy/registrations/erc20.go create mode 100644 chains/evm/deployment/tokens/strategy/registrations/helpers.go delete mode 100644 chains/evm/deployment/tokens/strategy/registrations/registrations.go create mode 100644 chains/evm/deployment/tokens/strategy/registrations/tip20.go diff --git a/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20.go b/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20.go new file mode 100644 index 0000000000..a62cad3def --- /dev/null +++ b/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20.go @@ -0,0 +1,65 @@ +package registrations + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +func init() { + strategy.GetRegistry().RegisterEVM(burnMintERC20Strategy{}) +} + +type burnMintERC20Strategy struct{} + +func (burnMintERC20Strategy) ContractType() deployment.ContractType { + return burn_mint_erc20.ContractType +} + +func (burnMintERC20Strategy) Capabilities() strategy.Capabilities { + return strategy.Capabilities{ + SupportsAdminRole: true, + SupportsCCIPAdmin: true, + SupportsPreMint: true, + ParticipatesInPoolRoleGrant: true, + } +} + +func (burnMintERC20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + qualifier := in.Symbol + maxSupply, preMint := scaledSupplyAndPreMint(in) + ref, err := contract.MaybeDeployContract(b, burn_mint_erc20.Deploy, chain, contract.DeployInput[burn_mint_erc20.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20.ContractType, *common_utils.Version_1_0_0), + ChainSelector: chain.Selector, + Args: burn_mint_erc20.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + Decimals: in.Decimals, + MaxSupply: maxSupply, + PreMint: preMint, + }, + Qualifier: &qualifier, + }, nil) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20 token: %w", err) + } + return ref, nil, nil +} + +func (burnMintERC20Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) +} + +func (burnMintERC20Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) +} diff --git a/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip.go b/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip.go new file mode 100644 index 0000000000..bad8bf1946 --- /dev/null +++ b/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip.go @@ -0,0 +1,65 @@ +package registrations + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +func init() { + strategy.GetRegistry().RegisterEVM(burnMintERC20WithDripStrategy{}) +} + +type burnMintERC20WithDripStrategy struct{} + +func (burnMintERC20WithDripStrategy) ContractType() deployment.ContractType { + return burn_mint_erc20_with_drip.ContractType +} + +func (burnMintERC20WithDripStrategy) Capabilities() strategy.Capabilities { + return strategy.Capabilities{ + SupportsAdminRole: true, + SupportsCCIPAdmin: true, + SupportsPreMint: true, + ParticipatesInPoolRoleGrant: true, + } +} + +func (burnMintERC20WithDripStrategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + qualifier := in.Symbol + maxSupply, preMint := scaledSupplyAndPreMint(in) + ref, err := contract.MaybeDeployContract(b, burn_mint_erc20_with_drip.Deploy, chain, contract.DeployInput[burn_mint_erc20_with_drip.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20_with_drip.ContractType, *common_utils.Version_1_0_0), + ChainSelector: chain.Selector, + Args: burn_mint_erc20_with_drip.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + Decimals: in.Decimals, + MaxSupply: maxSupply, + PreMint: preMint, + }, + Qualifier: &qualifier, + }, nil) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20WithDrip token: %w", err) + } + return ref, nil, nil +} + +func (burnMintERC20WithDripStrategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) +} + +func (burnMintERC20WithDripStrategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) +} diff --git a/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip_v150.go b/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip_v150.go new file mode 100644 index 0000000000..3c8e6d7962 --- /dev/null +++ b/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip_v150.go @@ -0,0 +1,63 @@ +package registrations + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" + drip_v150 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +func init() { + strategy.GetRegistry().RegisterEVM(burnMintERC20WithDripV150Strategy{}) +} + +// burnMintERC20WithDripV150Strategy is the v1.5.0 variant of BurnMintERC20WithDrip. +// Pre-mint is unsupported because the v1.5.0 constructor takes neither +// supply nor decimals; matches the historical tokenSupportsPreMint table. +type burnMintERC20WithDripV150Strategy struct{} + +func (burnMintERC20WithDripV150Strategy) ContractType() deployment.ContractType { + return drip_v150.ContractType +} + +func (burnMintERC20WithDripV150Strategy) Capabilities() strategy.Capabilities { + return strategy.Capabilities{ + SupportsAdminRole: true, + SupportsCCIPAdmin: true, + SupportsPreMint: false, + ParticipatesInPoolRoleGrant: true, + } +} + +func (burnMintERC20WithDripV150Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + qualifier := in.Symbol + ref, err := contract.MaybeDeployContract(b, drip_v150.Deploy, chain, contract.DeployInput[drip_v150.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(drip_v150.ContractType, *drip_v150.Version), + ChainSelector: chain.Selector, + Args: drip_v150.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + }, + Qualifier: &qualifier, + }, nil) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20WithDrip (v1.5.0) token: %w", err) + } + return ref, nil, nil +} + +func (burnMintERC20WithDripV150Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) +} + +func (burnMintERC20WithDripV150Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) +} diff --git a/chains/evm/deployment/tokens/strategy/registrations/doc.go b/chains/evm/deployment/tokens/strategy/registrations/doc.go new file mode 100644 index 0000000000..684467e446 --- /dev/null +++ b/chains/evm/deployment/tokens/strategy/registrations/doc.go @@ -0,0 +1,9 @@ +// Package registrations registers all known EVM token contract strategies +// with the singleton strategy.Registry at init time. Adapters that need +// per-token-type behavior pull in this package via a blank import; all +// known token types become available in one line. +// +// Convention: each strategy lives in its own file with its own init() that +// registers itself. Adding a new EVM token contract type means dropping in +// one new file (strategy struct + init); no central registry list to amend. +package registrations diff --git a/chains/evm/deployment/tokens/strategy/registrations/erc20.go b/chains/evm/deployment/tokens/strategy/registrations/erc20.go new file mode 100644 index 0000000000..d6d2a85266 --- /dev/null +++ b/chains/evm/deployment/tokens/strategy/registrations/erc20.go @@ -0,0 +1,55 @@ +package registrations + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +func init() { + strategy.GetRegistry().RegisterEVM(erc20Strategy{}) +} + +// erc20Strategy is the plain (non-CCIP-aware) ERC20 strategy. +type erc20Strategy struct{} + +func (erc20Strategy) ContractType() deployment.ContractType { return erc20.ContractType } + +func (erc20Strategy) Capabilities() strategy.Capabilities { + return strategy.Capabilities{} +} + +func (erc20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + qualifier := in.Symbol + ref, err := contract.MaybeDeployContract(b, erc20.Deploy, chain, contract.DeployInput[erc20.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(erc20.ContractType, *common_utils.Version_1_0_0), + ChainSelector: chain.Selector, + Args: erc20.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + }, + Qualifier: &qualifier, + }, nil) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy ERC20 token: %w", err) + } + return ref, nil, nil +} + +func (erc20Strategy) GrantPoolRoles(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address, _ uint64) ([]contract.WriteOutput, error) { + return nil, nil +} + +func (erc20Strategy) GrantExternalAdmin(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address, _ uint64) ([]contract.WriteOutput, error) { + return nil, nil +} diff --git a/chains/evm/deployment/tokens/strategy/registrations/helpers.go b/chains/evm/deployment/tokens/strategy/registrations/helpers.go new file mode 100644 index 0000000000..a0d345d74d --- /dev/null +++ b/chains/evm/deployment/tokens/strategy/registrations/helpers.go @@ -0,0 +1,67 @@ +package registrations + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + bnm_erc20_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc20" +) + +func scaledSupplyAndPreMint(in tokensapi.DeployTokenInput) (*big.Int, *big.Int) { + maxSupply := big.NewInt(0) + if in.Supply != nil { + maxSupply = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.Supply), in.Decimals) + } + preMint := big.NewInt(0) + if in.PreMint != nil { + preMint = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.PreMint), in.Decimals) + } + return maxSupply, preMint +} + +// grantBnMMintAndBurnRoles is shared by all BnM-family strategies. +// Historically the BnM, BnM+Drip (v1.0.0), and BnM+Drip (v1.5.0) types +// all dispatch to burn_mint_erc20.GrantMintAndBurnRoles (the v1.0.0 op); +// preserved here verbatim. +func grantBnMMintAndBurnRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantMintAndBurnRoles, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chainSelector, + Address: token, + Args: pool, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant mint and burn roles: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} + +func grantBnMDefaultAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + tokenContract, err := bnm_erc20_bindings.NewBurnMintERC20(token, chain.Client) + if err != nil { + return nil, fmt.Errorf("failed to instantiate BurnMintERC20 contract: %w", err) + } + role, err := tokenContract.DEFAULTADMINROLE(&bind.CallOpts{Context: b.GetContext()}) + if err != nil { + return nil, fmt.Errorf("failed to get default admin role constant: %w", err) + } + report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantAdminRole, chain, contract.FunctionInput[burn_mint_erc20.RoleAssignment]{ + ChainSelector: chainSelector, + Address: token, + Args: burn_mint_erc20.RoleAssignment{ + Role: role, + To: externalAdmin, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant default admin role: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} diff --git a/chains/evm/deployment/tokens/strategy/registrations/registrations.go b/chains/evm/deployment/tokens/strategy/registrations/registrations.go deleted file mode 100644 index ab0a52ab91..0000000000 --- a/chains/evm/deployment/tokens/strategy/registrations/registrations.go +++ /dev/null @@ -1,330 +0,0 @@ -// Package registrations registers all known EVM token contract strategies -// with the singleton strategy.Registry at init time. Adapters that need -// per-token-type behavior pull in this package via a blank import; all -// known token types become available in one line. -// -// Adding a new EVM token contract type means adding one strategy struct -// and one Register call in init below. No edits to pool adapters or -// deploy sequences are required. -package registrations - -import ( - "errors" - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" - drip_v150 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" - tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" - bnm_erc20_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc20" -) - -func init() { - r := strategy.GetRegistry() - r.RegisterEVM(erc20Strategy{}) - r.RegisterEVM(burnMintERC20Strategy{}) - r.RegisterEVM(burnMintERC20WithDripStrategy{}) - r.RegisterEVM(burnMintERC20WithDripV150Strategy{}) - r.RegisterEVM(tip20Strategy{}) -} - -// ---------- ERC20 (plain, not CCIP-aware) ---------- - -type erc20Strategy struct{} - -func (erc20Strategy) ContractType() deployment.ContractType { return erc20.ContractType } - -func (erc20Strategy) Capabilities() strategy.Capabilities { - return strategy.Capabilities{} -} - -func (erc20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - qualifier := in.Symbol - ref, err := contract.MaybeDeployContract(b, erc20.Deploy, chain, contract.DeployInput[erc20.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(erc20.ContractType, *common_utils.Version_1_0_0), - ChainSelector: chain.Selector, - Args: erc20.ConstructorArgs{ - Name: in.Name, - Symbol: in.Symbol, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy ERC20 token: %w", err) - } - return ref, nil, nil -} - -func (erc20Strategy) GrantPoolRoles(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address, _ uint64) ([]contract.WriteOutput, error) { - return nil, nil -} - -func (erc20Strategy) GrantExternalAdmin(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address, _ uint64) ([]contract.WriteOutput, error) { - return nil, nil -} - -// ---------- BurnMintERC20 (v1.0.0) ---------- - -type burnMintERC20Strategy struct{} - -func (burnMintERC20Strategy) ContractType() deployment.ContractType { - return burn_mint_erc20.ContractType -} - -func (burnMintERC20Strategy) Capabilities() strategy.Capabilities { - return strategy.Capabilities{ - SupportsAdminRole: true, - SupportsCCIPAdmin: true, - SupportsPreMint: true, - ParticipatesInPoolRoleGrant: true, - } -} - -func (burnMintERC20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - qualifier := in.Symbol - maxSupply, preMint := scaledSupplyAndPreMint(in) - ref, err := contract.MaybeDeployContract(b, burn_mint_erc20.Deploy, chain, contract.DeployInput[burn_mint_erc20.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20.ContractType, *common_utils.Version_1_0_0), - ChainSelector: chain.Selector, - Args: burn_mint_erc20.ConstructorArgs{ - Name: in.Name, - Symbol: in.Symbol, - Decimals: in.Decimals, - MaxSupply: maxSupply, - PreMint: preMint, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20 token: %w", err) - } - return ref, nil, nil -} - -func (burnMintERC20Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) -} - -func (burnMintERC20Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) -} - -// ---------- BurnMintERC20WithDrip (v1.0.0) ---------- - -type burnMintERC20WithDripStrategy struct{} - -func (burnMintERC20WithDripStrategy) ContractType() deployment.ContractType { - return burn_mint_erc20_with_drip.ContractType -} - -func (burnMintERC20WithDripStrategy) Capabilities() strategy.Capabilities { - return strategy.Capabilities{ - SupportsAdminRole: true, - SupportsCCIPAdmin: true, - SupportsPreMint: true, - ParticipatesInPoolRoleGrant: true, - } -} - -func (burnMintERC20WithDripStrategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - qualifier := in.Symbol - maxSupply, preMint := scaledSupplyAndPreMint(in) - ref, err := contract.MaybeDeployContract(b, burn_mint_erc20_with_drip.Deploy, chain, contract.DeployInput[burn_mint_erc20_with_drip.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20_with_drip.ContractType, *common_utils.Version_1_0_0), - ChainSelector: chain.Selector, - Args: burn_mint_erc20_with_drip.ConstructorArgs{ - Name: in.Name, - Symbol: in.Symbol, - Decimals: in.Decimals, - MaxSupply: maxSupply, - PreMint: preMint, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20WithDrip token: %w", err) - } - return ref, nil, nil -} - -func (burnMintERC20WithDripStrategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) -} - -func (burnMintERC20WithDripStrategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) -} - -// ---------- BurnMintERC20WithDrip (v1.5.0) ---------- -// -// Pre-mint is unsupported because the v1.5.0 constructor takes neither -// supply nor decimals; matches the historical tokenSupportsPreMint table. - -type burnMintERC20WithDripV150Strategy struct{} - -func (burnMintERC20WithDripV150Strategy) ContractType() deployment.ContractType { - return drip_v150.ContractType -} - -func (burnMintERC20WithDripV150Strategy) Capabilities() strategy.Capabilities { - return strategy.Capabilities{ - SupportsAdminRole: true, - SupportsCCIPAdmin: true, - SupportsPreMint: false, - ParticipatesInPoolRoleGrant: true, - } -} - -func (burnMintERC20WithDripV150Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - qualifier := in.Symbol - ref, err := contract.MaybeDeployContract(b, drip_v150.Deploy, chain, contract.DeployInput[drip_v150.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(drip_v150.ContractType, *drip_v150.Version), - ChainSelector: chain.Selector, - Args: drip_v150.ConstructorArgs{ - Name: in.Name, - Symbol: in.Symbol, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20WithDrip (v1.5.0) token: %w", err) - } - return ref, nil, nil -} - -func (burnMintERC20WithDripV150Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) -} - -func (burnMintERC20WithDripV150Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) -} - -// ---------- TIP-20 ---------- -// -// Tempo-only token; deployed via a factory sequence rather than -// MaybeDeployContract. CCIPAdmin and pre-mint do not apply. - -type tip20Strategy struct{} - -func (tip20Strategy) ContractType() deployment.ContractType { return tip20.ContractType } - -func (tip20Strategy) Capabilities() strategy.Capabilities { - return strategy.Capabilities{ - SupportsAdminRole: true, - SupportsCCIPAdmin: false, - SupportsPreMint: false, - ParticipatesInPoolRoleGrant: true, - } -} - -func (tip20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - // Initial admin must be the deployer so subsequent ops (e.g. GrantIssuerRole) - // pass IsAllowedCaller; ExternalAdmin receives DEFAULT_ADMIN_ROLE in a - // follow-up grant performed by the orchestrating sequence. - report, err := cldf_ops.ExecuteSequence(b, tip20.Deploy, chain, tip20.FactoryDeployArgs{ - QuoteToken: common.Address{}, - Currency: "", - Salt: [32]byte{}, - Symbol: in.Symbol, - Admin: chain.DeployerKey.From, - Name: in.Name, - }) - if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy TIP20 token via factory: %w", err) - } - if len(report.Output.Addresses) == 0 { - return datastore.AddressRef{}, nil, errors.New("no address returned from TIP20 factory deployment") - } - return report.Output.Addresses[0], nil, nil -} - -func (tip20Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, tip20.GrantIssuerRole, chain, contract.FunctionInput[common.Address]{ - ChainSelector: chainSelector, - Address: token, - Args: pool, - }) - if err != nil { - return nil, fmt.Errorf("failed to grant TIP-20 issuer role: %w", err) - } - return []contract.WriteOutput{report.Output}, nil -} - -func (tip20Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, tip20.GrantAdminRole, chain, contract.FunctionInput[common.Address]{ - ChainSelector: chainSelector, - Address: token, - Args: externalAdmin, - }) - if err != nil { - return nil, fmt.Errorf("failed to grant TIP-20 admin role: %w", err) - } - return []contract.WriteOutput{report.Output}, nil -} - -// ---------- shared helpers ---------- - -func scaledSupplyAndPreMint(in tokensapi.DeployTokenInput) (*big.Int, *big.Int) { - maxSupply := big.NewInt(0) - if in.Supply != nil { - maxSupply = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.Supply), in.Decimals) - } - preMint := big.NewInt(0) - if in.PreMint != nil { - preMint = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.PreMint), in.Decimals) - } - return maxSupply, preMint -} - -// grantBnMMintAndBurnRoles is shared by all BnM-family strategies. -// Historically the BnM, BnM+Drip (v1.0.0), and BnM+Drip (v1.5.0) types -// all dispatch to burn_mint_erc20.GrantMintAndBurnRoles (the v1.0.0 op); -// preserved here verbatim. -func grantBnMMintAndBurnRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantMintAndBurnRoles, chain, contract.FunctionInput[common.Address]{ - ChainSelector: chainSelector, - Address: token, - Args: pool, - }) - if err != nil { - return nil, fmt.Errorf("failed to grant mint and burn roles: %w", err) - } - return []contract.WriteOutput{report.Output}, nil -} - -func grantBnMDefaultAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - tokenContract, err := bnm_erc20_bindings.NewBurnMintERC20(token, chain.Client) - if err != nil { - return nil, fmt.Errorf("failed to instantiate BurnMintERC20 contract: %w", err) - } - role, err := tokenContract.DEFAULTADMINROLE(&bind.CallOpts{Context: b.GetContext()}) - if err != nil { - return nil, fmt.Errorf("failed to get default admin role constant: %w", err) - } - report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantAdminRole, chain, contract.FunctionInput[burn_mint_erc20.RoleAssignment]{ - ChainSelector: chainSelector, - Address: token, - Args: burn_mint_erc20.RoleAssignment{ - Role: role, - To: externalAdmin, - }, - }) - if err != nil { - return nil, fmt.Errorf("failed to grant default admin role: %w", err) - } - return []contract.WriteOutput{report.Output}, nil -} diff --git a/chains/evm/deployment/tokens/strategy/registrations/tip20.go b/chains/evm/deployment/tokens/strategy/registrations/tip20.go new file mode 100644 index 0000000000..872f3b65a1 --- /dev/null +++ b/chains/evm/deployment/tokens/strategy/registrations/tip20.go @@ -0,0 +1,82 @@ +package registrations + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +func init() { + strategy.GetRegistry().RegisterEVM(tip20Strategy{}) +} + +// tip20Strategy is the Tempo-only TIP-20 token strategy. Deployed via a +// factory sequence rather than MaybeDeployContract; CCIPAdmin and pre-mint +// do not apply. +type tip20Strategy struct{} + +func (tip20Strategy) ContractType() deployment.ContractType { return tip20.ContractType } + +func (tip20Strategy) Capabilities() strategy.Capabilities { + return strategy.Capabilities{ + SupportsAdminRole: true, + SupportsCCIPAdmin: false, + SupportsPreMint: false, + ParticipatesInPoolRoleGrant: true, + } +} + +func (tip20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + // Initial admin must be the deployer so subsequent ops (e.g. GrantIssuerRole) + // pass IsAllowedCaller; ExternalAdmin receives DEFAULT_ADMIN_ROLE in a + // follow-up grant performed by the orchestrating sequence. + report, err := cldf_ops.ExecuteSequence(b, tip20.Deploy, chain, tip20.FactoryDeployArgs{ + QuoteToken: common.Address{}, + Currency: "", + Salt: [32]byte{}, + Symbol: in.Symbol, + Admin: chain.DeployerKey.From, + Name: in.Name, + }) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy TIP20 token via factory: %w", err) + } + if len(report.Output.Addresses) == 0 { + return datastore.AddressRef{}, nil, errors.New("no address returned from TIP20 factory deployment") + } + return report.Output.Addresses[0], nil, nil +} + +func (tip20Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, tip20.GrantIssuerRole, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chainSelector, + Address: token, + Args: pool, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant TIP-20 issuer role: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} + +func (tip20Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, tip20.GrantAdminRole, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chainSelector, + Address: token, + Args: externalAdmin, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant TIP-20 admin role: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} From 09f6c5c44949d007d66e52780dfaf19407bdbd5e Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Tue, 28 Apr 2026 16:09:12 -0700 Subject: [PATCH 03/14] fix string nit --- chains/evm/deployment/v2_0_0/adapters/tokens.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chains/evm/deployment/v2_0_0/adapters/tokens.go b/chains/evm/deployment/v2_0_0/adapters/tokens.go index f8c0c9b36a..998591a4d1 100644 --- a/chains/evm/deployment/v2_0_0/adapters/tokens.go +++ b/chains/evm/deployment/v2_0_0/adapters/tokens.go @@ -148,7 +148,7 @@ func (t *TokenAdapter) DeployTokenPoolForToken() *cldf_ops.Sequence[tokens.Deplo tokenAddr, input.ChainSelector, probeErr) } } - if string(guardType) == string(tip20ops.ContractType) { + if guardType.String() == tip20ops.ContractType.String() { return sequences.OnChainOutput{}, fmt.Errorf("TIP-20 tokens are not supported on CCIP v2.0 (v1.6-only by product decision)") } From 2cd0973ff5a3858d15d8792d3229410470d77336 Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Sun, 3 May 2026 02:10:50 -0700 Subject: [PATCH 04/14] chore: remove STRATEGY_REFACTOR.md --- STRATEGY_REFACTOR.md | 81 -------------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 STRATEGY_REFACTOR.md diff --git a/STRATEGY_REFACTOR.md b/STRATEGY_REFACTOR.md deleted file mode 100644 index f327d17865..0000000000 --- a/STRATEGY_REFACTOR.md +++ /dev/null @@ -1,81 +0,0 @@ -# Per-Token-Type Strategy Refactor - -## Objective - -Two related complaints in the EVM token-expansion code: - -1. The token-pool deployment adapter (`chains/evm/deployment/v1_0_0/adapters/pool_adapter.go`) had a switch on token type to decide which role-grant operation to call. This switch grew with each new token type. The same token-type dispatch pattern repeated in three other places — token deployment, capability predicates, and the external-admin role grant. -2. The v2.0 adapter (`chains/evm/deployment/v2_0_0/adapters/tokens.go`) reimplemented the role-grant logic inline rather than reusing v1.0.0 utilities. New v1.6 token types did not flow through to v2.0 automatically. - -The goal: encapsulate everything specific to a token contract type behind one per-token strategy, registered into a registry that is independent of pool version. The four switches collapse to registry lookups, and v2.0 picks up new BurnMint-family token types automatically. - -## Direction - -A `EVMTokenStrategy` interface lives in `chains/evm/deployment/tokens/strategy/`: - -```go -type EVMTokenStrategy interface { - ContractType() deployment.ContractType - Capabilities() Capabilities - - Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) ( - datastore.AddressRef, []evm_contract.WriteOutput, error) - - GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, - token, pool common.Address, chainSelector uint64) ([]evm_contract.WriteOutput, error) - - GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, - token, externalAdmin common.Address, chainSelector uint64) ([]evm_contract.WriteOutput, error) -} -``` - -Strategies are registered into a singleton `Registry` keyed by `ContractType` only — pool version is deliberately excluded. - -All five known EVM token types (plain ERC20, BurnMintERC20, BurnMintERC20WithDrip v1.0.0, BurnMintERC20WithDrip v1.5.0, TIP-20) are wrapped as small strategy structs in one file at `chains/evm/deployment/tokens/strategy/registrations/registrations.go`. Adapters pick them up via a single blank import. - -The four dispatch sites become registry lookups: - -| Site | Now becomes | -|---|---| -| `pool_adapter.go` role-grant switch | `strat.GrantPoolRoles(...)` | -| `sequences/token.go` deploy switch | `strat.Deploy(...)` | -| `sequences/token.go` capability predicates | `strat.Capabilities().Supports*` | -| `sequences/token.go` external-admin switch | `strat.GrantExternalAdmin(...)` | - -The v2.0 adapter's `DeployTokenPoolForToken` keeps its v2-specific pool-type dispatch but its inline role-grant block becomes the same registry lookup as v1.0.0. The previous `isBurnMintTokenType` filter is deleted. - -## Key design decisions - -**Registry key is `(chainFamily, ContractType)`, not pool version.** This is the entire point of the refactor — adding a new BurnMint-family token type to the registry makes it available on v1.5.1, v1.6.x, and v2.0 simultaneously, with one line in `registrations.go`. - -**Capabilities is a struct with four explicit flags, including `ParticipatesInPoolRoleGrant`.** Two Plan agents reviewed the design independently; both converged on the explicit flag rather than inferring non-participation from a no-op `GrantPoolRoles` return. Audit reads the flag, not a side effect. - -**TIP-20 stays v1.6-only via an explicit guard inside `v2_0_0/adapters/tokens.go`.** The strategy registry is version-independent (TIP-20 is registered globally), but the v2.0 adapter rejects TIP-20 early with a clear error message. The v1.6-only constraint is locally checkable in the v2.0 file rather than implicit in pool-type gates elsewhere. - -**Strategies live in one file, ops packages stay untouched.** All five strategy structs are colocated in `registrations/registrations.go`. Ops packages (`burn_mint_erc20`, `tip20`, etc.) are not modified — strategies are thin wrappers that compose existing operations. Adding a new token type means: add an ops package (as today), add a strategy struct, add one Register call. No edits to any pool adapter. - -**Default policies are preserved per call site.** Deploy lookup miss is fail-fast; role-grant lookup miss is warn-and-continue; capability lookup miss returns the zero-value struct (all-false). Three different policies, all matching the prior behavior verbatim. - -**Pool-type dispatch (`isBurnMintPoolType` etc.) is deliberately untouched.** Token type and pool type are orthogonal; conflating them was rejected in design review. - -## What changed - -New (3 files): `chains/evm/deployment/tokens/strategy/{strategy.go, registry.go, registrations/registrations.go}`. - -Edited: -- `chains/evm/deployment/v1_0_0/adapters/pool_adapter.go` — role-grant switch replaced with registry call; unused ops imports removed. -- `chains/evm/deployment/v1_0_0/sequences/token.go` — deploy switch, capability predicates, and external-admin switch all replaced with registry lookups; three `tokenSupports*` predicate funcs deleted. -- `chains/evm/deployment/v2_0_0/adapters/tokens.go` — TIP-20 guard added; role-grant tail replaced with registry call; `isBurnMintTokenType` deleted. -- `chains/evm/deployment/v1_0_0/adapters/init.go` — blank import of `registrations` so strategies are loaded for all transitively-importing adapters. - -`go build ./...` and `go vet ./...` are clean across all submodules of the experiments workspace. - -## What's deferred - -Tests are deferred to a follow-up pass. Once the implementation direction is confirmed, the test pass should add: -- Registry unit tests -- Golden capability-table test asserting every existing token type's capabilities match the pre-refactor `tokenSupports*` truth tables -- TIP-20 v2.0 guard test -- An end-to-end test that pre-registers a fake `ContractType` strategy and exercises both v1.0 and v2.0 dispatch backbones - -The existing token deployment test (`v1_0_0/sequences/token_test.go`) was lightly updated to call the new registry instead of the deleted predicate so the package still compiles. From 09bb3acd96d74c1c9f0cdf8a78e089ef05c8631c Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Sun, 3 May 2026 02:11:03 -0700 Subject: [PATCH 05/14] chore: revert 2.0 changes --- .../evm/deployment/v2_0_0/adapters/tokens.go | 78 ++++++++----------- 1 file changed, 32 insertions(+), 46 deletions(-) diff --git a/chains/evm/deployment/v2_0_0/adapters/tokens.go b/chains/evm/deployment/v2_0_0/adapters/tokens.go index 998591a4d1..a683663e88 100644 --- a/chains/evm/deployment/v2_0_0/adapters/tokens.go +++ b/chains/evm/deployment/v2_0_0/adapters/tokens.go @@ -11,16 +11,17 @@ import ( "github.com/ethereum/go-ethereum/common" mcms_types "github.com/smartcontractkit/mcms/types" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" evm1_0_0 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/adapters" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/siloed_lock_release_token_pool" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/operations/token_pool" evm_tokens "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v2_0_0/sequences/tokens" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + bnmOps "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" + bnmDripOps "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" rmnproxyops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/rmn_proxy" - tip20ops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router" + bnmDripOps150 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" "github.com/smartcontractkit/chainlink-ccip/deployment/finality" "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" @@ -129,29 +130,6 @@ func (t *TokenAdapter) DeployTokenPoolForToken() *cldf_ops.Sequence[tokens.Deplo return sequences.OnChainOutput{}, nil } - // Defensive: TIP-20 tokens are v1.6-only by product decision; reject before pool deploy - // even when the pool-type gate would otherwise allow it. Resolve the token type via the - // caller-supplied input first, then fall back to a datastore probe by address. If neither - // resolves, log a warning so a potential bypass is visible rather than silent. - guardType := datastore.ContractType("") - if input.TokenRef != nil { - guardType = input.TokenRef.Type - } - if guardType == "" { - if typeProbe, probeErr := datastore_utils.FindAndFormatRef(input.ExistingDataStore, datastore.AddressRef{ - ChainSelector: input.ChainSelector, - Address: tokenAddr, - }, input.ChainSelector, datastore_utils.FullRef); probeErr == nil { - guardType = typeProbe.Type - } else { - b.Logger.Warnf("TIP-20 v2.0 guard could not resolve token type for address %s on chain %d (probe: %v); skipping guard", - tokenAddr, input.ChainSelector, probeErr) - } - } - if guardType.String() == tip20ops.ContractType.String() { - return sequences.OnChainOutput{}, fmt.Errorf("TIP-20 tokens are not supported on CCIP v2.0 (v1.6-only by product decision)") - } - tokenContract, err := erc20.NewERC20(common.HexToAddress(tokenAddr), evmChain.Client) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to bind ERC20 at %s: %w", tokenAddr, err) @@ -230,28 +208,30 @@ func (t *TokenAdapter) DeployTokenPoolForToken() *cldf_ops.Sequence[tokens.Deplo ChainSelector: input.ChainSelector, Address: tokenAddr, }, input.ChainSelector, datastore_utils.FullRef) - if lookupErr == nil { - strat, ok := strategy.GetRegistry().GetEVM(deployment.ContractType(toknRef.Type)) - if ok && strat.Capabilities().ParticipatesInPoolRoleGrant { - poolRef := deployOutput.Addresses[0] - poolAddr := common.HexToAddress(poolRef.Address) - if poolAddr == (common.Address{}) { - return sequences.OnChainOutput{}, errors.New("deployed token pool address is zero") - } - - grantWrites, grantErr := strat.GrantPoolRoles(b, evmChain, common.HexToAddress(tokenAddr), poolAddr, input.ChainSelector) - if grantErr != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to grant pool roles for token type %q (token %s, pool %s) on chain %d: %w", toknRef.Type, tokenAddr, poolAddr, input.ChainSelector, grantErr) - } - - if len(grantWrites) > 0 { - batchOp, bErr := contract.NewBatchOperationFromWrites(grantWrites) - if bErr != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to create batch operation for role grants: %w", bErr) - } - result.BatchOps = append(result.BatchOps, batchOp) - } + if lookupErr == nil && isBurnMintTokenType(toknRef.Type) { + poolRef := deployOutput.Addresses[0] + poolAddr := common.HexToAddress(poolRef.Address) + if poolAddr == (common.Address{}) { + return sequences.OnChainOutput{}, errors.New("deployed token pool address is zero") } + + grantReport, grantErr := cldf_ops.ExecuteOperation(b, + bnmOps.GrantMintAndBurnRoles, evmChain, + contract.FunctionInput[common.Address]{ + ChainSelector: input.ChainSelector, + Address: common.HexToAddress(tokenAddr), + Args: poolAddr, + }, + ) + if grantErr != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to grant mint/burn roles to pool %s for token %s: %w", poolAddr, tokenAddr, grantErr) + } + + batchOp, bErr := contract.NewBatchOperationFromWrites([]contract.WriteOutput{grantReport.Output}) + if bErr != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to create batch operation for role grants: %w", bErr) + } + result.BatchOps = append(result.BatchOps, batchOp) } } @@ -487,3 +467,9 @@ func isLockReleasePoolType(poolType deployment.ContractType) bool { return poolType == cciputils.LockReleaseTokenPool || poolType == siloed_lock_release_token_pool.ContractType } + +func isBurnMintTokenType(typ datastore.ContractType) bool { + return typ.String() == bnmOps.ContractType.String() || + typ.String() == bnmDripOps.ContractType.String() || + typ.String() == bnmDripOps150.ContractType.String() +} From 24bf9888cd2215885c30fa474cf9cc175dea2d1c Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Sun, 3 May 2026 15:07:57 -0700 Subject: [PATCH 06/14] refactor: wip experiment with different interface layout --- .../strategy/registrations/burn_mint_erc20.go | 65 ----- .../burn_mint_erc20_with_drip.go | 65 ----- .../burn_mint_erc20_with_drip_v150.go | 63 ---- .../tokens/strategy/registrations/doc.go | 9 - .../tokens/strategy/registrations/erc20.go | 55 ---- .../tokens/strategy/registrations/helpers.go | 67 ----- .../deployment/tokens/strategy/registry.go | 66 ----- .../deployment/tokens/strategy/strategy.go | 74 ----- .../deployment/tokens/tokenimpl/helpers.go | 102 +++++++ .../evm/deployment/tokens/tokenimpl/impl.go | 91 ++++++ .../evm/deployment/tokens/tokenimpl/lookup.go | 31 ++ .../tokens/tokenimpl/token_burn_mint_erc20.go | 86 ++++++ .../token_burn_mint_erc20_with_drip.go | 72 +++++ .../tokens/tokenimpl/token_erc20.go | 70 +++++ .../tip20.go => tokenimpl/token_tip20.go} | 96 ++++--- chains/evm/deployment/v1_0_0/adapters/init.go | 3 - .../v1_0_0/adapters/pool_adapter.go | 268 ++++++++---------- .../v1_0_0/operations/tip20/tip20_contract.go | 9 +- .../v1_0_0/operations/tip20/tip20_ops.go | 28 ++ .../evm/deployment/v1_0_0/sequences/token.go | 86 +++--- .../deployment/v1_0_0/sequences/token_test.go | 37 ++- 21 files changed, 725 insertions(+), 718 deletions(-) delete mode 100644 chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20.go delete mode 100644 chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip.go delete mode 100644 chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip_v150.go delete mode 100644 chains/evm/deployment/tokens/strategy/registrations/doc.go delete mode 100644 chains/evm/deployment/tokens/strategy/registrations/erc20.go delete mode 100644 chains/evm/deployment/tokens/strategy/registrations/helpers.go delete mode 100644 chains/evm/deployment/tokens/strategy/registry.go delete mode 100644 chains/evm/deployment/tokens/strategy/strategy.go create mode 100644 chains/evm/deployment/tokens/tokenimpl/helpers.go create mode 100644 chains/evm/deployment/tokens/tokenimpl/impl.go create mode 100644 chains/evm/deployment/tokens/tokenimpl/lookup.go create mode 100644 chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go create mode 100644 chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go create mode 100644 chains/evm/deployment/tokens/tokenimpl/token_erc20.go rename chains/evm/deployment/tokens/{strategy/registrations/tip20.go => tokenimpl/token_tip20.go} (52%) diff --git a/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20.go b/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20.go deleted file mode 100644 index a62cad3def..0000000000 --- a/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20.go +++ /dev/null @@ -1,65 +0,0 @@ -package registrations - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" - tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" -) - -func init() { - strategy.GetRegistry().RegisterEVM(burnMintERC20Strategy{}) -} - -type burnMintERC20Strategy struct{} - -func (burnMintERC20Strategy) ContractType() deployment.ContractType { - return burn_mint_erc20.ContractType -} - -func (burnMintERC20Strategy) Capabilities() strategy.Capabilities { - return strategy.Capabilities{ - SupportsAdminRole: true, - SupportsCCIPAdmin: true, - SupportsPreMint: true, - ParticipatesInPoolRoleGrant: true, - } -} - -func (burnMintERC20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - qualifier := in.Symbol - maxSupply, preMint := scaledSupplyAndPreMint(in) - ref, err := contract.MaybeDeployContract(b, burn_mint_erc20.Deploy, chain, contract.DeployInput[burn_mint_erc20.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20.ContractType, *common_utils.Version_1_0_0), - ChainSelector: chain.Selector, - Args: burn_mint_erc20.ConstructorArgs{ - Name: in.Name, - Symbol: in.Symbol, - Decimals: in.Decimals, - MaxSupply: maxSupply, - PreMint: preMint, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20 token: %w", err) - } - return ref, nil, nil -} - -func (burnMintERC20Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) -} - -func (burnMintERC20Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) -} diff --git a/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip.go b/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip.go deleted file mode 100644 index bad8bf1946..0000000000 --- a/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip.go +++ /dev/null @@ -1,65 +0,0 @@ -package registrations - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" - tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" -) - -func init() { - strategy.GetRegistry().RegisterEVM(burnMintERC20WithDripStrategy{}) -} - -type burnMintERC20WithDripStrategy struct{} - -func (burnMintERC20WithDripStrategy) ContractType() deployment.ContractType { - return burn_mint_erc20_with_drip.ContractType -} - -func (burnMintERC20WithDripStrategy) Capabilities() strategy.Capabilities { - return strategy.Capabilities{ - SupportsAdminRole: true, - SupportsCCIPAdmin: true, - SupportsPreMint: true, - ParticipatesInPoolRoleGrant: true, - } -} - -func (burnMintERC20WithDripStrategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - qualifier := in.Symbol - maxSupply, preMint := scaledSupplyAndPreMint(in) - ref, err := contract.MaybeDeployContract(b, burn_mint_erc20_with_drip.Deploy, chain, contract.DeployInput[burn_mint_erc20_with_drip.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20_with_drip.ContractType, *common_utils.Version_1_0_0), - ChainSelector: chain.Selector, - Args: burn_mint_erc20_with_drip.ConstructorArgs{ - Name: in.Name, - Symbol: in.Symbol, - Decimals: in.Decimals, - MaxSupply: maxSupply, - PreMint: preMint, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20WithDrip token: %w", err) - } - return ref, nil, nil -} - -func (burnMintERC20WithDripStrategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) -} - -func (burnMintERC20WithDripStrategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) -} diff --git a/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip_v150.go b/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip_v150.go deleted file mode 100644 index 3c8e6d7962..0000000000 --- a/chains/evm/deployment/tokens/strategy/registrations/burn_mint_erc20_with_drip_v150.go +++ /dev/null @@ -1,63 +0,0 @@ -package registrations - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" - drip_v150 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" - tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" -) - -func init() { - strategy.GetRegistry().RegisterEVM(burnMintERC20WithDripV150Strategy{}) -} - -// burnMintERC20WithDripV150Strategy is the v1.5.0 variant of BurnMintERC20WithDrip. -// Pre-mint is unsupported because the v1.5.0 constructor takes neither -// supply nor decimals; matches the historical tokenSupportsPreMint table. -type burnMintERC20WithDripV150Strategy struct{} - -func (burnMintERC20WithDripV150Strategy) ContractType() deployment.ContractType { - return drip_v150.ContractType -} - -func (burnMintERC20WithDripV150Strategy) Capabilities() strategy.Capabilities { - return strategy.Capabilities{ - SupportsAdminRole: true, - SupportsCCIPAdmin: true, - SupportsPreMint: false, - ParticipatesInPoolRoleGrant: true, - } -} - -func (burnMintERC20WithDripV150Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - qualifier := in.Symbol - ref, err := contract.MaybeDeployContract(b, drip_v150.Deploy, chain, contract.DeployInput[drip_v150.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(drip_v150.ContractType, *drip_v150.Version), - ChainSelector: chain.Selector, - Args: drip_v150.ConstructorArgs{ - Name: in.Name, - Symbol: in.Symbol, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20WithDrip (v1.5.0) token: %w", err) - } - return ref, nil, nil -} - -func (burnMintERC20WithDripV150Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMMintAndBurnRoles(b, chain, token, pool, chainSelector) -} - -func (burnMintERC20WithDripV150Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - return grantBnMDefaultAdminRole(b, chain, token, externalAdmin, chainSelector) -} diff --git a/chains/evm/deployment/tokens/strategy/registrations/doc.go b/chains/evm/deployment/tokens/strategy/registrations/doc.go deleted file mode 100644 index 684467e446..0000000000 --- a/chains/evm/deployment/tokens/strategy/registrations/doc.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package registrations registers all known EVM token contract strategies -// with the singleton strategy.Registry at init time. Adapters that need -// per-token-type behavior pull in this package via a blank import; all -// known token types become available in one line. -// -// Convention: each strategy lives in its own file with its own init() that -// registers itself. Adding a new EVM token contract type means dropping in -// one new file (strategy struct + init); no central registry list to amend. -package registrations diff --git a/chains/evm/deployment/tokens/strategy/registrations/erc20.go b/chains/evm/deployment/tokens/strategy/registrations/erc20.go deleted file mode 100644 index d6d2a85266..0000000000 --- a/chains/evm/deployment/tokens/strategy/registrations/erc20.go +++ /dev/null @@ -1,55 +0,0 @@ -package registrations - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" - tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" -) - -func init() { - strategy.GetRegistry().RegisterEVM(erc20Strategy{}) -} - -// erc20Strategy is the plain (non-CCIP-aware) ERC20 strategy. -type erc20Strategy struct{} - -func (erc20Strategy) ContractType() deployment.ContractType { return erc20.ContractType } - -func (erc20Strategy) Capabilities() strategy.Capabilities { - return strategy.Capabilities{} -} - -func (erc20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - qualifier := in.Symbol - ref, err := contract.MaybeDeployContract(b, erc20.Deploy, chain, contract.DeployInput[erc20.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(erc20.ContractType, *common_utils.Version_1_0_0), - ChainSelector: chain.Selector, - Args: erc20.ConstructorArgs{ - Name: in.Name, - Symbol: in.Symbol, - }, - Qualifier: &qualifier, - }, nil) - if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy ERC20 token: %w", err) - } - return ref, nil, nil -} - -func (erc20Strategy) GrantPoolRoles(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address, _ uint64) ([]contract.WriteOutput, error) { - return nil, nil -} - -func (erc20Strategy) GrantExternalAdmin(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address, _ uint64) ([]contract.WriteOutput, error) { - return nil, nil -} diff --git a/chains/evm/deployment/tokens/strategy/registrations/helpers.go b/chains/evm/deployment/tokens/strategy/registrations/helpers.go deleted file mode 100644 index a0d345d74d..0000000000 --- a/chains/evm/deployment/tokens/strategy/registrations/helpers.go +++ /dev/null @@ -1,67 +0,0 @@ -package registrations - -import ( - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" - tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" - bnm_erc20_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc20" -) - -func scaledSupplyAndPreMint(in tokensapi.DeployTokenInput) (*big.Int, *big.Int) { - maxSupply := big.NewInt(0) - if in.Supply != nil { - maxSupply = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.Supply), in.Decimals) - } - preMint := big.NewInt(0) - if in.PreMint != nil { - preMint = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.PreMint), in.Decimals) - } - return maxSupply, preMint -} - -// grantBnMMintAndBurnRoles is shared by all BnM-family strategies. -// Historically the BnM, BnM+Drip (v1.0.0), and BnM+Drip (v1.5.0) types -// all dispatch to burn_mint_erc20.GrantMintAndBurnRoles (the v1.0.0 op); -// preserved here verbatim. -func grantBnMMintAndBurnRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantMintAndBurnRoles, chain, contract.FunctionInput[common.Address]{ - ChainSelector: chainSelector, - Address: token, - Args: pool, - }) - if err != nil { - return nil, fmt.Errorf("failed to grant mint and burn roles: %w", err) - } - return []contract.WriteOutput{report.Output}, nil -} - -func grantBnMDefaultAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - tokenContract, err := bnm_erc20_bindings.NewBurnMintERC20(token, chain.Client) - if err != nil { - return nil, fmt.Errorf("failed to instantiate BurnMintERC20 contract: %w", err) - } - role, err := tokenContract.DEFAULTADMINROLE(&bind.CallOpts{Context: b.GetContext()}) - if err != nil { - return nil, fmt.Errorf("failed to get default admin role constant: %w", err) - } - report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantAdminRole, chain, contract.FunctionInput[burn_mint_erc20.RoleAssignment]{ - ChainSelector: chainSelector, - Address: token, - Args: burn_mint_erc20.RoleAssignment{ - Role: role, - To: externalAdmin, - }, - }) - if err != nil { - return nil, fmt.Errorf("failed to grant default admin role: %w", err) - } - return []contract.WriteOutput{report.Output}, nil -} diff --git a/chains/evm/deployment/tokens/strategy/registry.go b/chains/evm/deployment/tokens/strategy/registry.go deleted file mode 100644 index 18f9536d50..0000000000 --- a/chains/evm/deployment/tokens/strategy/registry.go +++ /dev/null @@ -1,66 +0,0 @@ -package strategy - -import ( - "sync" - - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" -) - -// Registry holds the per-token-type strategies for each chain family. -// EVM is the only family populated today; other families would gain -// their own interface and map alongside. -type Registry struct { - mu sync.Mutex - evm map[deployment.ContractType]EVMTokenStrategy -} - -func newRegistry() *Registry { - return &Registry{evm: make(map[deployment.ContractType]EVMTokenStrategy)} -} - -// RegisterEVM registers a strategy for an EVM token contract type. -// First registration wins; subsequent registrations for the same -// ContractType are no-ops, matching the semantics of -// tokens.RegisterTokenAdapter. -func (r *Registry) RegisterEVM(s EVMTokenStrategy) { - if s == nil { - return - } - r.mu.Lock() - defer r.mu.Unlock() - if _, exists := r.evm[s.ContractType()]; !exists { - r.evm[s.ContractType()] = s - } -} - -// GetEVM returns the registered strategy for an EVM token contract type. -// The boolean is false when no strategy is registered. -func (r *Registry) GetEVM(ct deployment.ContractType) (EVMTokenStrategy, bool) { - r.mu.Lock() - defer r.mu.Unlock() - s, ok := r.evm[ct] - return s, ok -} - -// CapabilitiesEVM returns the Capabilities for an EVM token contract -// type, or the zero value if no strategy is registered. The zero value -// preserves the historical "unknown type implies all-false" predicate -// behavior at the call sites. -func (r *Registry) CapabilitiesEVM(ct deployment.ContractType) Capabilities { - if s, ok := r.GetEVM(ct); ok { - return s.Capabilities() - } - return Capabilities{} -} - -var ( - singleton *Registry - once sync.Once -) - -// GetRegistry returns the global singleton registry. The first call -// constructs the registry; subsequent calls return the same pointer. -func GetRegistry() *Registry { - once.Do(func() { singleton = newRegistry() }) - return singleton -} diff --git a/chains/evm/deployment/tokens/strategy/strategy.go b/chains/evm/deployment/tokens/strategy/strategy.go deleted file mode 100644 index 32cf8fdba1..0000000000 --- a/chains/evm/deployment/tokens/strategy/strategy.go +++ /dev/null @@ -1,74 +0,0 @@ -// Package strategy provides per-token-contract-type behaviors for EVM -// token deployment and pool wiring. A strategy encapsulates everything -// specific to one token contract type (e.g. BurnMintERC20, TIP-20): -// how to deploy the token, how to grant pool roles after a pool is -// deployed, how to grant an external admin role, and which capabilities -// the token contract supports. -// -// Strategies are looked up by ContractType from the singleton Registry -// and are independent of pool version, so adding a new token type makes -// it available to every pool-version adapter that consults the registry. -package strategy - -import ( - "github.com/ethereum/go-ethereum/common" - - tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" - evm_contract "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" -) - -// Capabilities reports the optional flow steps a token contract type -// participates in. The orchestrating sequence reads these flags to -// decide whether to invoke the corresponding step. -// -// ParticipatesInPoolRoleGrant is declared explicitly rather than inferred -// from a no-op GrantPoolRoles return so that intentional non-participation -// (e.g. plain ERC20) is distinguishable from a strategy bug that returned -// no writes by accident. -type Capabilities struct { - SupportsAdminRole bool - SupportsCCIPAdmin bool - SupportsPreMint bool - ParticipatesInPoolRoleGrant bool -} - -// EVMTokenStrategy encapsulates everything specific to one EVM token -// contract type. Implementations are registered with the singleton -// Registry keyed by ContractType. -type EVMTokenStrategy interface { - // ContractType returns the deployment.ContractType used as the - // registry key. - ContractType() deployment.ContractType - - // Capabilities returns the static feature flags for this token type. - Capabilities() Capabilities - - // Deploy performs the token contract deployment, returning the - // resulting datastore reference and any token-side write outputs - // produced during deployment. Implementations wrap either an - // Operation (via contract.MaybeDeployContract) or a Sequence - // (via cldf_ops.ExecuteSequence) as appropriate for the token type. - Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) ( - datastore.AddressRef, []evm_contract.WriteOutput, error) - - // GrantPoolRoles emits the writes that authorize a freshly-deployed - // pool to mint/burn (or its TIP-20 issuer-role equivalent) against - // this token. Returns (nil, nil) for token types that do not - // participate in pool role granting; ParticipatesInPoolRoleGrant - // is the authoritative flag, callers should consult it first. - GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, - token, pool common.Address, chainSelector uint64) ( - []evm_contract.WriteOutput, error) - - // GrantExternalAdmin grants the default-admin or contract-specific - // admin role to externalAdmin. Implementations return (nil, nil) - // for token types whose Capabilities.SupportsAdminRole is false; - // callers should consult that flag first. - GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, - token, externalAdmin common.Address, chainSelector uint64) ( - []evm_contract.WriteOutput, error) -} diff --git a/chains/evm/deployment/tokens/tokenimpl/helpers.go b/chains/evm/deployment/tokens/tokenimpl/helpers.go new file mode 100644 index 0000000000..869f0af190 --- /dev/null +++ b/chains/evm/deployment/tokens/tokenimpl/helpers.go @@ -0,0 +1,102 @@ +package tokenimpl + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + bnm_erc20_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc20" +) + +func revokeBnMDefaultAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { + tokenContract, err := bnm_erc20_bindings.NewBurnMintERC20(token, chain.Client) + if err != nil { + return nil, fmt.Errorf("failed to instantiate BurnMintERC20 contract: %w", err) + } + role, err := tokenContract.DEFAULTADMINROLE(&bind.CallOpts{Context: b.GetContext()}) + if err != nil { + return nil, fmt.Errorf("failed to get default admin role constant: %w", err) + } + report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.RevokeAdminRole, chain, contract.FunctionInput[burn_mint_erc20.RoleAssignment]{ + ChainSelector: chain.Selector, + Address: token, + Args: burn_mint_erc20.RoleAssignment{ + Role: role, + To: user, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to revoke default admin role: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} + +func grantBnMDefaultAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { + tokenContract, err := bnm_erc20_bindings.NewBurnMintERC20(token, chain.Client) + if err != nil { + return nil, fmt.Errorf("failed to instantiate BurnMintERC20 contract: %w", err) + } + role, err := tokenContract.DEFAULTADMINROLE(&bind.CallOpts{Context: b.GetContext()}) + if err != nil { + return nil, fmt.Errorf("failed to get default admin role constant: %w", err) + } + report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantAdminRole, chain, contract.FunctionInput[burn_mint_erc20.RoleAssignment]{ + ChainSelector: chain.Selector, + Address: token, + Args: burn_mint_erc20.RoleAssignment{ + Role: role, + To: user, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant default admin role: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} + +func grantBnMMintAndBurnRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantMintAndBurnRoles, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chain.Selector, + Address: token, + Args: pool, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant mint and burn roles: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} + +func transferTokensERC20(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, erc20.Transfer, chain, contract.FunctionInput[erc20.TransferArgs]{ + ChainSelector: chain.Selector, + Address: token, + Args: erc20.TransferArgs{ + Amount: scaledAmount, + Receiver: to, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to transfer ERC20 tokens: %w", err) + } + + return []contract.WriteOutput{report.Output}, nil +} + +func setBnMCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.SetCCIPAdmin, chain, contract.FunctionInput[string]{ + ChainSelector: chain.Selector, + Address: token, + Args: ccipAdmin.Hex(), + }) + if err != nil { + return nil, fmt.Errorf("failed to set CCIP admin: %w", err) + } + return []contract.WriteOutput{report.Output}, nil +} diff --git a/chains/evm/deployment/tokens/tokenimpl/impl.go b/chains/evm/deployment/tokens/tokenimpl/impl.go new file mode 100644 index 0000000000..438b6dc94b --- /dev/null +++ b/chains/evm/deployment/tokens/tokenimpl/impl.go @@ -0,0 +1,91 @@ +// Package tokenimpl provides per-token-contract-type behaviors for EVM +// token deployment and pool wiring. A token implementation encapsulates everything +// specific to one token contract type (e.g. BurnMintERC20, TIP-20): +// how to deploy the token, how to grant pool roles after a pool is +// deployed, how to grant an external admin role, and which capabilities +// the token contract supports. +// +// Token implementations are looked up by ContractType through the EVM tokenimpl package +// and are independent of pool version, so adding a new token type makes it +// available to every pool-version adapter that consults the token implementation lookup. +package tokenimpl + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// CapabilitySet reports the optional flow steps a token contract type +// participates in. The orchestrating sequence reads these flags to +// decide whether to invoke the corresponding step. +// +// ParticipatesInPoolRoleGrant is declared explicitly rather than inferred +// from a no-op GrantPoolRoles return so that intentional non-participation +// (e.g. plain ERC20) is distinguishable from a strategy bug that returned +// no writes by accident. +type CapabilitySet struct { + // ParticipatesInPoolRoleGrant is true when the token requires token-side + // role grants for the pool to operate; GrantPoolRoles must emit those writes. + ParticipatesInPoolRoleGrant bool + + // SupportsAdminRole is true when the token exposes a manageable admin or + // default-admin role; GrantAdminRole and RevokeAdminRole must implement it. + SupportsAdminRole bool + + // SupportsCCIPAdmin is true when the token has a token-level CCIP admin; + // SetCCIPAdmin must emit the write that updates it. + SupportsCCIPAdmin bool + + // SupportsPreMint is true when the token can mint during deployment and + // transfer those tokens afterward to the configured recipient. + SupportsPreMint bool +} + +// Token encapsulates everything specific to one EVM token contract type. +type Token interface { + // ContractType returns the deployment.ContractType used as the registry key. + ContractType() deployment.ContractType + + // Capabilities returns the static feature flags for this token type. + Capabilities() CapabilitySet + + // RevokeAdminRole revokes the default-admin or contract-specific admin + // role from user. Callers should consult SupportsAdminRole first. + RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) + + // GrantAdminRole grants the default-admin or contract-specific + // admin role to user. Implementations return (nil, nil) for any + // token types whose Capabilities.SupportsAdminRole is false; + // callers should consult that flag first. + GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) + + // GrantPoolRoles emits the writes that authorize a freshly-deployed pool + // to mint/burn (or its TIP-20 issuer-role equivalent) against this token. + // Returns an error for token types that don't participate in pool role + // granting; ParticipatesInPoolRoleGrant is the authoritative flag, callers + // should consult it first. + GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) + + // SetCCIPAdmin sets the token-level CCIP admin where the token contract + // supports one. Callers should consult SupportsCCIPAdmin first. + SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, admin common.Address) ([]contract.WriteOutput, error) + + // Transfer emits the writes that transfer already-scaled token units from + // the deployer to to, typically for post-deploy pre-mint distribution. + Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) + + // Deploy performs the token contract deployment, returning the + // resulting datastore reference and any token-side write outputs + // produced during deployment. Implementations wrap either an + // Operation (via contract.MaybeDeployContract) or a Sequence + // (via cldf_ops.ExecuteSequence) as appropriate for the token type. + Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) +} diff --git a/chains/evm/deployment/tokens/tokenimpl/lookup.go b/chains/evm/deployment/tokens/tokenimpl/lookup.go new file mode 100644 index 0000000000..48920e8b14 --- /dev/null +++ b/chains/evm/deployment/tokens/tokenimpl/lookup.go @@ -0,0 +1,31 @@ +package tokenimpl + +import ( + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" +) + +var tokenImpls = map[deployment.ContractType]Token{ + burn_mint_erc20_with_drip.ContractType: tokenBurnMintERC20WithDrip{}, + burn_mint_erc20.ContractType: tokenBurnMintERC20{}, + erc20.ContractType: tokenERC20{}, + tip20.ContractType: tokenTIP20{}, +} + +// Get returns the token implementation for an EVM token contract type. +func Get(ct deployment.ContractType) (Token, bool) { + s, ok := tokenImpls[ct] + return s, ok +} + +// Capabilities returns the Capabilities for an EVM token contract type, or the +// zero value if no strategy exists. +func Capabilities(ct deployment.ContractType) CapabilitySet { + if s, ok := Get(ct); ok { + return s.Capabilities() + } + return CapabilitySet{} +} diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go new file mode 100644 index 0000000000..cddae2306f --- /dev/null +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go @@ -0,0 +1,86 @@ +package tokenimpl + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +type tokenBurnMintERC20 struct{} + +func (tokenBurnMintERC20) ContractType() deployment.ContractType { + return burn_mint_erc20.ContractType +} + +func (tokenBurnMintERC20) Capabilities() CapabilitySet { + return CapabilitySet{ + ParticipatesInPoolRoleGrant: true, + SupportsAdminRole: true, + SupportsCCIPAdmin: true, + SupportsPreMint: true, + } +} + +func (tokenBurnMintERC20) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { + return revokeBnMDefaultAdminRole(b, chain, token, user) +} + +func (tokenBurnMintERC20) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { + return grantBnMDefaultAdminRole(b, chain, token, externalAdmin) +} + +func (tokenBurnMintERC20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { + return grantBnMMintAndBurnRoles(b, chain, token, pool) +} + +func (tokenBurnMintERC20) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { + return setBnMCCIPAdmin(b, chain, token, ccipAdmin) +} + +func (tokenBurnMintERC20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { + // NOTE: BnM ERC20 tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 strategy. + return transferTokensERC20(b, chain, token, to, scaledAmount) +} + +func (tokenBurnMintERC20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + maxSupply := big.NewInt(0) + if in.Supply != nil { + maxSupply = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.Supply), in.Decimals) + } + + preMint := big.NewInt(0) + if in.PreMint != nil { + preMint = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.PreMint), in.Decimals) + } + + ref, err := contract.MaybeDeployContract(b, burn_mint_erc20.Deploy, chain, + contract.DeployInput[burn_mint_erc20.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20.ContractType, *common_utils.Version_1_0_0), + ChainSelector: chain.Selector, + Qualifier: &in.Symbol, + Args: burn_mint_erc20.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + Decimals: in.Decimals, + MaxSupply: maxSupply, + PreMint: preMint, + }, + }, + nil, + ) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20 token: %w", err) + } + + return ref, nil, nil +} diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go new file mode 100644 index 0000000000..1e17c734af --- /dev/null +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go @@ -0,0 +1,72 @@ +package tokenimpl + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + drip_v150 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +type tokenBurnMintERC20WithDrip struct{} + +func (tokenBurnMintERC20WithDrip) ContractType() deployment.ContractType { + return drip_v150.ContractType +} + +func (tokenBurnMintERC20WithDrip) Capabilities() CapabilitySet { + return CapabilitySet{ + ParticipatesInPoolRoleGrant: true, + SupportsAdminRole: true, + SupportsCCIPAdmin: true, + SupportsPreMint: false, + } +} + +func (tokenBurnMintERC20WithDrip) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { + return revokeBnMDefaultAdminRole(b, chain, token, externalAdmin) +} + +func (tokenBurnMintERC20WithDrip) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { + return grantBnMDefaultAdminRole(b, chain, token, externalAdmin) +} + +func (tokenBurnMintERC20WithDrip) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { + return grantBnMMintAndBurnRoles(b, chain, token, pool) +} + +func (tokenBurnMintERC20WithDrip) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { + return setBnMCCIPAdmin(b, chain, token, ccipAdmin) +} + +func (tokenBurnMintERC20WithDrip) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { + // NOTE: BnM ERC20 drip tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 strategy. + return transferTokensERC20(b, chain, token, to, scaledAmount) +} + +func (tokenBurnMintERC20WithDrip) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + ref, err := contract.MaybeDeployContract(b, drip_v150.Deploy, chain, + contract.DeployInput[drip_v150.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(drip_v150.ContractType, *drip_v150.Version), + ChainSelector: chain.Selector, + Qualifier: &in.Symbol, + Args: drip_v150.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + }, + }, + nil, + ) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20WithDrip (v1.5.0) token: %w", err) + } + + return ref, nil, nil +} diff --git a/chains/evm/deployment/tokens/tokenimpl/token_erc20.go b/chains/evm/deployment/tokens/tokenimpl/token_erc20.go new file mode 100644 index 0000000000..d01449cafd --- /dev/null +++ b/chains/evm/deployment/tokens/tokenimpl/token_erc20.go @@ -0,0 +1,70 @@ +package tokenimpl + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// tokenERC20 is the plain (non-CCIP-aware) ERC20 strategy. +type tokenERC20 struct{} + +func (tokenERC20) ContractType() deployment.ContractType { return erc20.ContractType } + +func (tokenERC20) Capabilities() CapabilitySet { + return CapabilitySet{ + ParticipatesInPoolRoleGrant: false, + SupportsAdminRole: false, + SupportsCCIPAdmin: false, + SupportsPreMint: false, + } +} + +func (tokenERC20) RevokeAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { + return nil, fmt.Errorf("admin role not supported for plain ERC20 strategy") +} + +func (tokenERC20) GrantAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { + return nil, fmt.Errorf("admin role granting not supported for plain ERC20 strategy") +} + +func (tokenERC20) GrantPoolRoles(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { + return nil, fmt.Errorf("pool role granting not supported for plain ERC20 strategy") +} + +func (tokenERC20) SetCCIPAdmin(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { + return nil, fmt.Errorf("CCIP admin role not supported for plain ERC20 strategy") +} + +func (tokenERC20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { + return transferTokensERC20(b, chain, token, to, scaledAmount) +} + +func (tokenERC20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + ref, err := contract.MaybeDeployContract(b, erc20.Deploy, chain, + contract.DeployInput[erc20.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(erc20.ContractType, *common_utils.Version_1_0_0), + ChainSelector: chain.Selector, + Qualifier: &in.Symbol, + Args: erc20.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + }, + }, + nil, + ) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy ERC20 token: %w", err) + } + return ref, nil, nil +} diff --git a/chains/evm/deployment/tokens/strategy/registrations/tip20.go b/chains/evm/deployment/tokens/tokenimpl/token_tip20.go similarity index 52% rename from chains/evm/deployment/tokens/strategy/registrations/tip20.go rename to chains/evm/deployment/tokens/tokenimpl/token_tip20.go index 872f3b65a1..cafe8521f9 100644 --- a/chains/evm/deployment/tokens/strategy/registrations/tip20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_tip20.go @@ -1,12 +1,12 @@ -package registrations +package tokenimpl import ( "errors" "fmt" + "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" @@ -16,50 +16,49 @@ import ( cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) -func init() { - strategy.GetRegistry().RegisterEVM(tip20Strategy{}) -} - -// tip20Strategy is the Tempo-only TIP-20 token strategy. Deployed via a +// tokenTIP20 is the Tempo-only TIP-20 token strategy. Deployed via a // factory sequence rather than MaybeDeployContract; CCIPAdmin and pre-mint // do not apply. -type tip20Strategy struct{} +type tokenTIP20 struct{} -func (tip20Strategy) ContractType() deployment.ContractType { return tip20.ContractType } +func (tokenTIP20) ContractType() deployment.ContractType { return tip20.ContractType } -func (tip20Strategy) Capabilities() strategy.Capabilities { - return strategy.Capabilities{ +func (tokenTIP20) Capabilities() CapabilitySet { + return CapabilitySet{ + ParticipatesInPoolRoleGrant: true, SupportsAdminRole: true, SupportsCCIPAdmin: false, SupportsPreMint: false, - ParticipatesInPoolRoleGrant: true, } } -func (tip20Strategy) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - // Initial admin must be the deployer so subsequent ops (e.g. GrantIssuerRole) - // pass IsAllowedCaller; ExternalAdmin receives DEFAULT_ADMIN_ROLE in a - // follow-up grant performed by the orchestrating sequence. - report, err := cldf_ops.ExecuteSequence(b, tip20.Deploy, chain, tip20.FactoryDeployArgs{ - QuoteToken: common.Address{}, - Currency: "", - Salt: [32]byte{}, - Symbol: in.Symbol, - Admin: chain.DeployerKey.From, - Name: in.Name, +func (tokenTIP20) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, tip20.RevokeAdminRole, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chain.Selector, + Address: token, + Args: user, }) if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy TIP20 token via factory: %w", err) + return nil, fmt.Errorf("failed to revoke TIP-20 admin role: %w", err) } - if len(report.Output.Addresses) == 0 { - return datastore.AddressRef{}, nil, errors.New("no address returned from TIP20 factory deployment") + return []contract.WriteOutput{report.Output}, nil +} + +func (tokenTIP20) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, tip20.GrantAdminRole, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chain.Selector, + Address: token, + Args: user, + }) + if err != nil { + return nil, fmt.Errorf("failed to grant TIP-20 admin role: %w", err) } - return report.Output.Addresses[0], nil, nil + return []contract.WriteOutput{report.Output}, nil } -func (tip20Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { +func (tokenTIP20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { report, err := cldf_ops.ExecuteOperation(b, tip20.GrantIssuerRole, chain, contract.FunctionInput[common.Address]{ - ChainSelector: chainSelector, + ChainSelector: chain.Selector, Address: token, Args: pool, }) @@ -69,14 +68,43 @@ func (tip20Strategy) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, p return []contract.WriteOutput{report.Output}, nil } -func (tip20Strategy) GrantExternalAdmin(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address, chainSelector uint64) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, tip20.GrantAdminRole, chain, contract.FunctionInput[common.Address]{ - ChainSelector: chainSelector, +func (tokenTIP20) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { + return nil, fmt.Errorf("CCIP admin role not supported for TIP-20 strategy") +} + +func (tokenTIP20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, tip20.Transfer, chain, contract.FunctionInput[tip20.TransferArgs]{ + ChainSelector: chain.Selector, Address: token, - Args: externalAdmin, + Args: tip20.TransferArgs{ + Amount: scaledAmount, + Receiver: to, + }, }) if err != nil { - return nil, fmt.Errorf("failed to grant TIP-20 admin role: %w", err) + return nil, fmt.Errorf("failed to transfer TIP-20 tokens: %w", err) } + return []contract.WriteOutput{report.Output}, nil } + +func (tokenTIP20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + // Initial admin must be the deployer so subsequent ops (e.g. GrantIssuerRole) + // pass IsAllowedCaller; ExternalAdmin receives DEFAULT_ADMIN_ROLE in a + // follow-up grant performed by the orchestrating sequence. + report, err := cldf_ops.ExecuteSequence(b, tip20.Deploy, chain, tip20.FactoryDeployArgs{ + QuoteToken: common.Address{}, + Currency: in.Currency, + Salt: [32]byte{}, + Symbol: in.Symbol, + Admin: chain.DeployerKey.From, + Name: in.Name, + }) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy TIP20 token via factory: %w", err) + } + if len(report.Output.Addresses) == 0 { + return datastore.AddressRef{}, nil, errors.New("no address returned from TIP20 factory deployment") + } + return report.Output.Addresses[0], nil, nil +} diff --git a/chains/evm/deployment/v1_0_0/adapters/init.go b/chains/evm/deployment/v1_0_0/adapters/init.go index e5acdaaf78..1315631983 100644 --- a/chains/evm/deployment/v1_0_0/adapters/init.go +++ b/chains/evm/deployment/v1_0_0/adapters/init.go @@ -4,9 +4,6 @@ import ( "github.com/Masterminds/semver/v3" chain_selectors "github.com/smartcontractkit/chain-selectors" - // Pull in EVM token-contract strategies so the per-token-type registry is - // populated before any consumer of strategy.GetRegistry runs. - _ "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy/registrations" deployapi "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" mcmsreaderapi "github.com/smartcontractkit/chainlink-ccip/deployment/utils/changesets" diff --git a/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go b/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go index 274004f08b..76872009bf 100644 --- a/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go +++ b/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go @@ -8,7 +8,7 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/tokenimpl" datastore_utils_evm "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/datastore" tarops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/token_admin_registry" tarseq "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/sequences" @@ -259,6 +259,8 @@ func (a *EVMPoolAdapter) DeployTokenPoolForToken() *cldf_ops.Sequence[tokensapi. a.Ops.Version(), "Deploy a token pool for a token on an EVM chain", func(b cldf_ops.Bundle, chains cldf_chain.BlockChains, input tokensapi.DeployTokenPoolInput) (sequences.OnChainOutput, error) { + var writes []evm_contract.WriteOutput + if a.DeployTokenPoolSeq == nil { return sequences.OnChainOutput{}, errors.New("DeployTokenPoolSeq is not set on EVMPoolAdapter") } @@ -285,94 +287,57 @@ func (a *EVMPoolAdapter) DeployTokenPoolForToken() *cldf_ops.Sequence[tokensapi. if input.TokenRef.Type != "" { toknFilterDS.Type = input.TokenRef.Type } + toknRef, err := datastore_utils.FindAndFormatRef(input.ExistingDataStore, toknFilterDS, input.ChainSelector, datastore_utils.FullRef) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to find token address for symbol %q on chain %d: %w", input.TokenRef.Qualifier, input.ChainSelector, err) } - - if input.RateLimitAdmin != "" && len(out.Output.Addresses) >= 1 { - poolBytes, err := a.AddressRefToBytes(out.Output.Addresses[0]) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to convert token pool address ref to bytes: %w", err) - } - poolAddr := common.BytesToAddress(poolBytes) - if poolAddr == (common.Address{}) { - return sequences.OnChainOutput{}, errors.New("deployed token pool address is zero address") - } - - rlAdminHex := input.RateLimitAdmin - if !common.IsHexAddress(rlAdminHex) { - return sequences.OnChainOutput{}, fmt.Errorf("rate limit admin address %q is not a valid hex address", input.RateLimitAdmin) - } - rlAdminAddr := common.HexToAddress(rlAdminHex) - if rlAdminAddr == (common.Address{}) { - return sequences.OnChainOutput{}, errors.New("rate limit admin address cannot be the zero address") - } - - output, err := a.Ops.SetRateLimitAdmin(b, chain, poolAddr, rlAdminAddr) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to set rate limit admin: %w", err) - } - batchOp, err := evm_contract.NewBatchOperationFromWrites([]evm_contract.WriteOutput{output}) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to create batch operation: %w", err) - } - result.BatchOps = append(result.BatchOps, batchOp) + toknAddr, err := datastore_utils_evm.ToEVMAddress(toknRef) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to convert token ref to EVM address for chain %d: %w", input.ChainSelector, err) + } + if toknAddr == (common.Address{}) { + return sequences.OnChainOutput{}, fmt.Errorf("token address for symbol %q is zero address", input.TokenRef.Qualifier) } - isPoolTypeBnM := input.PoolType == cciputils.BurnMintTokenPool.String() - if isPoolTypeBnM && len(out.Output.Addresses) >= 1 { - poolRef := out.Output.Addresses[0] - - poolAddrBytes, addrErr := a.AddressRefToBytes(poolRef) - if addrErr != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to convert deployed token pool address ref to bytes: %w", addrErr) - } - toknAddrBytes, addrErr := a.AddressRefToBytes(toknRef) - if addrErr != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to convert token address ref to bytes: %w", addrErr) - } - - poolAddr := common.BytesToAddress(poolAddrBytes) - if poolAddr == (common.Address{}) { - return sequences.OnChainOutput{}, errors.New("deployed token pool address is zero address") - } - toknAddr := common.BytesToAddress(toknAddrBytes) - if toknAddr == (common.Address{}) { - return sequences.OnChainOutput{}, fmt.Errorf("token address for symbol %q is zero address", input.TokenRef.Qualifier) - } + var poolRef datastore.AddressRef + if len(out.Output.Addresses) >= 1 { + poolRef = out.Output.Addresses[0] + } - var writes []evm_contract.WriteOutput - strat, ok := strategy.GetRegistry().GetEVM(deployment.ContractType(toknRef.Type)) - if !ok || !strat.Capabilities().ParticipatesInPoolRoleGrant { - // Pass through for unknown / non-participating token types; we don't want - // to block pool deployment, but log a warning since an unknown type likely - // indicates a missing strategy registration. - b.Logger.Warnf( - "token type %q has no pool role grant strategy registered, skipping grant for token pool %q on token %q on chain %d", - toknRef.Type.String(), poolAddr.Hex(), input.TokenRef.Qualifier, input.ChainSelector, - ) + if !datastore_utils.IsAddressRefEmpty(poolRef) { + if tokenPoolRolesWrites, err := tidyTokenPoolRoles(b, chain, input, poolRef, toknRef); err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to tidy token pool roles: %w", err) } else { - grantWrites, grantErr := strat.GrantPoolRoles(b, chain, toknAddr, poolAddr, input.ChainSelector) - if grantErr != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to grant pool roles for token type %q (token %q, pool %q) on chain %d: %w", toknRef.Type, input.TokenRef.Qualifier, poolAddr.Hex(), input.ChainSelector, grantErr) - } - writes = append(writes, grantWrites...) + writes = append(writes, tokenPoolRolesWrites...) } - - if len(writes) > 0 { - batchOp, bErr := evm_contract.NewBatchOperationFromWrites(writes) - if bErr != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to create batch operation for granting mint and burn roles: %w", bErr) + if input.RateLimitAdmin != "" { + rlAdminHex := input.RateLimitAdmin + if !common.IsHexAddress(rlAdminHex) { + return sequences.OnChainOutput{}, fmt.Errorf("rate limit admin address %q is not a valid hex address", input.RateLimitAdmin) + } + rlAdminAddr := common.HexToAddress(rlAdminHex) + if rlAdminAddr == (common.Address{}) { + return sequences.OnChainOutput{}, errors.New("rate limit admin address cannot be the zero address") + } + poolAddr, err := datastore_utils_evm.ToEVMAddress(poolRef) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to convert token pool ref to EVM address for chain %d: %w", input.ChainSelector, err) } - result.BatchOps = append(result.BatchOps, batchOp) + output, err := a.Ops.SetRateLimitAdmin(b, chain, poolAddr, rlAdminAddr) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to set rate limit admin: %w", err) + } + writes = append(writes, output) } } - writes, err := tidyTokenRoles(b, chain, input, toknRef) - if err != nil { + if tokenRolesWrites, err := tidyTokenRoles(b, chain, input, toknRef); err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to tidy token roles: %w", err) + } else { + writes = append(writes, tokenRolesWrites...) } + if len(writes) > 0 { batchOp, bErr := evm_contract.NewBatchOperationFromWrites(writes) if bErr != nil { @@ -386,20 +351,93 @@ func (a *EVMPoolAdapter) DeployTokenPoolForToken() *cldf_ops.Sequence[tokensapi. ) } +// tidyTokenPoolRoles grants a token pool the token-side roles required for its +// pool type. Burn/mint pools delegate role selection to the registered token +// strategy because token contracts expose different role APIs. +func tidyTokenPoolRoles( + b cldf_ops.Bundle, + chain evm.Chain, + input tokensapi.DeployTokenPoolInput, + poolRef datastore.AddressRef, + tokenRef datastore.AddressRef, +) ([]evm_contract.WriteOutput, error) { + tokenAddr, err := datastore_utils_evm.ToEVMAddress(tokenRef) + if err != nil { + return nil, fmt.Errorf("failed to convert token ref to EVM address for chain %d: %w", input.ChainSelector, err) + } + poolAddress, err := datastore_utils_evm.ToEVMAddress(poolRef) + if err != nil { + return nil, fmt.Errorf("failed to convert token pool ref to EVM address for chain %d: %w", input.ChainSelector, err) + } + + if input.PoolType == cciputils.BurnMintTokenPool.String() { + tokenImpl, ok := tokenimpl.Get(deployment.ContractType(tokenRef.Type)) + if !ok { + b.Logger.Warnf( + "unsupported token type %q for token at ref (%s); skipping admin role tidy for this token on chain %d", + tokenRef.Type.String(), datastore_utils.SprintRef(tokenRef), input.ChainSelector, + ) + return nil, nil + } + + tokenCaps := tokenImpl.Capabilities() + if !tokenCaps.ParticipatesInPoolRoleGrant { + b.Logger.Warnf( + "token type %q has no pool role grant strategy registered, skipping grant for token pool %q on token %q on chain %d", + tokenRef.Type.String(), poolAddress.Hex(), input.TokenRef.Qualifier, input.ChainSelector, + ) + return nil, nil + } + + if grantWrites, grantErr := tokenImpl.GrantPoolRoles(b, chain, tokenAddr, poolAddress); grantErr != nil { + return nil, fmt.Errorf("failed to grant pool roles for token type %q (token %q, pool %q) on chain %d: %w", tokenRef.Type, input.TokenRef.Qualifier, poolAddress.Hex(), input.ChainSelector, grantErr) + } else { + return grantWrites, nil + } + } + + b.Logger.Warnf( + "pool with ref (%s) was not configured with any roles", + datastore_utils.SprintRef(poolRef), + ) + + return nil, nil +} + // tidyTokenRoles will grant timelock admin rights on the token and remove // the deployer EOA as an admin. If timelock is not found in the datastore // (i.e. not deployed/not applicable which can be the case in test cases), // then it leaves the deployer account as an admin so the token isn't left // without an operator. -// -// TODO: we should refactor this such that we follow a token-type adapter -// pattern thereby avoiding this switch statement altogether. func tidyTokenRoles( b cldf_ops.Bundle, chain evm.Chain, input tokensapi.DeployTokenPoolInput, tokenRef datastore.AddressRef, ) ([]evm_contract.WriteOutput, error) { + tokenAddr, err := datastore_utils_evm.ToEVMAddress(tokenRef) + if err != nil { + return nil, fmt.Errorf("failed to convert token ref to EVM address for chain %d: %w", input.ChainSelector, err) + } + + tokenImpl, ok := tokenimpl.Get(deployment.ContractType(tokenRef.Type)) + if !ok { + b.Logger.Warnf( + "unsupported token type %q for token at ref (%s); skipping admin role tidy for this token on chain %d", + tokenRef.Type.String(), datastore_utils.SprintRef(tokenRef), input.ChainSelector, + ) + return nil, nil + } + + tokenCaps := tokenImpl.Capabilities() + if !tokenCaps.SupportsAdminRole { + b.Logger.Warnf( + "token type %q does not support admin role management; skipping tidy of token admin roles for token at ref (%s) on chain %d", + tokenRef.Type.String(), datastore_utils.SprintRef(tokenRef), input.ChainSelector, + ) + return nil, nil + } + timelockRef := datastore_utils.GetAddressRef( input.ExistingDataStore.Addresses().Filter(), input.ChainSelector, @@ -411,84 +449,20 @@ func tidyTokenRoles( b.Logger.Infof("CLL timelock not found for chain %d; keeping deployer as token admin", input.ChainSelector) return nil, nil } - timelockAddr, err := datastore_utils_evm.ToEVMAddress(timelockRef) if err != nil { return nil, fmt.Errorf("failed to convert timelock ref to EVM address for chain %d: %w", input.ChainSelector, err) } - - tokenAddr, err := datastore_utils_evm.ToEVMAddress(tokenRef) + grantWrites, err := tokenImpl.GrantAdminRole(b, chain, tokenAddr, timelockAddr) if err != nil { - return nil, fmt.Errorf("failed to convert token ref to EVM address for chain %d: %w", input.ChainSelector, err) + return nil, fmt.Errorf("failed to grant deployer admin role for token %q on chain %d: %w", tokenAddr.Hex(), input.ChainSelector, err) } - - switch tokenRef.Type.String() { - - // BnM ERC-20 - case bnmDripERC20ops.ContractType.String(), bnmERC20ops.ContractType.String(), bnmDripOps150.ContractType.String(): - defaultAdminRole, err := cldf_ops.ExecuteOperation(b, bnmERC20ops.GetDefaultAdminRole, chain, evm_contract.FunctionInput[struct{}]{ - ChainSelector: input.ChainSelector, - Address: tokenAddr, - Args: struct{}{}, - }) - if err != nil { - return nil, fmt.Errorf("failed to get default admin role for token %q on chain %d: %w", tokenAddr.Hex(), input.ChainSelector, err) - } - grantReport, err := cldf_ops.ExecuteOperation(b, bnmERC20ops.GrantAdminRole, chain, evm_contract.FunctionInput[bnmERC20ops.RoleAssignment]{ - ChainSelector: input.ChainSelector, - Address: tokenAddr, - Args: bnmERC20ops.RoleAssignment{ - Role: defaultAdminRole.Output, - To: timelockAddr, - }, - }) - if err != nil { - return nil, fmt.Errorf("failed to grant default admin role to timelock %q for token %q on chain %d: %w", timelockAddr.Hex(), tokenAddr.Hex(), input.ChainSelector, err) - } - revokeReport, err := cldf_ops.ExecuteOperation(b, bnmERC20ops.RevokeAdminRole, chain, evm_contract.FunctionInput[bnmERC20ops.RoleAssignment]{ - ChainSelector: input.ChainSelector, - Address: tokenAddr, - Args: bnmERC20ops.RoleAssignment{ - Role: defaultAdminRole.Output, - To: chain.DeployerKey.From, - }, - }) - if err != nil { - return nil, fmt.Errorf("failed to revoke default admin role from deployer %q for token %q on chain %d: %w", chain.DeployerKey.From.Hex(), tokenAddr.Hex(), input.ChainSelector, err) - } - return []evm_contract.WriteOutput{grantReport.Output, revokeReport.Output}, nil - - // TIP-20 - case tip20ops.ContractType.String(): - grantReport, err := cldf_ops.ExecuteOperation(b, tip20ops.GrantAdminRole, chain, evm_contract.FunctionInput[common.Address]{ - ChainSelector: input.ChainSelector, - Address: tokenAddr, - Args: timelockAddr, - }) - if err != nil { - return nil, fmt.Errorf("failed to grant TIP-20 default admin role to timelock %q for token %q on chain %d: %w", timelockAddr.Hex(), tokenAddr.Hex(), input.ChainSelector, err) - } - revokeReport, err := cldf_ops.ExecuteOperation(b, tip20ops.RevokeAdminRole, chain, evm_contract.FunctionInput[common.Address]{ - ChainSelector: input.ChainSelector, - Address: tokenAddr, - Args: chain.DeployerKey.From, - }) - if err != nil { - return nil, fmt.Errorf("failed to revoke TIP-20 default admin role from deployer %q for token %q on chain %d: %w", chain.DeployerKey.From.Hex(), tokenAddr.Hex(), input.ChainSelector, err) - } - return []evm_contract.WriteOutput{grantReport.Output, revokeReport.Output}, nil - + revokeWrites, err := tokenImpl.RevokeAdminRole(b, chain, tokenAddr, chain.DeployerKey.From) + if err != nil { + return nil, fmt.Errorf("failed to revoke deployer admin role for token %q on chain %d: %w", tokenAddr.Hex(), input.ChainSelector, err) } - b.Logger.Warnf( - "unsupported token type %q for token %q on chain %d; timelock %q is present but admin-role hardening was not applied and deployer may remain token admin", - tokenRef.Type.String(), - tokenAddr.Hex(), - input.ChainSelector, - timelockAddr.Hex(), - ) - - return nil, nil + return append(grantWrites, revokeWrites...), nil } // GetTokenAdminRegistryAddress looks up the TAR (v1.5.0) address from the datastore. diff --git a/chains/evm/deployment/v1_0_0/operations/tip20/tip20_contract.go b/chains/evm/deployment/v1_0_0/operations/tip20/tip20_contract.go index b8c8d88873..691d94d228 100644 --- a/chains/evm/deployment/v1_0_0/operations/tip20/tip20_contract.go +++ b/chains/evm/deployment/v1_0_0/operations/tip20/tip20_contract.go @@ -1,6 +1,7 @@ package tip20 import ( + "math/big" "strings" "github.com/ethereum/go-ethereum/accounts/abi" @@ -9,11 +10,11 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) -// TIP20TokenABI is a minimal ABI for TIP-20 token role management (ITIP20 ISSUER_ROLE + ITIP20RolesAuth). +// TIP20TokenABI is a minimal ABI for TIP-20 token role management and transfers (ITIP20 ISSUER_ROLE + ITIP20RolesAuth). // Role layout follows TIP20RolesAuth: https://github.com/tempoxyz/tempo/blob/main/tips/ref-impls/src/abstracts/TIP20RolesAuth.sol // hasRole is a public mapping, so the getter is hasRole(address,bytes32), not OpenZeppelin AccessControl order. // ISSUER_ROLE: https://github.com/tempoxyz/tempo-std/blob/master/src/interfaces/ITIP20.sol -const TIP20TokenABI = `[{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32"},{"name":"account","type":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32"},{"name":"account","type":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"account","type":"address"},{"name":"role","type":"bytes32"}],"outputs":[{"name":"","type":"bool"}],"stateMutability":"view"},{"type":"function","name":"ISSUER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view"}]` +const TIP20TokenABI = `[{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32"},{"name":"account","type":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32"},{"name":"account","type":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"account","type":"address"},{"name":"role","type":"bytes32"}],"outputs":[{"name":"","type":"bool"}],"stateMutability":"view"},{"type":"function","name":"ISSUER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"transfer","inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable"}]` // DefaultAdminRole is TIP20RolesAuth.DEFAULT_ADMIN_ROLE (bytes32(0)). var DefaultAdminRole [32]byte @@ -47,6 +48,10 @@ func (t *TIP20Token) RevokeRole(opts *bind.TransactOpts, role [32]byte, account return t.contract.Transact(opts, "revokeRole", role, account) } +func (t *TIP20Token) Transfer(opts *bind.TransactOpts, to common.Address, amount *big.Int) (*types.Transaction, error) { + return t.contract.Transact(opts, "transfer", to, amount) +} + func (t *TIP20Token) HasRole(opts *bind.CallOpts, account common.Address, role [32]byte) (bool, error) { var out []any err := t.contract.Call(opts, &out, "hasRole", account, role) diff --git a/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops.go b/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops.go index edd7ba011c..e61e4da9ac 100644 --- a/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops.go +++ b/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops.go @@ -3,6 +3,7 @@ package tip20 import ( "errors" "fmt" + "math/big" "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -51,6 +52,11 @@ type FactoryDeployArgs struct { Salt [32]byte // Optional salt for deterministic deployment. Defaults to a random salt if not provided. } +type TransferArgs struct { + Receiver common.Address + Amount *big.Int +} + // Deploy deploys the TIP20 token contract with the provided deploy arguments. The TIP20 token is ERC20 compliant and includes additional // features as defined in the TIP20 standard: https://www.mintlify.com/tempoxyz/tempo/protocol/tip20/overview#erc-20-compatibility. This // sequence is only applicable for Tempo testnet / mainnet. The token is deployed via the factory contract as recommended in the docs. We @@ -162,6 +168,28 @@ var Deploy = operations.NewSequence( }, ) +var Transfer = contract.NewWrite(contract.WriteParams[TransferArgs, *TIP20Token]{ + Name: "tip20:transfer", + Version: Version, + Description: "Transfer TIP-20 tokens to a specified address", + ContractType: ContractType, + ContractABI: TIP20TokenABI, + NewContract: NewTIP20Token, + IsAllowedCaller: contract.AllCallersAllowed[*TIP20Token, TransferArgs], + Validate: func(args TransferArgs) error { + if args.Amount == nil || args.Amount.Cmp(big.NewInt(0)) <= 0 { + return errors.New("amount must be greater than 0") + } + if args.Receiver == (common.Address{}) { + return errors.New("receiver address is required") + } + return nil + }, + CallContract: func(token *TIP20Token, opts *bind.TransactOpts, args TransferArgs) (*types.Transaction, error) { + return token.Transfer(opts, args.Receiver, args.Amount) + }, +}) + var GrantIssuerRole = contract.NewWrite(contract.WriteParams[common.Address, *TIP20Token]{ Name: "tip20:grant-issuer-role", Version: Version, diff --git a/chains/evm/deployment/v1_0_0/sequences/token.go b/chains/evm/deployment/v1_0_0/sequences/token.go index 3b0061977a..3d3dc24691 100644 --- a/chains/evm/deployment/v1_0_0/sequences/token.go +++ b/chains/evm/deployment/v1_0_0/sequences/token.go @@ -1,7 +1,6 @@ package sequences import ( - "errors" "fmt" "math/big" @@ -9,13 +8,8 @@ import ( mcms_types "github.com/smartcontractkit/mcms/types" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" - // Defensive blank import: ensures all known EVM token strategies are - // registered when this package is imported directly (e.g. by tests) - // rather than transitively via an adapter package. - _ "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy/registrations" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/tokenimpl" + datastore_utils_evm "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/datastore" tokenapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" @@ -30,7 +24,10 @@ var DeployToken = cldf_ops.NewSequence( common_utils.Version_1_0_0, "Deploy given type of token contracts", func(b cldf_ops.Bundle, chains cldf_chain.BlockChains, input tokenapi.DeployTokenInput) (sequences.OnChainOutput, error) { - chain := chains.EVMChains()[input.ChainSelector] + chain, ok := chains.EVMChains()[input.ChainSelector] + if !ok { + return sequences.OnChainOutput{}, fmt.Errorf("chain with selector %d not found among provided chains", input.ChainSelector) + } preMint := big.NewInt(0) if input.PreMint != nil { @@ -51,64 +48,55 @@ var DeployToken = cldf_ops.NewSequence( ccipAdmin = common.HexToAddress(input.CCIPAdmin) } - strat, ok := strategy.GetRegistry().GetEVM(input.Type) + tokenImpl, ok := tokenimpl.Get(input.Type) if !ok { return sequences.OnChainOutput{}, fmt.Errorf("unsupported token type: %s", input.Type) } - caps := strat.Capabilities() - - tokenRef, deployWrites, err := strat.Deploy(b, chain, input) + tokenRefr, deployWrites, err := tokenImpl.Deploy(b, chain, input) if err != nil { return sequences.OnChainOutput{}, err } + tokenAddr, err := datastore_utils_evm.ToEVMAddress(tokenRefr) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("invalid token address reference: %w", err) + } - addresses := []datastore.AddressRef{tokenRef} - writes := append([]contract.WriteOutput(nil), deployWrites...) - tokenAddr := common.HexToAddress(tokenRef.Address) - - if caps.SupportsPreMint && preMint.Cmp(big.NewInt(0)) > 0 && len(input.Senders) > 0 { - firstSender := input.Senders[0] - if !common.IsHexAddress(firstSender) { - return sequences.OnChainOutput{}, fmt.Errorf("invalid sender address: %s", firstSender) + caps := tokenImpl.Capabilities() + recv := common.Address{} + if len(input.Senders) >= 1 && preMint.Cmp(big.NewInt(0)) > 0 && caps.SupportsPreMint { + address := input.Senders[0] + if !common.IsHexAddress(address) { + return sequences.OnChainOutput{}, fmt.Errorf("invalid pre-mint recipient address: %s", address) } - tokReceiver := common.HexToAddress(firstSender) - if tokReceiver == (common.Address{}) { - return sequences.OnChainOutput{}, errors.New("refusing to transfer pre-minted tokens to the zero address") + recv = common.HexToAddress(address) + if recv == (common.Address{}) { + return sequences.OnChainOutput{}, fmt.Errorf("pre-mint recipient address cannot be the zero address") } - if len(input.Senders) > 1 { - b.Logger.Warnf("Multiple senders provided but only the first one (%s) will receive the pre-minted tokens", tokReceiver.Hex()) - } - transferReport, err := cldf_ops.ExecuteOperation(b, erc20.Transfer, chain, contract.FunctionInput[erc20.TransferArgs]{ - ChainSelector: chain.Selector, - Address: tokenAddr, - Args: erc20.TransferArgs{ - Receiver: tokReceiver, - Amount: preMint, - }, - }) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to transfer pre-minted tokens to sender %s: %w", tokReceiver.Hex(), err) + if len(input.Senders) != 1 { + b.Logger.Warnf("Multiple sender addresses provided, but adapter only supports one. Only the first address will receive the tokens: %s", address) } - writes = append(writes, transferReport.Output) } - if input.CCIPAdmin != "" && caps.SupportsCCIPAdmin { - setCCIPAdminReport, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.SetCCIPAdmin, chain, contract.FunctionInput[string]{ - ChainSelector: chain.Selector, - Address: tokenAddr, - Args: ccipAdmin.Hex(), - }) + writes := append([]contract.WriteOutput{}, deployWrites...) + if recv != (common.Address{}) && caps.SupportsPreMint { + transferWrites, err := tokenImpl.Transfer(b, chain, tokenAddr, recv, preMint) if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to set CCIP admin: %w", err) + return sequences.OnChainOutput{}, fmt.Errorf("failed to mint pre-mint tokens: %w", err) } - writes = append(writes, setCCIPAdminReport.Output) + writes = append(writes, transferWrites...) } - if input.ExternalAdmin != "" && caps.SupportsAdminRole { - adminWrites, err := strat.GrantExternalAdmin(b, chain, tokenAddr, externalAdmin, chain.Selector) + grantWrites, err := tokenImpl.GrantAdminRole(b, chain, tokenAddr, externalAdmin) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to grant admin role to %s: %w", input.ExternalAdmin, err) } + writes = append(writes, grantWrites...) + } + if input.CCIPAdmin != "" && caps.SupportsCCIPAdmin { + adminWrites, err := tokenImpl.SetCCIPAdmin(b, chain, tokenAddr, ccipAdmin) + if err != nil { + return sequences.OnChainOutput{}, fmt.Errorf("failed to set CCIP admin: %w", err) + } writes = append(writes, adminWrites...) } @@ -118,7 +106,7 @@ var DeployToken = cldf_ops.NewSequence( } return sequences.OnChainOutput{ - Addresses: addresses, + Addresses: []datastore.AddressRef{tokenRefr}, BatchOps: []mcms_types.BatchOperation{batchOp}, }, nil }, diff --git a/chains/evm/deployment/v1_0_0/sequences/token_test.go b/chains/evm/deployment/v1_0_0/sequences/token_test.go index 7d72f0ea39..55b4007ac4 100644 --- a/chains/evm/deployment/v1_0_0/sequences/token_test.go +++ b/chains/evm/deployment/v1_0_0/sequences/token_test.go @@ -14,11 +14,11 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/strategy" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/tokens/tokenimpl" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" bnm_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc20" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" @@ -72,16 +72,12 @@ func TestEVMTokenDeployments(t *testing.T) { requiresSupply: true, }, { - name: "BurnMintERC20WithDripToken", - tokenType: burn_mint_erc20_with_drip.ContractType, - tokenName: "Test BurnMint ERC20 With Drip", - tokenSymbol: "TBMDRIP", - decimals: 18, - ccipAdmin: "0x1111111111111111111111111111111111111111", - sender: "", - supply: &maxSupply, - preMint: &preMint, - requiresSupply: true, + name: "BurnMintERC20WithDrip", + tokenType: burn_mint_erc20_with_drip.ContractType, + tokenName: "Test BurnMint ERC20 With Drip", + tokenSymbol: "TBMDRIP", + decimals: 18, + ccipAdmin: "0x1111111111111111111111111111111111111111", }, } @@ -197,7 +193,7 @@ func TestEVMTokenDeployments(t *testing.T) { } } - tokenSupportsAdmin := strategy.GetRegistry().CapabilitiesEVM(tc.tokenType).SupportsAdminRole + tokenSupportsAdmin := tokenimpl.Capabilities(tc.tokenType).SupportsAdminRole if tokenSupportsAdmin { // Verify CCIP Admin was set correctly t.Log(" Verifying CCIP Admin...") @@ -236,11 +232,14 @@ func TestEVMTokenDeployments(t *testing.T) { func TestTokenSupportsAdminRole(t *testing.T) { t.Parallel() - caps := func(ct cldf.ContractType) bool { - return strategy.GetRegistry().CapabilitiesEVM(ct).SupportsAdminRole + tokenTypes := map[cldf.ContractType]bool{ + burn_mint_erc20_with_drip.ContractType: true, + burn_mint_erc20.ContractType: true, + tip20.ContractType: true, + erc20.ContractType: false, + } + + for tt, supportsAdmin := range tokenTypes { + require.Equal(t, supportsAdmin, tokenimpl.Capabilities(tt).SupportsAdminRole, "Token type %s admin role support mismatch", tt) } - require.True(t, caps(burn_mint_erc20.ContractType)) - require.True(t, caps(burn_mint_erc20_with_drip.ContractType)) - require.True(t, caps(tip20.ContractType)) - require.False(t, caps(erc20.ContractType)) } From 6844264a8d392d03d922c9ecbca34895f18928d3 Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Sun, 3 May 2026 15:52:26 -0700 Subject: [PATCH 07/14] chore: clean up --- .../deployment/tokens/tokenimpl/helpers.go | 29 +++++++++---------- .../evm/deployment/tokens/tokenimpl/impl.go | 15 ---------- .../evm/deployment/tokens/tokenimpl/lookup.go | 3 +- .../tokens/tokenimpl/token_burn_mint_erc20.go | 8 ++--- .../token_burn_mint_erc20_with_drip.go | 8 ++--- .../tokens/tokenimpl/token_erc20.go | 12 ++++---- .../tokens/tokenimpl/token_tip20.go | 13 ++++----- .../deployment/v1_0_0/sequences/token_test.go | 4 +-- 8 files changed, 37 insertions(+), 55 deletions(-) diff --git a/chains/evm/deployment/tokens/tokenimpl/helpers.go b/chains/evm/deployment/tokens/tokenimpl/helpers.go index 869f0af190..be6cff75b7 100644 --- a/chains/evm/deployment/tokens/tokenimpl/helpers.go +++ b/chains/evm/deployment/tokens/tokenimpl/helpers.go @@ -15,7 +15,7 @@ import ( bnm_erc20_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc20" ) -func revokeBnMDefaultAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { +func revokeDefaultAdminRoleBurnMintERC20(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { tokenContract, err := bnm_erc20_bindings.NewBurnMintERC20(token, chain.Client) if err != nil { return nil, fmt.Errorf("failed to instantiate BurnMintERC20 contract: %w", err) @@ -38,7 +38,7 @@ func revokeBnMDefaultAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user c return []contract.WriteOutput{report.Output}, nil } -func grantBnMDefaultAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { +func grantDefaultAdminRoleBurnMintERC20(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { tokenContract, err := bnm_erc20_bindings.NewBurnMintERC20(token, chain.Client) if err != nil { return nil, fmt.Errorf("failed to instantiate BurnMintERC20 contract: %w", err) @@ -61,7 +61,7 @@ func grantBnMDefaultAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user co return []contract.WriteOutput{report.Output}, nil } -func grantBnMMintAndBurnRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { +func grantMintAndBurnRolesBurnMintERC20(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.GrantMintAndBurnRoles, chain, contract.FunctionInput[common.Address]{ ChainSelector: chain.Selector, Address: token, @@ -73,30 +73,29 @@ func grantBnMMintAndBurnRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool co return []contract.WriteOutput{report.Output}, nil } -func transferTokensERC20(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, erc20.Transfer, chain, contract.FunctionInput[erc20.TransferArgs]{ +func setCCIPAdminBurnMintERC20(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.SetCCIPAdmin, chain, contract.FunctionInput[string]{ ChainSelector: chain.Selector, Address: token, - Args: erc20.TransferArgs{ - Amount: scaledAmount, - Receiver: to, - }, + Args: ccipAdmin.Hex(), }) if err != nil { - return nil, fmt.Errorf("failed to transfer ERC20 tokens: %w", err) + return nil, fmt.Errorf("failed to set CCIP admin: %w", err) } - return []contract.WriteOutput{report.Output}, nil } -func setBnMCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, burn_mint_erc20.SetCCIPAdmin, chain, contract.FunctionInput[string]{ +func transferTokensERC20(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { + report, err := cldf_ops.ExecuteOperation(b, erc20.Transfer, chain, contract.FunctionInput[erc20.TransferArgs]{ ChainSelector: chain.Selector, Address: token, - Args: ccipAdmin.Hex(), + Args: erc20.TransferArgs{ + Amount: scaledAmount, + Receiver: to, + }, }) if err != nil { - return nil, fmt.Errorf("failed to set CCIP admin: %w", err) + return nil, fmt.Errorf("failed to transfer ERC20 tokens: %w", err) } return []contract.WriteOutput{report.Output}, nil } diff --git a/chains/evm/deployment/tokens/tokenimpl/impl.go b/chains/evm/deployment/tokens/tokenimpl/impl.go index 438b6dc94b..cb5981d579 100644 --- a/chains/evm/deployment/tokens/tokenimpl/impl.go +++ b/chains/evm/deployment/tokens/tokenimpl/impl.go @@ -1,13 +1,3 @@ -// Package tokenimpl provides per-token-contract-type behaviors for EVM -// token deployment and pool wiring. A token implementation encapsulates everything -// specific to one token contract type (e.g. BurnMintERC20, TIP-20): -// how to deploy the token, how to grant pool roles after a pool is -// deployed, how to grant an external admin role, and which capabilities -// the token contract supports. -// -// Token implementations are looked up by ContractType through the EVM tokenimpl package -// and are independent of pool version, so adding a new token type makes it -// available to every pool-version adapter that consults the token implementation lookup. package tokenimpl import ( @@ -26,11 +16,6 @@ import ( // CapabilitySet reports the optional flow steps a token contract type // participates in. The orchestrating sequence reads these flags to // decide whether to invoke the corresponding step. -// -// ParticipatesInPoolRoleGrant is declared explicitly rather than inferred -// from a no-op GrantPoolRoles return so that intentional non-participation -// (e.g. plain ERC20) is distinguishable from a strategy bug that returned -// no writes by accident. type CapabilitySet struct { // ParticipatesInPoolRoleGrant is true when the token requires token-side // role grants for the pool to operate; GrantPoolRoles must emit those writes. diff --git a/chains/evm/deployment/tokens/tokenimpl/lookup.go b/chains/evm/deployment/tokens/tokenimpl/lookup.go index 48920e8b14..b180ef2cff 100644 --- a/chains/evm/deployment/tokens/tokenimpl/lookup.go +++ b/chains/evm/deployment/tokens/tokenimpl/lookup.go @@ -21,8 +21,7 @@ func Get(ct deployment.ContractType) (Token, bool) { return s, ok } -// Capabilities returns the Capabilities for an EVM token contract type, or the -// zero value if no strategy exists. +// Capabilities returns the capability set for an EVM token contract type, or the zero value if the token implementation does not exist. func Capabilities(ct deployment.ContractType) CapabilitySet { if s, ok := Get(ct); ok { return s.Capabilities() diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go index cddae2306f..c7abbcc6a8 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go @@ -32,19 +32,19 @@ func (tokenBurnMintERC20) Capabilities() CapabilitySet { } func (tokenBurnMintERC20) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { - return revokeBnMDefaultAdminRole(b, chain, token, user) + return revokeDefaultAdminRoleBurnMintERC20(b, chain, token, user) } func (tokenBurnMintERC20) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { - return grantBnMDefaultAdminRole(b, chain, token, externalAdmin) + return grantDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } func (tokenBurnMintERC20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { - return grantBnMMintAndBurnRoles(b, chain, token, pool) + return grantMintAndBurnRolesBurnMintERC20(b, chain, token, pool) } func (tokenBurnMintERC20) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { - return setBnMCCIPAdmin(b, chain, token, ccipAdmin) + return setCCIPAdminBurnMintERC20(b, chain, token, ccipAdmin) } func (tokenBurnMintERC20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go index 1e17c734af..3466dd2aa5 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go @@ -31,19 +31,19 @@ func (tokenBurnMintERC20WithDrip) Capabilities() CapabilitySet { } func (tokenBurnMintERC20WithDrip) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { - return revokeBnMDefaultAdminRole(b, chain, token, externalAdmin) + return revokeDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } func (tokenBurnMintERC20WithDrip) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { - return grantBnMDefaultAdminRole(b, chain, token, externalAdmin) + return grantDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } func (tokenBurnMintERC20WithDrip) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { - return grantBnMMintAndBurnRoles(b, chain, token, pool) + return grantMintAndBurnRolesBurnMintERC20(b, chain, token, pool) } func (tokenBurnMintERC20WithDrip) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { - return setBnMCCIPAdmin(b, chain, token, ccipAdmin) + return setCCIPAdminBurnMintERC20(b, chain, token, ccipAdmin) } func (tokenBurnMintERC20WithDrip) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { diff --git a/chains/evm/deployment/tokens/tokenimpl/token_erc20.go b/chains/evm/deployment/tokens/tokenimpl/token_erc20.go index d01449cafd..cc298ff453 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_erc20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_erc20.go @@ -16,10 +16,11 @@ import ( cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) -// tokenERC20 is the plain (non-CCIP-aware) ERC20 strategy. type tokenERC20 struct{} -func (tokenERC20) ContractType() deployment.ContractType { return erc20.ContractType } +func (tokenERC20) ContractType() deployment.ContractType { + return erc20.ContractType +} func (tokenERC20) Capabilities() CapabilitySet { return CapabilitySet{ @@ -35,15 +36,15 @@ func (tokenERC20) RevokeAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Ad } func (tokenERC20) GrantAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { - return nil, fmt.Errorf("admin role granting not supported for plain ERC20 strategy") + return nil, fmt.Errorf("admin role granting not supported for plain ERC20 token") } func (tokenERC20) GrantPoolRoles(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { - return nil, fmt.Errorf("pool role granting not supported for plain ERC20 strategy") + return nil, fmt.Errorf("pool role granting not supported for plain ERC20 token") } func (tokenERC20) SetCCIPAdmin(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { - return nil, fmt.Errorf("CCIP admin role not supported for plain ERC20 strategy") + return nil, fmt.Errorf("CCIP admin role not supported for plain ERC20 token") } func (tokenERC20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { @@ -66,5 +67,6 @@ func (tokenERC20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.Deploy if err != nil { return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy ERC20 token: %w", err) } + return ref, nil, nil } diff --git a/chains/evm/deployment/tokens/tokenimpl/token_tip20.go b/chains/evm/deployment/tokens/tokenimpl/token_tip20.go index cafe8521f9..3e1d9c7e98 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_tip20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_tip20.go @@ -16,12 +16,11 @@ import ( cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) -// tokenTIP20 is the Tempo-only TIP-20 token strategy. Deployed via a -// factory sequence rather than MaybeDeployContract; CCIPAdmin and pre-mint -// do not apply. type tokenTIP20 struct{} -func (tokenTIP20) ContractType() deployment.ContractType { return tip20.ContractType } +func (tokenTIP20) ContractType() deployment.ContractType { + return tip20.ContractType +} func (tokenTIP20) Capabilities() CapabilitySet { return CapabilitySet{ @@ -69,7 +68,7 @@ func (tokenTIP20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool } func (tokenTIP20) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { - return nil, fmt.Errorf("CCIP admin role not supported for TIP-20 strategy") + return nil, fmt.Errorf("CCIP admin role not supported for TIP-20 tokens") } func (tokenTIP20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { @@ -89,9 +88,6 @@ func (tokenTIP20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common. } func (tokenTIP20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - // Initial admin must be the deployer so subsequent ops (e.g. GrantIssuerRole) - // pass IsAllowedCaller; ExternalAdmin receives DEFAULT_ADMIN_ROLE in a - // follow-up grant performed by the orchestrating sequence. report, err := cldf_ops.ExecuteSequence(b, tip20.Deploy, chain, tip20.FactoryDeployArgs{ QuoteToken: common.Address{}, Currency: in.Currency, @@ -106,5 +102,6 @@ func (tokenTIP20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.Deploy if len(report.Output.Addresses) == 0 { return datastore.AddressRef{}, nil, errors.New("no address returned from TIP20 factory deployment") } + return report.Output.Addresses[0], nil, nil } diff --git a/chains/evm/deployment/v1_0_0/sequences/token_test.go b/chains/evm/deployment/v1_0_0/sequences/token_test.go index 55b4007ac4..98630eeed0 100644 --- a/chains/evm/deployment/v1_0_0/sequences/token_test.go +++ b/chains/evm/deployment/v1_0_0/sequences/token_test.go @@ -193,8 +193,8 @@ func TestEVMTokenDeployments(t *testing.T) { } } - tokenSupportsAdmin := tokenimpl.Capabilities(tc.tokenType).SupportsAdminRole - if tokenSupportsAdmin { + caps := tokenimpl.Capabilities(tc.tokenType) + if caps.SupportsAdminRole { // Verify CCIP Admin was set correctly t.Log(" Verifying CCIP Admin...") onChainCCIPAdmin, err := tokenContract.GetCCIPAdmin(&bind.CallOpts{}) From cc7df82ec691841180774b31fb3d86ac15b8f580 Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Sun, 3 May 2026 16:41:27 -0700 Subject: [PATCH 08/14] chore: fix stale messages --- .../evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go | 2 +- .../tokens/tokenimpl/token_burn_mint_erc20_with_drip.go | 2 +- chains/evm/deployment/tokens/tokenimpl/token_erc20.go | 2 +- chains/evm/deployment/v1_0_0/adapters/pool_adapter.go | 4 ++-- chains/evm/deployment/v1_0_0/sequences/token.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go index c7abbcc6a8..678cce286a 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go @@ -48,7 +48,7 @@ func (tokenBurnMintERC20) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token } func (tokenBurnMintERC20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { - // NOTE: BnM ERC20 tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 strategy. + // NOTE: BnM ERC20 tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 token. return transferTokensERC20(b, chain, token, to, scaledAmount) } diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go index 3466dd2aa5..9c3196c65e 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go @@ -47,7 +47,7 @@ func (tokenBurnMintERC20WithDrip) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chai } func (tokenBurnMintERC20WithDrip) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { - // NOTE: BnM ERC20 drip tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 strategy. + // NOTE: BnM ERC20 drip tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 token. return transferTokensERC20(b, chain, token, to, scaledAmount) } diff --git a/chains/evm/deployment/tokens/tokenimpl/token_erc20.go b/chains/evm/deployment/tokens/tokenimpl/token_erc20.go index cc298ff453..e113d631cb 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_erc20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_erc20.go @@ -32,7 +32,7 @@ func (tokenERC20) Capabilities() CapabilitySet { } func (tokenERC20) RevokeAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { - return nil, fmt.Errorf("admin role not supported for plain ERC20 strategy") + return nil, fmt.Errorf("admin role not supported for plain ERC20 token") } func (tokenERC20) GrantAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { diff --git a/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go b/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go index 76872009bf..0d64d69056 100644 --- a/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go +++ b/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go @@ -374,7 +374,7 @@ func tidyTokenPoolRoles( tokenImpl, ok := tokenimpl.Get(deployment.ContractType(tokenRef.Type)) if !ok { b.Logger.Warnf( - "unsupported token type %q for token at ref (%s); skipping admin role tidy for this token on chain %d", + "unsupported token type %q for token at ref (%s); skipping pool role grants for this token on chain %d", tokenRef.Type.String(), datastore_utils.SprintRef(tokenRef), input.ChainSelector, ) return nil, nil @@ -455,7 +455,7 @@ func tidyTokenRoles( } grantWrites, err := tokenImpl.GrantAdminRole(b, chain, tokenAddr, timelockAddr) if err != nil { - return nil, fmt.Errorf("failed to grant deployer admin role for token %q on chain %d: %w", tokenAddr.Hex(), input.ChainSelector, err) + return nil, fmt.Errorf("failed to grant timelock admin role for token %q on chain %d: %w", tokenAddr.Hex(), input.ChainSelector, err) } revokeWrites, err := tokenImpl.RevokeAdminRole(b, chain, tokenAddr, chain.DeployerKey.From) if err != nil { diff --git a/chains/evm/deployment/v1_0_0/sequences/token.go b/chains/evm/deployment/v1_0_0/sequences/token.go index 3d3dc24691..f0a603774f 100644 --- a/chains/evm/deployment/v1_0_0/sequences/token.go +++ b/chains/evm/deployment/v1_0_0/sequences/token.go @@ -81,7 +81,7 @@ var DeployToken = cldf_ops.NewSequence( if recv != (common.Address{}) && caps.SupportsPreMint { transferWrites, err := tokenImpl.Transfer(b, chain, tokenAddr, recv, preMint) if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to mint pre-mint tokens: %w", err) + return sequences.OnChainOutput{}, fmt.Errorf("failed to transfer pre-minted tokens: %w", err) } writes = append(writes, transferWrites...) } From 3a2f00d726486d35c6c60e488170da5daa1304c1 Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Sun, 3 May 2026 16:43:28 -0700 Subject: [PATCH 09/14] fix: replace tip20 deploy sequence with function --- .../tokens/tokenimpl/token_tip20.go | 10 +- .../v1_0_0/operations/tip20/tip20_ops.go | 202 ++++++++---------- .../v1_0_0/operations/tip20/tip20_ops_test.go | 5 +- 3 files changed, 97 insertions(+), 120 deletions(-) diff --git a/chains/evm/deployment/tokens/tokenimpl/token_tip20.go b/chains/evm/deployment/tokens/tokenimpl/token_tip20.go index 3e1d9c7e98..89e00f4d64 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_tip20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_tip20.go @@ -1,7 +1,6 @@ package tokenimpl import ( - "errors" "fmt" "math/big" @@ -88,7 +87,7 @@ func (tokenTIP20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common. } func (tokenTIP20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteSequence(b, tip20.Deploy, chain, tip20.FactoryDeployArgs{ + tokenRef, writes, err := tip20.DeployTokenViaFactory(b, chain, tip20.FactoryDeployArgs{ QuoteToken: common.Address{}, Currency: in.Currency, Salt: [32]byte{}, @@ -97,11 +96,8 @@ func (tokenTIP20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.Deploy Name: in.Name, }) if err != nil { - return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy TIP20 token via factory: %w", err) - } - if len(report.Output.Addresses) == 0 { - return datastore.AddressRef{}, nil, errors.New("no address returned from TIP20 factory deployment") + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy TIP-20 token via factory: %w", err) } - return report.Output.Addresses[0], nil, nil + return tokenRef, writes, nil } diff --git a/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops.go b/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops.go index e61e4da9ac..4ab082d503 100644 --- a/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops.go +++ b/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops.go @@ -9,10 +9,8 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - mcms_types "github.com/smartcontractkit/mcms/types" chainsel "github.com/smartcontractkit/chain-selectors" - "github.com/smartcontractkit/chainlink-ccip/deployment/utils/sequences" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" @@ -39,7 +37,7 @@ const ( // If we want to deploy a token with currency USD, then its quote token must also be USD. const DefaultQuoteToken = TokenPathUSD -// NOTE: we chose USD as the default currency since most of the well-known TIP20 tokens on Tempo +// NOTE: we chose USD as the default currency since most of the well-known TIP-20 tokens on Tempo // use USD as their currency (e.g. ThetaUSD, BetaUSD, AlphaUSD, PathUSD). const DefaultCurrency = "USD" @@ -47,7 +45,7 @@ type FactoryDeployArgs struct { Currency string // The token currency. Defaults to USD if not provided. Symbol string // The token symbol. This is a required input. Name string // The token name. This is a required input. - QuoteToken common.Address // Address of a pre-existing TIP20 token to use as the quote token. Defaults to PathUSD if not provided. + QuoteToken common.Address // Address of a pre-existing TIP-20 token to use as the quote token. Defaults to PathUSD if not provided. Admin common.Address // The token admin. Defaults to the deployer address if not provided. Salt [32]byte // Optional salt for deterministic deployment. Defaults to a random salt if not provided. } @@ -57,116 +55,100 @@ type TransferArgs struct { Amount *big.Int } -// Deploy deploys the TIP20 token contract with the provided deploy arguments. The TIP20 token is ERC20 compliant and includes additional -// features as defined in the TIP20 standard: https://www.mintlify.com/tempoxyz/tempo/protocol/tip20/overview#erc-20-compatibility. This -// sequence is only applicable for Tempo testnet / mainnet. The token is deployed via the factory contract as recommended in the docs. We -// use sensible defaults for QuoteToken, Currency, Admin, and Salt to reduce the configuration burden on the user when deploying a TIP20 -// token. +// DeployTokenViaFactory deploys the TIP-20 token contract with the provided deploy arguments. The TIP-20 token is ERC20 compliant and includes +// additional features as defined in the TIP-20 standard: https://www.mintlify.com/tempoxyz/tempo/protocol/tip20/overview#erc-20-compatibility. +// This function is only applicable for Tempo testnet / mainnet. The token is deployed via the factory contract as recommended in the docs - we +// use sensible defaults for QuoteToken, Currency, Admin, and Salt to reduce the configuration burden on the user when deploying a TIP-20 token // // Factory Contract: https://github.com/tempoxyz/tempo/blob/a20e2e46c7cba6164ef95c91bf83d5fc614750f3/tips/ref-impls/src/TIP20Factory.sol#L1 // Token Contract: https://github.com/tempoxyz/tempo/blob/a20e2e46c7cba6164ef95c91bf83d5fc614750f3/tips/ref-impls/src/TIP20.sol#L1 // Docs: https://www.mintlify.com/tempoxyz/tempo/protocol/tip20/overview -var Deploy = operations.NewSequence( - "tip20:deploy", - Version, - "Deploys a TIP20 token via the TIP20 factory. Only applicable for Tempo testnet / mainnet.", - func(b operations.Bundle, chain evm.Chain, input FactoryDeployArgs) (sequences.OnChainOutput, error) { - isTempoTestnet := chainsel.TEMPO_TESTNET_MODERATO.Selector == chain.Selector || chainsel.TEMPO_TESTNET.Selector == chain.Selector - isTempoMainnet := chainsel.TEMPO_MAINNET.Selector == chain.Selector - if !isTempoTestnet && !isTempoMainnet { - return sequences.OnChainOutput{}, errors.New("TIP20 token deployment is only supported on Tempo testnet and mainnet") - } - - factoryAddr := common.HexToAddress(TokenFactoryAddress) - deployerKey := chain.DeployerKey.From - if input.Symbol == "" { - return sequences.OnChainOutput{}, errors.New("symbol is required") - } - if input.Name == "" { - return sequences.OnChainOutput{}, errors.New("name is required") - } - if input.QuoteToken == (common.Address{}) { - input.QuoteToken = common.HexToAddress(DefaultQuoteToken) - } - if input.Currency == "" { - input.Currency = DefaultCurrency - } - if input.Admin == (common.Address{}) { - input.Admin = deployerKey - } - if input.Salt == [32]byte{} { - if salt, err := generateValidSalt(b, chain, factoryAddr, deployerKey); err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("failed to produce a valid salt for token deployment: %w", err) - } else { - input.Salt = salt - } - } - - b.Logger.Infof("Validating quote token address: %s", input.QuoteToken.Hex()) - isQuoteTokenValid, err := operations.ExecuteOperation(b, IsTIP20, chain, contract.FunctionInput[common.Address]{ - ChainSelector: chain.Selector, - Address: factoryAddr, - Args: input.QuoteToken, - }) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("isTIP20 quote token: %w", err) - } - if !isQuoteTokenValid.Output { - return sequences.OnChainOutput{}, errors.New("quoteToken must be a valid TIP-20 token address") - } - - b.Logger.Infof("Deploying TIP20 token: %+v", input) - createTokenReport, err := operations.ExecuteOperation(b, CreateToken, chain, contract.FunctionInput[CreateTokenArgs]{ - ChainSelector: chain.Selector, - Address: factoryAddr, - Args: CreateTokenArgs{ - QuoteToken: input.QuoteToken, - Currency: input.Currency, - Symbol: input.Symbol, - Admin: input.Admin, - Name: input.Name, - Salt: input.Salt, - }, - }) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("createToken: %w", err) - } - - b.Logger.Info("Retrieving address of deployed token via factory's getTokenAddress function") - tokenAddrReport, err := operations.ExecuteOperation(b, GetTokenAddress, chain, contract.FunctionInput[GetTokenAddressArgs]{ - ChainSelector: chain.Selector, - Address: factoryAddr, - Args: GetTokenAddressArgs{ - Sender: deployerKey, - Salt: input.Salt, - }, - }) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("getTokenAddress after deploy: %w", err) - } - - b.Logger.Infof("Deployed TIP20 token at address: %s", tokenAddrReport.Output.Hex()) - batchOp, err := contract.NewBatchOperationFromWrites([]contract.WriteOutput{createTokenReport.Output}) - if err != nil { - return sequences.OnChainOutput{}, fmt.Errorf("batch operation: %w", err) - } - - return sequences.OnChainOutput{ - Addresses: []datastore.AddressRef{ - { - ChainSelector: chain.Selector, - Address: tokenAddrReport.Output.Hex(), - Qualifier: input.Symbol, - Type: datastore.ContractType(ContractType), - Version: Version, - }, - }, - BatchOps: []mcms_types.BatchOperation{ - batchOp, - }, - }, nil - }, -) +func DeployTokenViaFactory(b operations.Bundle, chain evm.Chain, input FactoryDeployArgs) (datastore.AddressRef, []contract.WriteOutput, error) { + isTempoTestnet := chainsel.TEMPO_TESTNET_MODERATO.Selector == chain.Selector || chainsel.TEMPO_TESTNET.Selector == chain.Selector + isTempoMainnet := chainsel.TEMPO_MAINNET.Selector == chain.Selector + if !isTempoTestnet && !isTempoMainnet { + return datastore.AddressRef{}, nil, errors.New("TIP-20 token deployment is only supported on Tempo testnet and mainnet") + } + + factoryAddr := common.HexToAddress(TokenFactoryAddress) + deployerKey := chain.DeployerKey.From + if input.Symbol == "" { + return datastore.AddressRef{}, nil, errors.New("symbol is required") + } + if input.Name == "" { + return datastore.AddressRef{}, nil, errors.New("name is required") + } + if input.QuoteToken == (common.Address{}) { + input.QuoteToken = common.HexToAddress(DefaultQuoteToken) + } + if input.Currency == "" { + input.Currency = DefaultCurrency + } + if input.Admin == (common.Address{}) { + input.Admin = deployerKey + } + if input.Salt == [32]byte{} { + if salt, err := generateValidSalt(b, chain, factoryAddr, deployerKey); err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to produce a valid salt for token deployment: %w", err) + } else { + input.Salt = salt + } + } + + b.Logger.Infof("Validating quote token address: %s", input.QuoteToken.Hex()) + isQuoteTokenValid, err := operations.ExecuteOperation(b, IsTIP20, chain, contract.FunctionInput[common.Address]{ + ChainSelector: chain.Selector, + Address: factoryAddr, + Args: input.QuoteToken, + }) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("isTIP20 quote token: %w", err) + } + if !isQuoteTokenValid.Output { + return datastore.AddressRef{}, nil, errors.New("quoteToken must be a valid TIP-20 token address") + } + + b.Logger.Infof("Deploying TIP-20 token: %+v", input) + createTokenReport, err := operations.ExecuteOperation(b, CreateToken, chain, contract.FunctionInput[CreateTokenArgs]{ + ChainSelector: chain.Selector, + Address: factoryAddr, + Args: CreateTokenArgs{ + QuoteToken: input.QuoteToken, + Currency: input.Currency, + Symbol: input.Symbol, + Admin: input.Admin, + Name: input.Name, + Salt: input.Salt, + }, + }) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("createToken: %w", err) + } + + b.Logger.Info("Retrieving address of deployed token via factory's getTokenAddress function") + tokenAddrReport, err := operations.ExecuteOperation(b, GetTokenAddress, chain, contract.FunctionInput[GetTokenAddressArgs]{ + ChainSelector: chain.Selector, + Address: factoryAddr, + Args: GetTokenAddressArgs{ + Sender: deployerKey, + Salt: input.Salt, + }, + }) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("getTokenAddress after deploy: %w", err) + } + + b.Logger.Infof("Deployed TIP-20 token at address: %s", tokenAddrReport.Output.Hex()) + tokenRef := datastore.AddressRef{ + ChainSelector: chain.Selector, + Address: tokenAddrReport.Output.Hex(), + Qualifier: input.Symbol, + Type: datastore.ContractType(ContractType), + Version: Version, + } + + return tokenRef, []contract.WriteOutput{createTokenReport.Output}, nil +} var Transfer = contract.NewWrite(contract.WriteParams[TransferArgs, *TIP20Token]{ Name: "tip20:transfer", diff --git a/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops_test.go b/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops_test.go index 369eaee001..aaa846a209 100644 --- a/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops_test.go +++ b/chains/evm/deployment/v1_0_0/operations/tip20/tip20_ops_test.go @@ -9,10 +9,9 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" - "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) -func TestDeploy_RejectsNonTempoChain(t *testing.T) { +func TestDeployTokenViaFactory_RejectsNonTempoChain(t *testing.T) { t.Parallel() evmSel := chainsel.ETHEREUM_MAINNET.Selector @@ -24,7 +23,7 @@ func TestDeploy_RejectsNonTempoChain(t *testing.T) { chain, ok := e.BlockChains.EVMChains()[evmSel] require.True(t, ok) - _, err = operations.ExecuteSequence(e.OperationsBundle, tip20.Deploy, chain, tip20.FactoryDeployArgs{ + _, _, err = tip20.DeployTokenViaFactory(e.OperationsBundle, chain, tip20.FactoryDeployArgs{ QuoteToken: common.Address{}, // defaults to sensible value Currency: "", // defaults to sensible value Salt: [32]byte{}, // generate random salt From 5b26a3f1510c289ca7f4193fd97cd7baa91714f7 Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Sun, 3 May 2026 16:55:52 -0700 Subject: [PATCH 10/14] chore: minor clean up --- chains/evm/deployment/tokens/tokenimpl/impl.go | 6 +++--- chains/evm/deployment/v1_0_0/sequences/token_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chains/evm/deployment/tokens/tokenimpl/impl.go b/chains/evm/deployment/tokens/tokenimpl/impl.go index cb5981d579..772cf0bdb7 100644 --- a/chains/evm/deployment/tokens/tokenimpl/impl.go +++ b/chains/evm/deployment/tokens/tokenimpl/impl.go @@ -69,8 +69,8 @@ type Token interface { // Deploy performs the token contract deployment, returning the // resulting datastore reference and any token-side write outputs - // produced during deployment. Implementations wrap either an - // Operation (via contract.MaybeDeployContract) or a Sequence - // (via cldf_ops.ExecuteSequence) as appropriate for the token type. + // produced during deployment. Implementations may call lower-level + // deployment operations or helpers, but batching is handled by the + // outer token deployment sequence. Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) } diff --git a/chains/evm/deployment/v1_0_0/sequences/token_test.go b/chains/evm/deployment/v1_0_0/sequences/token_test.go index 98630eeed0..4b431cfaa9 100644 --- a/chains/evm/deployment/v1_0_0/sequences/token_test.go +++ b/chains/evm/deployment/v1_0_0/sequences/token_test.go @@ -194,7 +194,7 @@ func TestEVMTokenDeployments(t *testing.T) { } caps := tokenimpl.Capabilities(tc.tokenType) - if caps.SupportsAdminRole { + if caps.SupportsCCIPAdmin { // Verify CCIP Admin was set correctly t.Log(" Verifying CCIP Admin...") onChainCCIPAdmin, err := tokenContract.GetCCIPAdmin(&bind.CallOpts{}) From 8c7f5245df1e40569dd85bba2bb03c025f45c012 Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Sun, 3 May 2026 18:24:39 -0700 Subject: [PATCH 11/14] fix: introduce deprecated token type for backwards compatibility --- .../evm/deployment/tokens/tokenimpl/impl.go | 6 +- .../evm/deployment/tokens/tokenimpl/lookup.go | 14 +-- .../token_burn_mint_erc20_with_drip_v1_0_0.go | 89 +++++++++++++++++++ ...token_burn_mint_erc20_with_drip_v1_5_0.go} | 18 ++-- .../v1_0_0/adapters/pool_adapter.go | 8 +- 5 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go rename chains/evm/deployment/tokens/tokenimpl/{token_burn_mint_erc20_with_drip.go => token_burn_mint_erc20_with_drip_v1_5_0.go} (64%) diff --git a/chains/evm/deployment/tokens/tokenimpl/impl.go b/chains/evm/deployment/tokens/tokenimpl/impl.go index 772cf0bdb7..71dffa1218 100644 --- a/chains/evm/deployment/tokens/tokenimpl/impl.go +++ b/chains/evm/deployment/tokens/tokenimpl/impl.go @@ -47,9 +47,9 @@ type Token interface { RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) // GrantAdminRole grants the default-admin or contract-specific - // admin role to user. Implementations return (nil, nil) for any - // token types whose Capabilities.SupportsAdminRole is false; - // callers should consult that flag first. + // admin role to user. Returns an error for token types whose + // Capabilities.SupportsAdminRole is false; callers should consult + // that flag first. GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) // GrantPoolRoles emits the writes that authorize a freshly-deployed pool diff --git a/chains/evm/deployment/tokens/tokenimpl/lookup.go b/chains/evm/deployment/tokens/tokenimpl/lookup.go index b180ef2cff..a5cb52f8af 100644 --- a/chains/evm/deployment/tokens/tokenimpl/lookup.go +++ b/chains/evm/deployment/tokens/tokenimpl/lookup.go @@ -1,18 +1,20 @@ package tokenimpl import ( - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" + bnmERC20 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" + dripV1_0_0 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" + dripV1_5_0 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" ) var tokenImpls = map[deployment.ContractType]Token{ - burn_mint_erc20_with_drip.ContractType: tokenBurnMintERC20WithDrip{}, - burn_mint_erc20.ContractType: tokenBurnMintERC20{}, - erc20.ContractType: tokenERC20{}, - tip20.ContractType: tokenTIP20{}, + dripV1_5_0.ContractType: tokenBurnMintERC20WithDripV1_5_0{}, + dripV1_0_0.ContractType: tokenBurnMintERC20WithDripV1_0_0{}, + bnmERC20.ContractType: tokenBurnMintERC20{}, + erc20.ContractType: tokenERC20{}, + tip20.ContractType: tokenTIP20{}, } // Get returns the token implementation for an EVM token contract type. diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go new file mode 100644 index 0000000000..1fc13ddc68 --- /dev/null +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go @@ -0,0 +1,89 @@ +package tokenimpl + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + drip_v100 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// Deprecated: BurnMintERC20WithDripToken has no actual drip functionality - it +// is retained only for compatibility with existing tests and should be removed +// in a future cleanup. +type tokenBurnMintERC20WithDripV1_0_0 struct{} + +func (tokenBurnMintERC20WithDripV1_0_0) ContractType() deployment.ContractType { + return drip_v100.ContractType +} + +func (tokenBurnMintERC20WithDripV1_0_0) Capabilities() CapabilitySet { + return CapabilitySet{ + ParticipatesInPoolRoleGrant: true, + SupportsAdminRole: true, + SupportsCCIPAdmin: true, + SupportsPreMint: true, + } +} + +func (tokenBurnMintERC20WithDripV1_0_0) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { + return revokeDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) +} + +func (tokenBurnMintERC20WithDripV1_0_0) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { + return grantDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) +} + +func (tokenBurnMintERC20WithDripV1_0_0) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { + return grantMintAndBurnRolesBurnMintERC20(b, chain, token, pool) +} + +func (tokenBurnMintERC20WithDripV1_0_0) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { + return setCCIPAdminBurnMintERC20(b, chain, token, ccipAdmin) +} + +func (tokenBurnMintERC20WithDripV1_0_0) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { + // NOTE: BnM ERC20 drip tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 token. + return transferTokensERC20(b, chain, token, to, scaledAmount) +} + +func (tokenBurnMintERC20WithDripV1_0_0) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + maxSupply := big.NewInt(0) + if in.Supply != nil { + maxSupply = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.Supply), in.Decimals) + } + + preMint := big.NewInt(0) + if in.PreMint != nil { + preMint = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.PreMint), in.Decimals) + } + + ref, err := contract.MaybeDeployContract(b, drip_v100.Deploy, chain, + contract.DeployInput[drip_v100.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(drip_v100.ContractType, *common_utils.Version_1_0_0), + ChainSelector: chain.Selector, + Qualifier: &in.Symbol, + Args: drip_v100.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + Decimals: in.Decimals, + MaxSupply: maxSupply, + PreMint: preMint, + }, + }, + nil, + ) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC20WithDripToken token: %w", err) + } + + return ref, nil, nil +} diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go similarity index 64% rename from chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go rename to chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go index 9c3196c65e..7b7c5a8f2c 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go @@ -15,13 +15,13 @@ import ( cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) -type tokenBurnMintERC20WithDrip struct{} +type tokenBurnMintERC20WithDripV1_5_0 struct{} -func (tokenBurnMintERC20WithDrip) ContractType() deployment.ContractType { +func (tokenBurnMintERC20WithDripV1_5_0) ContractType() deployment.ContractType { return drip_v150.ContractType } -func (tokenBurnMintERC20WithDrip) Capabilities() CapabilitySet { +func (tokenBurnMintERC20WithDripV1_5_0) Capabilities() CapabilitySet { return CapabilitySet{ ParticipatesInPoolRoleGrant: true, SupportsAdminRole: true, @@ -30,28 +30,28 @@ func (tokenBurnMintERC20WithDrip) Capabilities() CapabilitySet { } } -func (tokenBurnMintERC20WithDrip) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { return revokeDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } -func (tokenBurnMintERC20WithDrip) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { return grantDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } -func (tokenBurnMintERC20WithDrip) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { return grantMintAndBurnRolesBurnMintERC20(b, chain, token, pool) } -func (tokenBurnMintERC20WithDrip) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { return setCCIPAdminBurnMintERC20(b, chain, token, ccipAdmin) } -func (tokenBurnMintERC20WithDrip) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { // NOTE: BnM ERC20 drip tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 token. return transferTokensERC20(b, chain, token, to, scaledAmount) } -func (tokenBurnMintERC20WithDrip) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { ref, err := contract.MaybeDeployContract(b, drip_v150.Deploy, chain, contract.DeployInput[drip_v150.ConstructorArgs]{ TypeAndVersion: deployment.NewTypeAndVersion(drip_v150.ContractType, *drip_v150.Version), diff --git a/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go b/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go index 0d64d69056..7e76f30c92 100644 --- a/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go +++ b/chains/evm/deployment/v1_0_0/adapters/pool_adapter.go @@ -324,6 +324,9 @@ func (a *EVMPoolAdapter) DeployTokenPoolForToken() *cldf_ops.Sequence[tokensapi. if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to convert token pool ref to EVM address for chain %d: %w", input.ChainSelector, err) } + if poolAddr == (common.Address{}) { + return sequences.OnChainOutput{}, errors.New("deployed token pool address cannot be the zero address") + } output, err := a.Ops.SetRateLimitAdmin(b, chain, poolAddr, rlAdminAddr) if err != nil { return sequences.OnChainOutput{}, fmt.Errorf("failed to set rate limit admin: %w", err) @@ -396,11 +399,6 @@ func tidyTokenPoolRoles( } } - b.Logger.Warnf( - "pool with ref (%s) was not configured with any roles", - datastore_utils.SprintRef(poolRef), - ) - return nil, nil } From 7d74e7577fcced554e03c1efa8b9bad0730aedbb Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Mon, 4 May 2026 21:51:34 -0700 Subject: [PATCH 12/14] fix: add erc677 --- .../evm/deployment/tokens/tokenimpl/impl.go | 4 +- .../evm/deployment/tokens/tokenimpl/lookup.go | 3 + .../tokens/tokenimpl/token_burn_mint_erc20.go | 2 +- .../token_burn_mint_erc20_with_drip_v1_0_0.go | 2 +- .../token_burn_mint_erc20_with_drip_v1_5_0.go | 2 +- .../tokenimpl/token_burn_mint_erc677.go | 71 +++++++++++++++++++ .../tokens/tokenimpl/token_erc20.go | 2 +- .../tokens/tokenimpl/token_tip20.go | 2 +- .../deployment/v1_0_0/sequences/token_test.go | 3 + 9 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go diff --git a/chains/evm/deployment/tokens/tokenimpl/impl.go b/chains/evm/deployment/tokens/tokenimpl/impl.go index 71dffa1218..74ef7e8d5d 100644 --- a/chains/evm/deployment/tokens/tokenimpl/impl.go +++ b/chains/evm/deployment/tokens/tokenimpl/impl.go @@ -54,10 +54,12 @@ type Token interface { // GrantPoolRoles emits the writes that authorize a freshly-deployed pool // to mint/burn (or its TIP-20 issuer-role equivalent) against this token. + // proposalExecutor is the MCMS timelock (or zero when unused); BurnMintERC677 + // uses it for PrepareGrantMintAndBurnRoles. Other token types ignore it. // Returns an error for token types that don't participate in pool role // granting; ParticipatesInPoolRoleGrant is the authoritative flag, callers // should consult it first. - GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) + GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool, proposalExecutor common.Address) ([]contract.WriteOutput, error) // SetCCIPAdmin sets the token-level CCIP admin where the token contract // supports one. Callers should consult SupportsCCIPAdmin first. diff --git a/chains/evm/deployment/tokens/tokenimpl/lookup.go b/chains/evm/deployment/tokens/tokenimpl/lookup.go index a5cb52f8af..d1ebc6978f 100644 --- a/chains/evm/deployment/tokens/tokenimpl/lookup.go +++ b/chains/evm/deployment/tokens/tokenimpl/lookup.go @@ -6,12 +6,15 @@ import ( "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/tip20" dripV1_5_0 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" ) var tokenImpls = map[deployment.ContractType]Token{ dripV1_5_0.ContractType: tokenBurnMintERC20WithDripV1_5_0{}, dripV1_0_0.ContractType: tokenBurnMintERC20WithDripV1_0_0{}, + utils.ERC677TokenHelper: tokenBurnMintERC677{}, + utils.BurnMintToken: tokenBurnMintERC677{}, bnmERC20.ContractType: tokenBurnMintERC20{}, erc20.ContractType: tokenERC20{}, tip20.ContractType: tokenTIP20{}, diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go index 678cce286a..2524dc4038 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go @@ -39,7 +39,7 @@ func (tokenBurnMintERC20) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, tok return grantDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } -func (tokenBurnMintERC20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { return grantMintAndBurnRolesBurnMintERC20(b, chain, token, pool) } diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go index 1fc13ddc68..b74c7d97e0 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go @@ -42,7 +42,7 @@ func (tokenBurnMintERC20WithDripV1_0_0) GrantAdminRole(b cldf_ops.Bundle, chain return grantDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } -func (tokenBurnMintERC20WithDripV1_0_0) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_0_0) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { return grantMintAndBurnRolesBurnMintERC20(b, chain, token, pool) } diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go index 7b7c5a8f2c..0ddd507ae1 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go @@ -38,7 +38,7 @@ func (tokenBurnMintERC20WithDripV1_5_0) GrantAdminRole(b cldf_ops.Bundle, chain return grantDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } -func (tokenBurnMintERC20WithDripV1_5_0) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { return grantMintAndBurnRolesBurnMintERC20(b, chain, token, pool) } diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go new file mode 100644 index 0000000000..ca25f6cfd6 --- /dev/null +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go @@ -0,0 +1,71 @@ +package tokenimpl + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + bnmERC677 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677" + tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + cciputils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +type tokenBurnMintERC677 struct{} + +func (tokenBurnMintERC677) ContractType() deployment.ContractType { + return cciputils.BurnMintToken +} + +func (tokenBurnMintERC677) Capabilities() CapabilitySet { + return CapabilitySet{ + ParticipatesInPoolRoleGrant: true, + // Admin tidy uses BurnMintERC20 operations in tidyTokenRoles; ERC677 uses a + // different binding until dedicated admin ops exist for this type. + SupportsAdminRole: false, + SupportsCCIPAdmin: false, + SupportsPreMint: false, + } +} + +func (tokenBurnMintERC677) RevokeAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { + return nil, fmt.Errorf("admin role revoke not supported for BurnMintERC677 token type") +} + +func (tokenBurnMintERC677) GrantAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { + return nil, fmt.Errorf("admin role grant not supported for BurnMintERC677 token type") +} + +func (tokenBurnMintERC677) GrantPoolRoles( + b cldf_ops.Bundle, + chain evm.Chain, + token, pool, proposalExecutor common.Address, +) ([]contract.WriteOutput, error) { + return bnmERC677.PrepareGrantMintAndBurnRoles( + b, + chain, + contract.FunctionInput[common.Address]{ + ChainSelector: chain.Selector, + Address: token, + Args: pool, + }, + proposalExecutor, + ) +} + +func (tokenBurnMintERC677) SetCCIPAdmin(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { + return nil, fmt.Errorf("CCIP admin not supported for BurnMintERC677 token type via this deployment path") +} + +func (tokenBurnMintERC677) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { + return transferTokensERC20(b, chain, token, to, scaledAmount) +} + +func (tokenBurnMintERC677) Deploy(_ cldf_ops.Bundle, _ evm.Chain, _ tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + return datastore.AddressRef{}, nil, fmt.Errorf("deploy BurnMintERC677 token is not implemented in tokenimpl; deploy out of band and record in datastore") +} diff --git a/chains/evm/deployment/tokens/tokenimpl/token_erc20.go b/chains/evm/deployment/tokens/tokenimpl/token_erc20.go index e113d631cb..0a0ce6a476 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_erc20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_erc20.go @@ -39,7 +39,7 @@ func (tokenERC20) GrantAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Add return nil, fmt.Errorf("admin role granting not supported for plain ERC20 token") } -func (tokenERC20) GrantPoolRoles(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenERC20) GrantPoolRoles(_ cldf_ops.Bundle, _ evm.Chain, _, _, _ common.Address) ([]contract.WriteOutput, error) { return nil, fmt.Errorf("pool role granting not supported for plain ERC20 token") } diff --git a/chains/evm/deployment/tokens/tokenimpl/token_tip20.go b/chains/evm/deployment/tokens/tokenimpl/token_tip20.go index 89e00f4d64..022c74d969 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_tip20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_tip20.go @@ -54,7 +54,7 @@ func (tokenTIP20) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user return []contract.WriteOutput{report.Output}, nil } -func (tokenTIP20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool common.Address) ([]contract.WriteOutput, error) { +func (tokenTIP20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { report, err := cldf_ops.ExecuteOperation(b, tip20.GrantIssuerRole, chain, contract.FunctionInput[common.Address]{ ChainSelector: chain.Selector, Address: token, diff --git a/chains/evm/deployment/v1_0_0/sequences/token_test.go b/chains/evm/deployment/v1_0_0/sequences/token_test.go index 4b431cfaa9..c08be6665f 100644 --- a/chains/evm/deployment/v1_0_0/sequences/token_test.go +++ b/chains/evm/deployment/v1_0_0/sequences/token_test.go @@ -22,6 +22,7 @@ import ( bnm_bindings "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc20" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils" ) // TestEVMTokenDeployments tests various EVM token deployments using the DeployToken sequence directly. @@ -235,6 +236,8 @@ func TestTokenSupportsAdminRole(t *testing.T) { tokenTypes := map[cldf.ContractType]bool{ burn_mint_erc20_with_drip.ContractType: true, burn_mint_erc20.ContractType: true, + utils.ERC677TokenHelper: false, + utils.BurnMintToken: false, tip20.ContractType: true, erc20.ContractType: false, } From b4275b5b2abf45bd12422bd20de1c1da33985e65 Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Mon, 4 May 2026 22:24:27 -0700 Subject: [PATCH 13/14] chore: simplify imports --- .../tokens/tokenimpl/token_burn_mint_erc20.go | 18 ++++++------ .../token_burn_mint_erc20_with_drip_v1_0_0.go | 28 +++++++++---------- .../token_burn_mint_erc20_with_drip_v1_5_0.go | 26 ++++++++--------- .../tokenimpl/token_burn_mint_erc677.go | 22 +++++++-------- .../tokens/tokenimpl/token_erc20.go | 18 ++++++------ .../tokens/tokenimpl/token_tip20.go | 22 +++++++-------- 6 files changed, 67 insertions(+), 67 deletions(-) diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go index 2524dc4038..34d2e1a0cd 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20.go @@ -8,12 +8,12 @@ import ( "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) type tokenBurnMintERC20 struct{} @@ -31,28 +31,28 @@ func (tokenBurnMintERC20) Capabilities() CapabilitySet { } } -func (tokenBurnMintERC20) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20) RevokeAdminRole(b operations.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { return revokeDefaultAdminRoleBurnMintERC20(b, chain, token, user) } -func (tokenBurnMintERC20) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20) GrantAdminRole(b operations.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { return grantDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } -func (tokenBurnMintERC20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20) GrantPoolRoles(b operations.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { return grantMintAndBurnRolesBurnMintERC20(b, chain, token, pool) } -func (tokenBurnMintERC20) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20) SetCCIPAdmin(b operations.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { return setCCIPAdminBurnMintERC20(b, chain, token, ccipAdmin) } -func (tokenBurnMintERC20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20) Transfer(b operations.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { // NOTE: BnM ERC20 tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 token. return transferTokensERC20(b, chain, token, to, scaledAmount) } -func (tokenBurnMintERC20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { +func (tokenBurnMintERC20) Deploy(b operations.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { maxSupply := big.NewInt(0) if in.Supply != nil { maxSupply = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.Supply), in.Decimals) @@ -65,7 +65,7 @@ func (tokenBurnMintERC20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensap ref, err := contract.MaybeDeployContract(b, burn_mint_erc20.Deploy, chain, contract.DeployInput[burn_mint_erc20.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20.ContractType, *common_utils.Version_1_0_0), + TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20.ContractType, *utils.Version_1_0_0), ChainSelector: chain.Selector, Qualifier: &in.Symbol, Args: burn_mint_erc20.ConstructorArgs{ diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go index b74c7d97e0..8efcf108a6 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_0_0.go @@ -6,14 +6,14 @@ import ( "github.com/ethereum/go-ethereum/common" - drip_v100 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc20_with_drip" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) // Deprecated: BurnMintERC20WithDripToken has no actual drip functionality - it @@ -22,7 +22,7 @@ import ( type tokenBurnMintERC20WithDripV1_0_0 struct{} func (tokenBurnMintERC20WithDripV1_0_0) ContractType() deployment.ContractType { - return drip_v100.ContractType + return burn_mint_erc20_with_drip.ContractType } func (tokenBurnMintERC20WithDripV1_0_0) Capabilities() CapabilitySet { @@ -34,28 +34,28 @@ func (tokenBurnMintERC20WithDripV1_0_0) Capabilities() CapabilitySet { } } -func (tokenBurnMintERC20WithDripV1_0_0) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_0_0) RevokeAdminRole(b operations.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { return revokeDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } -func (tokenBurnMintERC20WithDripV1_0_0) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_0_0) GrantAdminRole(b operations.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { return grantDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } -func (tokenBurnMintERC20WithDripV1_0_0) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_0_0) GrantPoolRoles(b operations.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { return grantMintAndBurnRolesBurnMintERC20(b, chain, token, pool) } -func (tokenBurnMintERC20WithDripV1_0_0) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_0_0) SetCCIPAdmin(b operations.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { return setCCIPAdminBurnMintERC20(b, chain, token, ccipAdmin) } -func (tokenBurnMintERC20WithDripV1_0_0) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_0_0) Transfer(b operations.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { // NOTE: BnM ERC20 drip tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 token. return transferTokensERC20(b, chain, token, to, scaledAmount) } -func (tokenBurnMintERC20WithDripV1_0_0) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_0_0) Deploy(b operations.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { maxSupply := big.NewInt(0) if in.Supply != nil { maxSupply = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.Supply), in.Decimals) @@ -66,12 +66,12 @@ func (tokenBurnMintERC20WithDripV1_0_0) Deploy(b cldf_ops.Bundle, chain evm.Chai preMint = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.PreMint), in.Decimals) } - ref, err := contract.MaybeDeployContract(b, drip_v100.Deploy, chain, - contract.DeployInput[drip_v100.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(drip_v100.ContractType, *common_utils.Version_1_0_0), + ref, err := contract.MaybeDeployContract(b, burn_mint_erc20_with_drip.Deploy, chain, + contract.DeployInput[burn_mint_erc20_with_drip.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20_with_drip.ContractType, *utils.Version_1_0_0), ChainSelector: chain.Selector, Qualifier: &in.Symbol, - Args: drip_v100.ConstructorArgs{ + Args: burn_mint_erc20_with_drip.ConstructorArgs{ Name: in.Name, Symbol: in.Symbol, Decimals: in.Decimals, diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go index 0ddd507ae1..89f8ea51df 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc20_with_drip_v1_5_0.go @@ -6,19 +6,19 @@ import ( "github.com/ethereum/go-ethereum/common" - drip_v150 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/burn_mint_erc20_with_drip" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) type tokenBurnMintERC20WithDripV1_5_0 struct{} func (tokenBurnMintERC20WithDripV1_5_0) ContractType() deployment.ContractType { - return drip_v150.ContractType + return burn_mint_erc20_with_drip.ContractType } func (tokenBurnMintERC20WithDripV1_5_0) Capabilities() CapabilitySet { @@ -30,34 +30,34 @@ func (tokenBurnMintERC20WithDripV1_5_0) Capabilities() CapabilitySet { } } -func (tokenBurnMintERC20WithDripV1_5_0) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) RevokeAdminRole(b operations.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { return revokeDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } -func (tokenBurnMintERC20WithDripV1_5_0) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) GrantAdminRole(b operations.Bundle, chain evm.Chain, token, externalAdmin common.Address) ([]contract.WriteOutput, error) { return grantDefaultAdminRoleBurnMintERC20(b, chain, token, externalAdmin) } -func (tokenBurnMintERC20WithDripV1_5_0) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) GrantPoolRoles(b operations.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { return grantMintAndBurnRolesBurnMintERC20(b, chain, token, pool) } -func (tokenBurnMintERC20WithDripV1_5_0) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) SetCCIPAdmin(b operations.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { return setCCIPAdminBurnMintERC20(b, chain, token, ccipAdmin) } -func (tokenBurnMintERC20WithDripV1_5_0) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC20WithDripV1_5_0) Transfer(b operations.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { // NOTE: BnM ERC20 drip tokens inherit from a standard ERC20 implementation, so we can use the same transfer helper function as the plain ERC20 token. return transferTokensERC20(b, chain, token, to, scaledAmount) } -func (tokenBurnMintERC20WithDripV1_5_0) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - ref, err := contract.MaybeDeployContract(b, drip_v150.Deploy, chain, - contract.DeployInput[drip_v150.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(drip_v150.ContractType, *drip_v150.Version), +func (tokenBurnMintERC20WithDripV1_5_0) Deploy(b operations.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + ref, err := contract.MaybeDeployContract(b, burn_mint_erc20_with_drip.Deploy, chain, + contract.DeployInput[burn_mint_erc20_with_drip.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc20_with_drip.ContractType, *burn_mint_erc20_with_drip.Version), ChainSelector: chain.Selector, Qualifier: &in.Symbol, - Args: drip_v150.ConstructorArgs{ + Args: burn_mint_erc20_with_drip.ConstructorArgs{ Name: in.Name, Symbol: in.Symbol, }, diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go index ca25f6cfd6..d5274992d2 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go @@ -6,20 +6,20 @@ import ( "github.com/ethereum/go-ethereum/common" - bnmERC677 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - cciputils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) type tokenBurnMintERC677 struct{} func (tokenBurnMintERC677) ContractType() deployment.ContractType { - return cciputils.BurnMintToken + return utils.BurnMintToken } func (tokenBurnMintERC677) Capabilities() CapabilitySet { @@ -33,20 +33,20 @@ func (tokenBurnMintERC677) Capabilities() CapabilitySet { } } -func (tokenBurnMintERC677) RevokeAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC677) RevokeAdminRole(_ operations.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { return nil, fmt.Errorf("admin role revoke not supported for BurnMintERC677 token type") } -func (tokenBurnMintERC677) GrantAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC677) GrantAdminRole(_ operations.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { return nil, fmt.Errorf("admin role grant not supported for BurnMintERC677 token type") } func (tokenBurnMintERC677) GrantPoolRoles( - b cldf_ops.Bundle, + b operations.Bundle, chain evm.Chain, token, pool, proposalExecutor common.Address, ) ([]contract.WriteOutput, error) { - return bnmERC677.PrepareGrantMintAndBurnRoles( + return burn_mint_erc677.PrepareGrantMintAndBurnRoles( b, chain, contract.FunctionInput[common.Address]{ @@ -58,14 +58,14 @@ func (tokenBurnMintERC677) GrantPoolRoles( ) } -func (tokenBurnMintERC677) SetCCIPAdmin(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC677) SetCCIPAdmin(_ operations.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { return nil, fmt.Errorf("CCIP admin not supported for BurnMintERC677 token type via this deployment path") } -func (tokenBurnMintERC677) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC677) Transfer(b operations.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { return transferTokensERC20(b, chain, token, to, scaledAmount) } -func (tokenBurnMintERC677) Deploy(_ cldf_ops.Bundle, _ evm.Chain, _ tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { +func (tokenBurnMintERC677) Deploy(_ operations.Bundle, _ evm.Chain, _ tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { return datastore.AddressRef{}, nil, fmt.Errorf("deploy BurnMintERC677 token is not implemented in tokenimpl; deploy out of band and record in datastore") } diff --git a/chains/evm/deployment/tokens/tokenimpl/token_erc20.go b/chains/evm/deployment/tokens/tokenimpl/token_erc20.go index 0a0ce6a476..a27555dc0d 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_erc20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_erc20.go @@ -8,12 +8,12 @@ import ( "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_0_0/operations/erc20" tokensapi "github.com/smartcontractkit/chainlink-ccip/deployment/tokens" - common_utils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" + "github.com/smartcontractkit/chainlink-ccip/deployment/utils" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) type tokenERC20 struct{} @@ -31,30 +31,30 @@ func (tokenERC20) Capabilities() CapabilitySet { } } -func (tokenERC20) RevokeAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenERC20) RevokeAdminRole(_ operations.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { return nil, fmt.Errorf("admin role not supported for plain ERC20 token") } -func (tokenERC20) GrantAdminRole(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenERC20) GrantAdminRole(_ operations.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { return nil, fmt.Errorf("admin role granting not supported for plain ERC20 token") } -func (tokenERC20) GrantPoolRoles(_ cldf_ops.Bundle, _ evm.Chain, _, _, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenERC20) GrantPoolRoles(_ operations.Bundle, _ evm.Chain, _, _, _ common.Address) ([]contract.WriteOutput, error) { return nil, fmt.Errorf("pool role granting not supported for plain ERC20 token") } -func (tokenERC20) SetCCIPAdmin(_ cldf_ops.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { +func (tokenERC20) SetCCIPAdmin(_ operations.Bundle, _ evm.Chain, _, _ common.Address) ([]contract.WriteOutput, error) { return nil, fmt.Errorf("CCIP admin role not supported for plain ERC20 token") } -func (tokenERC20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { +func (tokenERC20) Transfer(b operations.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { return transferTokensERC20(b, chain, token, to, scaledAmount) } -func (tokenERC20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { +func (tokenERC20) Deploy(b operations.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { ref, err := contract.MaybeDeployContract(b, erc20.Deploy, chain, contract.DeployInput[erc20.ConstructorArgs]{ - TypeAndVersion: deployment.NewTypeAndVersion(erc20.ContractType, *common_utils.Version_1_0_0), + TypeAndVersion: deployment.NewTypeAndVersion(erc20.ContractType, *utils.Version_1_0_0), ChainSelector: chain.Selector, Qualifier: &in.Symbol, Args: erc20.ConstructorArgs{ diff --git a/chains/evm/deployment/tokens/tokenimpl/token_tip20.go b/chains/evm/deployment/tokens/tokenimpl/token_tip20.go index 022c74d969..41ef4323ba 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_tip20.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_tip20.go @@ -12,7 +12,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" ) type tokenTIP20 struct{} @@ -30,8 +30,8 @@ func (tokenTIP20) Capabilities() CapabilitySet { } } -func (tokenTIP20) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, tip20.RevokeAdminRole, chain, contract.FunctionInput[common.Address]{ +func (tokenTIP20) RevokeAdminRole(b operations.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { + report, err := operations.ExecuteOperation(b, tip20.RevokeAdminRole, chain, contract.FunctionInput[common.Address]{ ChainSelector: chain.Selector, Address: token, Args: user, @@ -42,8 +42,8 @@ func (tokenTIP20) RevokeAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, use return []contract.WriteOutput{report.Output}, nil } -func (tokenTIP20) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, tip20.GrantAdminRole, chain, contract.FunctionInput[common.Address]{ +func (tokenTIP20) GrantAdminRole(b operations.Bundle, chain evm.Chain, token, user common.Address) ([]contract.WriteOutput, error) { + report, err := operations.ExecuteOperation(b, tip20.GrantAdminRole, chain, contract.FunctionInput[common.Address]{ ChainSelector: chain.Selector, Address: token, Args: user, @@ -54,8 +54,8 @@ func (tokenTIP20) GrantAdminRole(b cldf_ops.Bundle, chain evm.Chain, token, user return []contract.WriteOutput{report.Output}, nil } -func (tokenTIP20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, tip20.GrantIssuerRole, chain, contract.FunctionInput[common.Address]{ +func (tokenTIP20) GrantPoolRoles(b operations.Bundle, chain evm.Chain, token, pool, _ common.Address) ([]contract.WriteOutput, error) { + report, err := operations.ExecuteOperation(b, tip20.GrantIssuerRole, chain, contract.FunctionInput[common.Address]{ ChainSelector: chain.Selector, Address: token, Args: pool, @@ -66,12 +66,12 @@ func (tokenTIP20) GrantPoolRoles(b cldf_ops.Bundle, chain evm.Chain, token, pool return []contract.WriteOutput{report.Output}, nil } -func (tokenTIP20) SetCCIPAdmin(b cldf_ops.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { +func (tokenTIP20) SetCCIPAdmin(b operations.Bundle, chain evm.Chain, token, ccipAdmin common.Address) ([]contract.WriteOutput, error) { return nil, fmt.Errorf("CCIP admin role not supported for TIP-20 tokens") } -func (tokenTIP20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { - report, err := cldf_ops.ExecuteOperation(b, tip20.Transfer, chain, contract.FunctionInput[tip20.TransferArgs]{ +func (tokenTIP20) Transfer(b operations.Bundle, chain evm.Chain, token, to common.Address, scaledAmount *big.Int) ([]contract.WriteOutput, error) { + report, err := operations.ExecuteOperation(b, tip20.Transfer, chain, contract.FunctionInput[tip20.TransferArgs]{ ChainSelector: chain.Selector, Address: token, Args: tip20.TransferArgs{ @@ -86,7 +86,7 @@ func (tokenTIP20) Transfer(b cldf_ops.Bundle, chain evm.Chain, token, to common. return []contract.WriteOutput{report.Output}, nil } -func (tokenTIP20) Deploy(b cldf_ops.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { +func (tokenTIP20) Deploy(b operations.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { tokenRef, writes, err := tip20.DeployTokenViaFactory(b, chain, tip20.FactoryDeployArgs{ QuoteToken: common.Address{}, Currency: in.Currency, From 1522a299674d228d456d8a92f73d95df30082500 Mon Sep 17 00:00:00 2001 From: chris-de-leon-cll <147140544+chris-de-leon-cll@users.noreply.github.com> Date: Tue, 5 May 2026 10:58:02 -0700 Subject: [PATCH 14/14] fix: use correct BnM ERC677 bindings --- .../tokenimpl/token_burn_mint_erc677.go | 33 +- .../burn_mint_erc677/burn_mint_erc677.go | 312 +++++------------- .../burn_mint_erc677/burn_mint_erc677_test.go | 102 ++++-- 3 files changed, 175 insertions(+), 272 deletions(-) diff --git a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go index d5274992d2..812787f063 100644 --- a/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go +++ b/chains/evm/deployment/tokens/tokenimpl/token_burn_mint_erc677.go @@ -41,11 +41,7 @@ func (tokenBurnMintERC677) GrantAdminRole(_ operations.Bundle, _ evm.Chain, _, _ return nil, fmt.Errorf("admin role grant not supported for BurnMintERC677 token type") } -func (tokenBurnMintERC677) GrantPoolRoles( - b operations.Bundle, - chain evm.Chain, - token, pool, proposalExecutor common.Address, -) ([]contract.WriteOutput, error) { +func (tokenBurnMintERC677) GrantPoolRoles(b operations.Bundle, chain evm.Chain, token, pool, proposalExecutor common.Address) ([]contract.WriteOutput, error) { return burn_mint_erc677.PrepareGrantMintAndBurnRoles( b, chain, @@ -66,6 +62,29 @@ func (tokenBurnMintERC677) Transfer(b operations.Bundle, chain evm.Chain, token, return transferTokensERC20(b, chain, token, to, scaledAmount) } -func (tokenBurnMintERC677) Deploy(_ operations.Bundle, _ evm.Chain, _ tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { - return datastore.AddressRef{}, nil, fmt.Errorf("deploy BurnMintERC677 token is not implemented in tokenimpl; deploy out of band and record in datastore") +func (tokenBurnMintERC677) Deploy(b operations.Bundle, chain evm.Chain, in tokensapi.DeployTokenInput) (datastore.AddressRef, []contract.WriteOutput, error) { + maxSupply := big.NewInt(0) + if in.Supply != nil { + maxSupply = tokensapi.ScaleTokenAmount(new(big.Int).SetUint64(*in.Supply), in.Decimals) + } + + ref, err := contract.MaybeDeployContract(b, burn_mint_erc677.Deploy, chain, + contract.DeployInput[burn_mint_erc677.ConstructorArgs]{ + TypeAndVersion: deployment.NewTypeAndVersion(burn_mint_erc677.ContractType, *utils.Version_1_0_0), + ChainSelector: chain.Selector, + Qualifier: &in.Symbol, + Args: burn_mint_erc677.ConstructorArgs{ + Name: in.Name, + Symbol: in.Symbol, + Decimals: in.Decimals, + MaxSupply: maxSupply, + }, + }, + nil, + ) + if err != nil { + return datastore.AddressRef{}, nil, fmt.Errorf("failed to deploy BurnMintERC677 token: %w", err) + } + + return ref, nil, nil } diff --git a/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677/burn_mint_erc677.go b/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677/burn_mint_erc677.go index 0099cfbd7a..36d07998c7 100644 --- a/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677/burn_mint_erc677.go +++ b/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677/burn_mint_erc677.go @@ -4,179 +4,48 @@ import ( "context" "errors" "fmt" - "strings" + "math/big" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/operations/contract" + cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment" cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract" cciputils "github.com/smartcontractkit/chainlink-ccip/deployment/utils" - cldf_deployment "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc677" ) var ContractType cldf_deployment.ContractType = cciputils.BurnMintToken -const burnMintERC677ABI = `[{"inputs":[],"name":"BURN_MINT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"burnAndMinter","type":"address"}],"name":"grantMintAndBurnRoles","outputs":[],"stateMutability":"nonpayable","type":"function"}]` - +// AuthorityKind records whether an address may call grantMintAndBurnRoles on BurnMintERC677. +// The chainlink-evm token is Ownable (not AccessControl): only the owner may grant pool roles. type AuthorityKind string const ( - AuthorityBurnMintAdmin AuthorityKind = "burn-mint-admin" - AuthorityDefaultAdmin AuthorityKind = "default-admin" - AuthorityOwner AuthorityKind = "owner" - AuthorityUnauthorized AuthorityKind = "unauthorized" + AuthorityOwner AuthorityKind = "owner" + AuthorityUnauthorized AuthorityKind = "unauthorized" ) -type GrantMintAndBurnRolesAuthority struct { - Kind AuthorityKind - BurnMintAdminRole [32]byte - AdminRole [32]byte - Owner common.Address -} - -func (a GrantMintAndBurnRolesAuthority) CanGrantMintAndBurnRoles() bool { - return a.Kind == AuthorityBurnMintAdmin || a.Kind == AuthorityOwner +type ConstructorArgs struct { + Name string + Symbol string + Decimals uint8 + MaxSupply *big.Int } -type RoleAssignment struct { - Role [32]byte - To common.Address -} - -type burnMintERC677 struct { - address common.Address - contract *bind.BoundContract -} - -func newBurnMintERC677(address common.Address, backend bind.ContractBackend) (*burnMintERC677, error) { - parsed, err := abi.JSON(strings.NewReader(burnMintERC677ABI)) - if err != nil { - return nil, err - } - - return &burnMintERC677{ - address: address, - contract: bind.NewBoundContract(address, parsed, backend, backend, backend), - }, nil -} - -func (token *burnMintERC677) Owner(opts *bind.CallOpts) (common.Address, error) { - var out []interface{} - err := token.contract.Call(opts, &out, "owner") - if err != nil { - return common.Address{}, err - } - - owner := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) - return owner, nil -} - -func (token *burnMintERC677) BurnMintAdminRole(opts *bind.CallOpts) ([32]byte, error) { - var out []interface{} - err := token.contract.Call(opts, &out, "BURN_MINT_ADMIN_ROLE") - if err != nil { - return [32]byte{}, err - } - - role := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) - return role, nil -} - -func (token *burnMintERC677) GetRoleAdmin(opts *bind.CallOpts, role [32]byte) ([32]byte, error) { - var out []interface{} - err := token.contract.Call(opts, &out, "getRoleAdmin", role) - if err != nil { - return [32]byte{}, err - } - - adminRole := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) - return adminRole, nil -} - -func (token *burnMintERC677) HasRole(opts *bind.CallOpts, role [32]byte, account common.Address) (bool, error) { - var out []interface{} - err := token.contract.Call(opts, &out, "hasRole", role, account) - if err != nil { - return false, err - } - - hasRole := *abi.ConvertType(out[0], new(bool)).(*bool) - return hasRole, nil -} - -func (token *burnMintERC677) GrantRole(opts *bind.TransactOpts, role [32]byte, account common.Address) (*types.Transaction, error) { - return token.contract.Transact(opts, "grantRole", role, account) -} - -func (token *burnMintERC677) GrantMintAndBurnRoles(opts *bind.TransactOpts, burnAndMinter common.Address) (*types.Transaction, error) { - return token.contract.Transact(opts, "grantMintAndBurnRoles", burnAndMinter) +type GrantMintAndBurnRolesAuthority struct { + Kind AuthorityKind + Owner common.Address } -func (token *burnMintERC677) resolveGrantMintAndBurnRolesAuthority( - opts *bind.CallOpts, - caller common.Address, -) (GrantMintAndBurnRolesAuthority, error) { - burnMintAdminRole, accessControlErr := token.BurnMintAdminRole(opts) - if accessControlErr == nil { - hasBurnMintAdminRole, err := token.HasRole(opts, burnMintAdminRole, caller) - if err != nil { - return GrantMintAndBurnRolesAuthority{}, fmt.Errorf("failed to check burn/mint admin role for %s: %w", caller, err) - } - if hasBurnMintAdminRole { - return GrantMintAndBurnRolesAuthority{ - Kind: AuthorityBurnMintAdmin, - BurnMintAdminRole: burnMintAdminRole, - }, nil - } - - adminRole, err := token.GetRoleAdmin(opts, burnMintAdminRole) - if err != nil { - return GrantMintAndBurnRolesAuthority{}, fmt.Errorf("failed to get burn/mint admin role admin: %w", err) - } - hasRoleAdmin, err := token.HasRole(opts, adminRole, caller) - if err != nil { - return GrantMintAndBurnRolesAuthority{}, fmt.Errorf("failed to check burn/mint role admin for %s: %w", caller, err) - } - if hasRoleAdmin { - return GrantMintAndBurnRolesAuthority{ - Kind: AuthorityDefaultAdmin, - BurnMintAdminRole: burnMintAdminRole, - AdminRole: adminRole, - }, nil - } - - return GrantMintAndBurnRolesAuthority{ - Kind: AuthorityUnauthorized, - BurnMintAdminRole: burnMintAdminRole, - AdminRole: adminRole, - }, nil - } - - owner, ownerErr := token.Owner(opts) - if ownerErr == nil { - if owner == caller { - return GrantMintAndBurnRolesAuthority{ - Kind: AuthorityOwner, - Owner: owner, - }, nil - } - return GrantMintAndBurnRolesAuthority{ - Kind: AuthorityUnauthorized, - Owner: owner, - }, nil - } - - return GrantMintAndBurnRolesAuthority{}, fmt.Errorf( - "token does not expose a supported burn/mint role authority interface: BURN_MINT_ADMIN_ROLE failed: %w; owner failed: %w", - accessControlErr, - ownerErr, - ) +func (a GrantMintAndBurnRolesAuthority) CanGrantMintAndBurnRoles() bool { + return a.Kind == AuthorityOwner } +// ResolveGrantMintAndBurnRolesAuthority returns whether caller is the Ownable owner of the token. func ResolveGrantMintAndBurnRolesAuthority( ctx context.Context, backend bind.ContractBackend, @@ -190,36 +59,46 @@ func ResolveGrantMintAndBurnRolesAuthority( return GrantMintAndBurnRolesAuthority{}, errors.New("caller address cannot be zero") } - token, err := newBurnMintERC677(tokenAddress, backend) + token, err := burn_mint_erc677.NewBurnMintERC677(tokenAddress, backend) if err != nil { return GrantMintAndBurnRolesAuthority{}, err } - return token.resolveGrantMintAndBurnRolesAuthority(&bind.CallOpts{Context: ctx}, caller) + owner, err := token.Owner(&bind.CallOpts{Context: ctx}) + if err != nil { + return GrantMintAndBurnRolesAuthority{}, fmt.Errorf("failed to read token owner: %w", err) + } + if owner == caller { + return GrantMintAndBurnRolesAuthority{ + Kind: AuthorityOwner, + Owner: owner, + }, nil + } + return GrantMintAndBurnRolesAuthority{ + Kind: AuthorityUnauthorized, + Owner: owner, + }, nil } +// PrepareGrantMintAndBurnRoles plans grantMintAndBurnRoles for the pool on a BurnMintERC677 token. +// The on-chain function is owner-gated. IsAllowedCaller on GrantMintAndBurnRoles uses AllCallersAllowed +// because MCMS simulations often use the deployer key while the token owner is the timelock. +// When proposalExecutor is set and differs from the deployer, it must be the token owner. func PrepareGrantMintAndBurnRoles( b cldf_ops.Bundle, chain cldf_evm.Chain, input contract.FunctionInput[common.Address], proposalExecutor common.Address, ) ([]contract.WriteOutput, error) { - writes := []contract.WriteOutput{} - deployer := chain.DeployerKey.From - if proposalExecutor == (common.Address{}) || proposalExecutor == deployer { - deployerAuthority, err := ResolveGrantMintAndBurnRolesAuthority(b.GetContext(), chain.Client, input.Address, deployer) - if err == nil && deployerAuthority.Kind == AuthorityDefaultAdmin { - grantAdminReport, execErr := cldf_ops.ExecuteOperation(b, GrantRole, chain, contract.FunctionInput[RoleAssignment]{ - ChainSelector: input.ChainSelector, - Address: input.Address, - Args: RoleAssignment{ - Role: deployerAuthority.BurnMintAdminRole, - To: deployer, - }, - }) - if execErr != nil { - return nil, fmt.Errorf("failed to grant burn/mint admin role to deployer %s: %w", deployer, execErr) - } - writes = append(writes, grantAdminReport.Output) + if proposalExecutor != (common.Address{}) && proposalExecutor != chain.DeployerKey.From { + auth, err := ResolveGrantMintAndBurnRolesAuthority(b.GetContext(), chain.Client, input.Address, proposalExecutor) + if err != nil { + return nil, fmt.Errorf("failed to validate proposal executor %s: %w", proposalExecutor, err) + } + if auth.Kind != AuthorityOwner { + return nil, fmt.Errorf( + "proposal executor %s is not the token owner (owner=%s) for token %s; cannot grant mint/burn roles", + proposalExecutor, auth.Owner, input.Address, + ) } } @@ -227,82 +106,51 @@ func PrepareGrantMintAndBurnRoles( if err != nil { return nil, err } - writes = append(writes, grantReport.Output) - if grantReport.Output.Executed() || proposalExecutor == (common.Address{}) { - return writes, nil - } - proposalAuthority, err := ResolveGrantMintAndBurnRolesAuthority(b.GetContext(), chain.Client, input.Address, proposalExecutor) - if err != nil { - return nil, fmt.Errorf("failed to validate proposal executor %s can grant burn/mint roles: %w", proposalExecutor, err) - } - switch proposalAuthority.Kind { - case AuthorityBurnMintAdmin, AuthorityOwner: - return writes, nil - case AuthorityDefaultAdmin: - grantAdminReport, execErr := cldf_ops.ExecuteOperation(b, GrantRole, chain, contract.FunctionInput[RoleAssignment]{ - ChainSelector: input.ChainSelector, - Address: input.Address, - Args: RoleAssignment{ - Role: proposalAuthority.BurnMintAdminRole, - To: proposalExecutor, - }, - }) - if execErr != nil { - return nil, fmt.Errorf("failed to grant burn/mint admin role to proposal executor %s: %w", proposalExecutor, execErr) - } - return append([]contract.WriteOutput{grantAdminReport.Output}, writes...), nil - default: - return nil, fmt.Errorf("proposal executor %s cannot grant burn/mint roles for token %s", proposalExecutor, input.Address) - } + return []contract.WriteOutput{grantReport.Output}, nil } -var GrantRole = contract.NewWrite(contract.WriteParams[RoleAssignment, *burnMintERC677]{ - Name: "burn_mint_erc677:grant-role", +var GrantMintAndBurnRoles = contract.NewWrite(contract.WriteParams[common.Address, *burn_mint_erc677.BurnMintERC677]{ + Name: "burn_mint_erc677:grant-mint-and-burn-roles", Version: cciputils.Version_1_0_0, - Description: "Grant role on AccessControl-compatible burn/mint token contract", + Description: "Grant mint and burn roles on BurnMintERC677 (owner-only on-chain)", ContractType: ContractType, - ContractABI: burnMintERC677ABI, - NewContract: newBurnMintERC677, - IsAllowedCaller: func(token *burnMintERC677, opts *bind.CallOpts, caller common.Address, input RoleAssignment) (bool, error) { - roleAdmin, err := token.GetRoleAdmin(opts, input.Role) - if err != nil { - return false, err - } - return token.HasRole(opts, roleAdmin, caller) - }, - Validate: func(input RoleAssignment) error { - if input.To == (common.Address{}) { - return errors.New("role assignee address cannot be zero") + ContractABI: burn_mint_erc677.BurnMintERC677ABI, + NewContract: burn_mint_erc677.NewBurnMintERC677, + // On-chain only the owner may call grantMintAndBurnRoles. Do not use OnlyOwner here: + // MCMS/timelock flows simulate with the deployer key while ownership is the timelock + IsAllowedCaller: contract.AllCallersAllowed[*burn_mint_erc677.BurnMintERC677, common.Address], + Validate: func(address common.Address) error { + if address == (common.Address{}) { + return errors.New("burn and minter address cannot be zero") } return nil }, - CallContract: func(token *burnMintERC677, opts *bind.TransactOpts, input RoleAssignment) (*types.Transaction, error) { - return token.GrantRole(opts, input.Role, input.To) + CallContract: func(token *burn_mint_erc677.BurnMintERC677, opts *bind.TransactOpts, input common.Address) (*types.Transaction, error) { + return token.GrantMintAndBurnRoles(opts, input) }, }) -var GrantMintAndBurnRoles = contract.NewWrite(contract.WriteParams[common.Address, *burnMintERC677]{ - Name: "burn_mint_erc677:grant-mint-and-burn-roles", - Version: cciputils.Version_1_0_0, - Description: "Grant mint and burn role on BurnMintERC677 token contract", - ContractType: ContractType, - ContractABI: burnMintERC677ABI, - NewContract: newBurnMintERC677, - IsAllowedCaller: func(token *burnMintERC677, opts *bind.CallOpts, caller common.Address, input common.Address) (bool, error) { - authority, err := token.resolveGrantMintAndBurnRolesAuthority(opts, caller) - if err != nil { - return false, err - } - return authority.CanGrantMintAndBurnRoles(), nil +var Deploy = contract.NewDeploy(contract.DeployParams[ConstructorArgs]{ + Name: "burn_mint_erc677:deploy", + Version: cciputils.Version_1_0_0, + Description: "Deploys the BurnMintERC677 token contract", + ContractMetadata: burn_mint_erc677.BurnMintERC677MetaData, + BytecodeByTypeAndVersion: map[string]contract.Bytecode{ + cldf_deployment.NewTypeAndVersion(ContractType, *cciputils.Version_1_0_0).String(): { + EVM: common.FromHex(burn_mint_erc677.BurnMintERC677Bin), + }, }, - Validate: func(address common.Address) error { - if address == (common.Address{}) { - return errors.New("burn and minter address cannot be zero") + Validate: func(args ConstructorArgs) error { + if args.Name == "" { + return errors.New("name is required") + } + if args.Symbol == "" { + return errors.New("symbol is required") + } + if args.MaxSupply == nil { + return errors.New("maxSupply is required") } return nil }, - CallContract: func(token *burnMintERC677, opts *bind.TransactOpts, input common.Address) (*types.Transaction, error) { - return token.GrantMintAndBurnRoles(opts, input) - }, }) diff --git a/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677/burn_mint_erc677_test.go b/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677/burn_mint_erc677_test.go index eaba694c82..f017377299 100644 --- a/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677/burn_mint_erc677_test.go +++ b/chains/evm/deployment/v1_0_0/operations/burn_mint_erc677/burn_mint_erc677_test.go @@ -10,72 +10,108 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/operations/contract" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/latest/cross_chain_token" "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + wrappers "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/burn_mint_erc677" ) -func TestPrepareGrantMintAndBurnRolesAddsAdminGrantForDefaultAdminProposalExecutor(t *testing.T) { +func TestResolveGrantMintAndBurnRolesAuthority_owner(t *testing.T) { const selector uint64 = 5009297550715157269 e, err := environment.New(t.Context(), environment.WithEVMSimulated(t, []uint64{selector})) require.NoError(t, err) chain := e.BlockChains.EVMChains()[selector] - timelock := common.HexToAddress("0x000000000000000000000000000000000000dEaD") pool := common.HexToAddress("0x000000000000000000000000000000000000bEEF") - tokenAddress, tx, _, err := cross_chain_token.DeployCrossChainToken( + tokenAddress, tx, _, err := wrappers.DeployBurnMintERC677( chain.DeployerKey, chain.Client, - cross_chain_token.BaseERC20ConstructorParams{ - Name: "Cross Chain Test Token", - Symbol: "CCTT", - MaxSupply: big.NewInt(0), - PreMint: big.NewInt(0), - PreMintRecipient: common.Address{}, - Decimals: 18, - CcipAdmin: timelock, - }, - common.Address{}, - timelock, + "Test Token", + "TT", + 18, + big.NewInt(0), ) require.NoError(t, err) _, err = chain.Confirm(tx) require.NoError(t, err) - authority, err := ResolveGrantMintAndBurnRolesAuthority(t.Context(), chain.Client, tokenAddress, timelock) + auth, err := ResolveGrantMintAndBurnRolesAuthority(t.Context(), chain.Client, tokenAddress, chain.DeployerKey.From) require.NoError(t, err) - require.Equal(t, AuthorityDefaultAdmin, authority.Kind) + require.Equal(t, AuthorityOwner, auth.Kind) + require.Equal(t, chain.DeployerKey.From, auth.Owner) writes, err := PrepareGrantMintAndBurnRoles(e.OperationsBundle, chain, contract.FunctionInput[common.Address]{ ChainSelector: selector, Address: tokenAddress, Args: pool, - }, timelock) + }, common.Address{}) + require.NoError(t, err) + require.Len(t, writes, 1) + + parsedABI, err := abi.JSON(strings.NewReader(wrappers.BurnMintERC677ABI)) + require.NoError(t, err) + wantData, err := parsedABI.Pack("grantMintAndBurnRoles", pool) + require.NoError(t, err) + require.Equal(t, wantData, writes[0].Tx.Data) +} + +func TestResolveGrantMintAndBurnRolesAuthority_unauthorizedCaller(t *testing.T) { + const selector uint64 = 5009297550715157269 + e, err := environment.New(t.Context(), environment.WithEVMSimulated(t, []uint64{selector})) require.NoError(t, err) - require.Len(t, writes, 2) - require.False(t, writes[0].Executed()) - require.False(t, writes[1].Executed()) - batchOp, err := contract.NewBatchOperationFromWrites(writes) + chain := e.BlockChains.EVMChains()[selector] + tokenAddress, tx, _, err := wrappers.DeployBurnMintERC677( + chain.DeployerKey, + chain.Client, + "Test Token", + "TT", + 18, + big.NewInt(0), + ) + require.NoError(t, err) + _, err = chain.Confirm(tx) + require.NoError(t, err) + + stranger := common.HexToAddress("0x000000000000000000000000000000000000CAFE") + pool := common.HexToAddress("0x000000000000000000000000000000000000bEEF") + + auth, err := ResolveGrantMintAndBurnRolesAuthority(t.Context(), chain.Client, tokenAddress, stranger) require.NoError(t, err) - require.Len(t, batchOp.Transactions, 2) - require.Equal(t, tokenAddress.Hex(), batchOp.Transactions[0].To) - require.Equal(t, tokenAddress.Hex(), batchOp.Transactions[1].To) + require.Equal(t, AuthorityUnauthorized, auth.Kind) + + _, err = PrepareGrantMintAndBurnRoles(e.OperationsBundle, chain, contract.FunctionInput[common.Address]{ + ChainSelector: selector, + Address: tokenAddress, + Args: pool, + }, stranger) + require.ErrorContains(t, err, "not the token owner") +} - parsedABI, err := abi.JSON(strings.NewReader(burnMintERC677ABI)) +func TestPrepareGrantMintAndBurnRoles_timelockMustBeOwnerWhenSet(t *testing.T) { + const selector uint64 = 5009297550715157269 + e, err := environment.New(t.Context(), environment.WithEVMSimulated(t, []uint64{selector})) require.NoError(t, err) - grantRoleData, err := parsedABI.Pack("grantRole", authority.BurnMintAdminRole, timelock) + + chain := e.BlockChains.EVMChains()[selector] + timelock := common.HexToAddress("0x000000000000000000000000000000000000dEaD") + pool := common.HexToAddress("0x000000000000000000000000000000000000bEEF") + + tokenAddress, tx, _, err := wrappers.DeployBurnMintERC677( + chain.DeployerKey, + chain.Client, + "Test Token", + "TT", + 18, + big.NewInt(0), + ) require.NoError(t, err) - grantMintAndBurnRolesData, err := parsedABI.Pack("grantMintAndBurnRoles", pool) + _, err = chain.Confirm(tx) require.NoError(t, err) - require.Equal(t, grantRoleData, batchOp.Transactions[0].Data) - require.Equal(t, grantMintAndBurnRolesData, batchOp.Transactions[1].Data) - unauthorizedExecutor := common.HexToAddress("0x000000000000000000000000000000000000CAFE") _, err = PrepareGrantMintAndBurnRoles(e.OperationsBundle, chain, contract.FunctionInput[common.Address]{ ChainSelector: selector, Address: tokenAddress, Args: pool, - }, unauthorizedExecutor) - require.ErrorContains(t, err, "cannot grant burn/mint roles") + }, timelock) + require.ErrorContains(t, err, "not the token owner") }