From bbc0f7a58c982af19c45dd62b38ed068c42d143e Mon Sep 17 00:00:00 2001 From: Graham Goh Date: Fri, 12 Jun 2026 17:59:49 +1000 Subject: [PATCH] feat: new mcms deploy changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor and redesigne to better accomodate new chains and utilising the new utils method integrated in cldf. The deploy changeset is chain agonistic and uses the registry pattern to add new chains that we want to deploy, each chain specific implementation is localised in its own folder eg mcms/evm. new structure ``` mcms/ ├── changesets/ │ ├── deploy/ # deploy changeset + deploy registry + types │ │ ├── changeset.go │ │ ├── registry.go │ │ └── types.go │ └── setconfig/ # setconfig changeset + setconfig registry + types │ ├── changeset.go │ ├── registry.go │ └── types.go ├── evm/ │ ├── deploy/ # registers with changesets/deploy │ └── setconfig/ # registers with changesets/setconfig ├── solana/ │ └── ... └── operations/ # legacy — consider moving under evm/ or legacy/ ``` This new folder structure feels more intuitive and user wanting to extend mcms with new chain should be able to follow the pattern easily JIRA: https://smartcontract-it.atlassian.net/browse/CLD-2719 --- go.mod | 21 +- go.sum | 42 +-- mcms/changesets/deploy/changeset.go | 128 ++++++++ mcms/changesets/deploy/changeset_evm_test.go | 196 ++++++++++++ mcms/changesets/deploy/doc.go | 89 ++++++ mcms/changesets/deploy/families.go | 16 + mcms/changesets/deploy/registry.go | 103 +++++++ mcms/changesets/deploy/types.go | 38 +++ mcms/evm/deploy/addresses.go | 74 +++++ mcms/evm/deploy/register.go | 44 +++ mcms/evm/deploy/sequence.go | 300 +++++++++++++++++++ 11 files changed, 1024 insertions(+), 27 deletions(-) create mode 100644 mcms/changesets/deploy/changeset.go create mode 100644 mcms/changesets/deploy/changeset_evm_test.go create mode 100644 mcms/changesets/deploy/doc.go create mode 100644 mcms/changesets/deploy/families.go create mode 100644 mcms/changesets/deploy/registry.go create mode 100644 mcms/changesets/deploy/types.go create mode 100644 mcms/evm/deploy/addresses.go create mode 100644 mcms/evm/deploy/register.go create mode 100644 mcms/evm/deploy/sequence.go diff --git a/go.mod b/go.mod index 4ff5574..b341dcc 100644 --- a/go.mod +++ b/go.mod @@ -18,10 +18,10 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.102 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260415165642-49f23e4d76cc - github.com/smartcontractkit/chainlink-deployments-framework v0.109.0 + github.com/smartcontractkit/chainlink-deployments-framework v0.110.0 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260421142741-9c7fbaf7c828 github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 - github.com/smartcontractkit/mcms v0.45.0 + github.com/smartcontractkit/mcms v0.47.1-0.20260609163952-0b2bf692ba6a github.com/smartcontractkit/quarantine v0.0.0-20251203215908-fd0551c6adf9 github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 github.com/spf13/cast v1.10.0 @@ -34,6 +34,7 @@ require ( ) require ( + cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect @@ -47,14 +48,14 @@ require ( github.com/avast/retry-go/v4 v4.7.0 // indirect github.com/awalterschulze/gographviz v2.0.3+incompatible // indirect github.com/aws/aws-sdk-go v1.55.8 // indirect - github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.11 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect - github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.59.2 // indirect + github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.61.2 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect github.com/aws/aws-sdk-go-v2/service/kms v1.50.1 // indirect @@ -62,7 +63,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect - github.com/aws/smithy-go v1.26.0 // indirect + github.com/aws/smithy-go v1.27.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -129,7 +130,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.30.2 // indirect + github.com/go-playground/validator/v10 v10.30.3 // indirect github.com/go-resty/resty/v2 v2.17.2 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect @@ -236,6 +237,7 @@ require ( github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/smartcontractkit/chainlink-aptos v0.0.0-20260430175646-295a7f9a1500 // indirect + github.com/smartcontractkit/chainlink-canton v0.0.0-20260609155219-dcbe77d4a320 // indirect github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735 // indirect @@ -243,11 +245,12 @@ require ( github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect github.com/smartcontractkit/chainlink-protos/op-catalog v0.1.0 // indirect github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67 // indirect - github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.4 // indirect + github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.5 // indirect github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 // indirect github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260514223130-48bc90aca745 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260408092456-3c6369888d4a // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect + github.com/smartcontractkit/go-daml v0.0.0-20260604143752-c6f6567940ba // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/smartcontractkit/libocr v0.0.0-20260403184524-b6409238958d // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect diff --git a/go.sum b/go.sum index 2505a1b..4667414 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= @@ -82,22 +84,22 @@ github.com/aws/aws-sdk-go v1.22.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= -github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= -github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2 v1.41.11 h1:9PRf7jyTMEUM6fuNRAJa2mO/skJfrF50rENJwf2LXqw= +github.com/aws/aws-sdk-go-v2 v1.41.11/go.mod h1:iiUX27gOXRuYaoeUVXhUpPwjJHzISfPAjjcuhUbLSVs= github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 h1:8sPbKi1/KRHwl5oR3qN9mUXestCeHuaRutxylnr/eVY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27/go.mod h1:QV9IVIopJ1dpQUno0f9VYDUwOEjj8u0iEJ4JiZVre3Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 h1:9d8AoASQY9UwrOSmiJ7uSM0MGUPFhnenwSvpaFfat2c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27/go.mod h1:x0rldpsnUQaQIs4Rh+Vwm9Z/0vI6BxadGtsgJfZFb8s= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= -github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.59.2 h1:I1oExVl2b6nJGv//TcU78k9Covm/htQ5gwPIcDlM2PI= -github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.59.2/go.mod h1:sxvHFUS0fM9Y3BpmDvwrO9fnQC0CrFSG8KD9THjv6k4= +github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.61.2 h1:Y72mUMDDsuH4oPVmO0OH798GLF3UaSY86x/KyV3pGWI= +github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.61.2/go.mod h1:L9TyMmB+T+9gsQ0LBcXujgHLZUngEPDTUm2ABkACdIU= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= @@ -112,8 +114,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= -github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= -github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= +github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= @@ -368,8 +370,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= -github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= +github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8= +github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -847,6 +849,8 @@ github.com/smartcontractkit/chain-selectors v1.0.102 h1:qYP4+72HfvogCHR5ymwRFee3 github.com/smartcontractkit/chain-selectors v1.0.102/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260430175646-295a7f9a1500 h1:045jrHCLI+MpeAyByJkyHbEjq0+aTPt04C7+sbsNNtw= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260430175646-295a7f9a1500/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= +github.com/smartcontractkit/chainlink-canton v0.0.0-20260609155219-dcbe77d4a320 h1:ix4tCtSTB7S2XGll+uqnhrqAQ+2iW/Zk/vnPjBMYRB0= +github.com/smartcontractkit/chainlink-canton v0.0.0-20260609155219-dcbe77d4a320/go.mod h1:WKmNUX4oy8IvB66ukudrE99uaXjlZ7WghCDwHOTyB1c= github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc h1:mvobZx5JV5PhG/9IXPReV+8mAGnupl0HIWQZ43zxzd4= github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc/go.mod h1:gzCVLUlNov/zFXSC7G6zcGkZU1IfNOHaakbAPDe5Woc= github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260415165642-49f23e4d76cc h1:War93neyFmv7pzuElZeZC3qc/OfGtLvEXvqL3qeBfM0= @@ -855,8 +859,8 @@ github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89/go.mod h1:G2AII0QmWzXx8Ag9IKnGN3h/gwwNnhHUOCviJievdvo= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10/go.mod h1:oiDa54M0FwxevWwyAX773lwdWvFYYlYHHQV1LQ5HpWY= -github.com/smartcontractkit/chainlink-deployments-framework v0.109.0 h1:sURmdL2OnO55SETWIFzIEqQH7RCiHJyW7on8HvfnLY8= -github.com/smartcontractkit/chainlink-deployments-framework v0.109.0/go.mod h1:ubpvoLoRdru8IQHw3TFr7KthbjYpAwmiRmvvNCf2daA= +github.com/smartcontractkit/chainlink-deployments-framework v0.110.0 h1:FkrP1bqV7+6aBuU7fMghz73AZ0SKh2LuQomsiUVNRsU= +github.com/smartcontractkit/chainlink-deployments-framework v0.110.0/go.mod h1:+NGoMnU8UBGc9e+QLFXQ3HdLxLoZ9HDjoSBq0WeZyJE= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260421142741-9c7fbaf7c828 h1:BmsFk/TSHL6dPPR86GTqgSrUXLSINNFC6cfpFRrQX+4= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260421142741-9c7fbaf7c828/go.mod h1:a260YnLyWq2NHLUN5cSVyMGk9nhO6RguCaTI2rsVqyA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735 h1:5bxDnwI0wuPoC0H5H3H2n9CnQPb5iakR6UmAY4j8KUg= @@ -871,8 +875,8 @@ github.com/smartcontractkit/chainlink-protos/op-catalog v0.1.0 h1:hGEJFD2X3oNIPX github.com/smartcontractkit/chainlink-protos/op-catalog v0.1.0/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs= github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67 h1:NNvPOgvf5vbOYVLxLST+5E88iPOAnpmzZGPihEx8DFc= github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67/go.mod h1:k1HSbHyPaQWPOj6lXDIAe04EuwbC5ge1nK+cpG2E8hE= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.4 h1:8M+2pA0qx9rXaxmpKouUHj983vQCGzztHkG0XjE5Eew= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.4/go.mod h1:nyOjn4ADJGqRMe3+4ZXSV+J/7nWb1H2Vx8Qk57eLRYA= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.5 h1:EiQx0LCPzxlfO9piSPeMCVSZAnp/BxAsPIGh/jBal18= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.5/go.mod h1:nyOjn4ADJGqRMe3+4ZXSV+J/7nWb1H2Vx8Qk57eLRYA= github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 h1:RwZXxdIAOyjp6cwc9Quxgr38k8r7ACz+Lxh9o/A6oH0= github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5/go.mod h1:kHYJnZUqiPF7/xN5273prV+srrLJkS77GbBXHLKQpx0= github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260514223130-48bc90aca745 h1:eieKLvYuzwBPh/FdbUS1gnIanI86zgWby1L10o90g4o= @@ -883,12 +887,14 @@ github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.202510141 github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014120029-d73d15cc23f7/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad h1:lgHxTHuzJIF3Vj6LSMOnjhqKgRqYW+0MV2SExtCYL1Q= github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= +github.com/smartcontractkit/go-daml v0.0.0-20260604143752-c6f6567940ba h1:peYJwUWOv54aigdk1VFzkmXdZmZK4xixfxv0Af1l6/I= +github.com/smartcontractkit/go-daml v0.0.0-20260604143752-c6f6567940ba/go.mod h1:SqWfl3Bp9NleC9jhzFUaOGzOZeKfldpY4QOW6A6NSNM= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20260403184524-b6409238958d h1:PvXor5Fjer7FIONSqYXbpd1LkA14hWrlAyxXzOrC9t8= github.com/smartcontractkit/libocr v0.0.0-20260403184524-b6409238958d/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= -github.com/smartcontractkit/mcms v0.45.0 h1:6Zx80KKLQOPXLhvrRkJKClANnBJmPa/r69CV5UUq/0I= -github.com/smartcontractkit/mcms v0.45.0/go.mod h1:/uOE69QmF7opKlaBNzp8djypmBoYSW0mk4V2iKWP418= +github.com/smartcontractkit/mcms v0.47.1-0.20260609163952-0b2bf692ba6a h1:Md5YPRT2oIgSD6U/gvy3BkB3hWXufu+eWIA+0ZFkngc= +github.com/smartcontractkit/mcms v0.47.1-0.20260609163952-0b2bf692ba6a/go.mod h1:7ZPOnN9CeoiiZkGQSPsyk23X+XFEtxQYpIx7kKy2ZP0= github.com/smartcontractkit/quarantine v0.0.0-20251203215908-fd0551c6adf9 h1:MOEuXYogv+RStASb8dWsyescu/xkigSi/Sv45NEjV7A= github.com/smartcontractkit/quarantine v0.0.0-20251203215908-fd0551c6adf9/go.mod h1:iwy4yWFuK+1JeoIRTaSOA9pl+8Kf//26zezxEXrAQEQ= github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 h1:zxcODLrFytOKmAd8ty8S/XK6WcIEJEgRBaL7sY/7l4Y= diff --git a/mcms/changesets/deploy/changeset.go b/mcms/changesets/deploy/changeset.go new file mode 100644 index 0000000..22ca1cb --- /dev/null +++ b/mcms/changesets/deploy/changeset.go @@ -0,0 +1,128 @@ +package deploy + +import ( + "errors" + "fmt" + "sync" + + chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-deployments-framework/changeset/sequenceutils" + cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "golang.org/x/sync/errgroup" +) + +var _ cldf.ChangeSetV2[Input] = Changeset{} + +// Input configures MCMS with timelock deployment per chain selector. +type Input struct { + ConfigByChain map[uint64]cldfproposalutils.MCMSWithTimelockConfig +} + +// Changeset deploys MCMS with timelock across all configured chains. +// +// Each chain is dispatched in parallel to its family's registered sequence. +// New addresses are written to the datastore. +type Changeset struct{} + +func (Changeset) VerifyPreconditions(env cldf.Environment, input Input) error { + if len(input.ConfigByChain) == 0 { + return errors.New("no chain configs provided") + } + + byFamily, err := groupByFamily(input.ConfigByChain) + if err != nil { + return err + } + + for family, chains := range byFamily { + reg, err := get(family) + if err != nil { + return err + } + if reg.Verify != nil { + if err := reg.Verify(env, chains); err != nil { + return fmt.Errorf("family %s: %w", family, err) + } + } + } + + return nil +} + +func (Changeset) Apply(env cldf.Environment, input Input) (cldf.ChangesetOutput, error) { + deps := Deps{ + BlockChains: env.BlockChains, + DataStore: env.DataStore, + } + + var ( + mu sync.Mutex + results []sequenceutils.OnChainOutput + reports []operations.Report[any, any] + eg errgroup.Group + ) + + for selector, cfg := range input.ConfigByChain { + family, err := chainselectors.GetSelectorFamily(selector) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("chain selector %d: %w", selector, err) + } + + reg, err := get(family) + if err != nil { + return cldf.ChangesetOutput{}, err + } + + in := ChainInput{ChainSelector: selector, Config: cfg} + eg.Go(func() error { + report, err := operations.ExecuteSequence(env.OperationsBundle, reg.Sequence, deps, in) + if err != nil { + return fmt.Errorf("chain %d: %w", selector, err) + } + mu.Lock() + results = append(results, report.Output) + reports = append(reports, report.ExecutionReports...) + mu.Unlock() + + return nil + }) + } + + if err := eg.Wait(); err != nil { + return cldf.ChangesetOutput{}, err + } + + agg := mergeOutputs(results) + + ds := cldfdatastore.NewMemoryDataStore() + if env.DataStore != nil { + if err := ds.Merge(env.DataStore); err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("merge environment datastore: %w", err) + } + } + if err := ds.WriteMetadata(agg.Metadata); err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("write deployment metadata to datastore: %w", err) + } + + return cldf.NewOutputBuilder(env, ds).WithOperationsReports(reports).Build() +} + +// mergeOutputs combines all per-chain OnChainOutput results into a single +// aggregate by appending their metadata slices and batch operations. +func mergeOutputs(outputs []sequenceutils.OnChainOutput) sequenceutils.OnChainOutput { + var agg sequenceutils.OnChainOutput + for _, out := range outputs { + agg.BatchOps = append(agg.BatchOps, out.BatchOps...) + agg.Metadata.Addresses = append(agg.Metadata.Addresses, out.Metadata.Addresses...) + agg.Metadata.Contracts = append(agg.Metadata.Contracts, out.Metadata.Contracts...) + agg.Metadata.Chains = append(agg.Metadata.Chains, out.Metadata.Chains...) + if out.Metadata.Env != nil { + agg.Metadata.Env = out.Metadata.Env + } + } + + return agg +} diff --git a/mcms/changesets/deploy/changeset_evm_test.go b/mcms/changesets/deploy/changeset_evm_test.go new file mode 100644 index 0000000..4400c56 --- /dev/null +++ b/mcms/changesets/deploy/changeset_evm_test.go @@ -0,0 +1,196 @@ +package deploy_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + chain_selectors "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + mcmstypes "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/cld-changesets/internal/semvers" + "github.com/smartcontractkit/cld-changesets/internal/testutil/evmtest" + evmstate "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/evm" + "github.com/smartcontractkit/cld-changesets/mcms/changesets/deploy" + + // Import EVM deploy package to auto-register the EVM family via init(). + _ "github.com/smartcontractkit/cld-changesets/mcms/evm/deploy" +) + +// mcmsConfig returns a minimal valid MCMS+timelock config for tests. +func mcmsConfig(t *testing.T, minDelay int64) cldfproposalutils.MCMSWithTimelockConfig { + t.Helper() + + signer := func(hex string) mcmstypes.Config { + return mcmstypes.Config{ + Quorum: 1, + Signers: []common.Address{common.HexToAddress(hex)}, + GroupSigners: []mcmstypes.Config{}, + } + } + + return cldfproposalutils.MCMSWithTimelockConfig{ + Proposer: signer("0x0000000000000000000000000000000000000001"), + Canceller: signer("0x0000000000000000000000000000000000000002"), + Bypasser: signer("0x0000000000000000000000000000000000000003"), + TimelockMinDelay: big.NewInt(minDelay), + } +} + +func TestDeployMCMSWithTimelock_FreshDeploy(t *testing.T) { + t.Parallel() + + selector := chain_selectors.TEST_90000001.Selector + + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + environment.WithLogger(logger.Test(t)), + )) + require.NoError(t, err) + + cfg := mcmsConfig(t, 0) + err = rt.Exec(runtime.ChangesetTask(deploy.Changeset{}, deploy.Input{ + ConfigByChain: map[uint64]cldfproposalutils.MCMSWithTimelockConfig{ + selector: cfg, + }, + })) + require.NoError(t, err) + + var reportsLen int + for _, out := range rt.State().Outputs { + reportsLen += len(out.Reports) + } + require.NotZero(t, reportsLen, "expected operation reports in changeset output") + + refs, err := rt.State().DataStore.Addresses().Fetch() + require.NoError(t, err) + require.Len(t, refs, 5, "expected 5 MCMS contract address refs") + + contractTypes := make(map[datastore.ContractType]struct{}, 5) + for _, ref := range refs { + require.Equal(t, selector, ref.ChainSelector) + require.True(t, semvers.V1_0_0.Equal(ref.Version)) + contractTypes[ref.Type] = struct{}{} + } + require.Contains(t, contractTypes, datastore.ContractType(mcmscontracts.BypasserManyChainMultisig)) + require.Contains(t, contractTypes, datastore.ContractType(mcmscontracts.CancellerManyChainMultisig)) + require.Contains(t, contractTypes, datastore.ContractType(mcmscontracts.ProposerManyChainMultisig)) + require.Contains(t, contractTypes, datastore.ContractType(mcmscontracts.RBACTimelock)) + require.Contains(t, contractTypes, datastore.ContractType(mcmscontracts.CallProxy)) + + state, err := evmstate.MaybeLoadMCMSWithTimelockStateDataStore(rt.Environment(), []uint64{selector}) + require.NoError(t, err) + require.NoError(t, state[selector].Validate()) + + chain := rt.Environment().BlockChains.EVMChains()[selector] + timelockInspector := mcmsevmsdk.NewTimelockInspector(chain.Client) + timelockAddr := state[selector].Timelock.Address().Hex() + + proposers, err := timelockInspector.GetProposers(t.Context(), timelockAddr) + require.NoError(t, err) + require.Equal(t, []string{state[selector].ProposerMcm.Address().Hex()}, proposers) + + executors, err := timelockInspector.GetExecutors(t.Context(), timelockAddr) + require.NoError(t, err) + require.Equal(t, []string{state[selector].CallProxy.Address().Hex()}, executors) + + cancellers, err := timelockInspector.GetCancellers(t.Context(), timelockAddr) + require.NoError(t, err) + require.ElementsMatch(t, []string{ + state[selector].CancellerMcm.Address().Hex(), + state[selector].ProposerMcm.Address().Hex(), + state[selector].BypasserMcm.Address().Hex(), + }, cancellers) + + bypassers, err := timelockInspector.GetBypassers(t.Context(), timelockAddr) + require.NoError(t, err) + require.Equal(t, []string{state[selector].BypasserMcm.Address().Hex()}, bypassers) +} + +func TestDeployMCMSWithTimelock_PartialDeploy(t *testing.T) { + t.Parallel() + + selector := chain_selectors.TEST_90000001.Selector + + bypasserAddr := evmtest.RandomAddress() + cancellerAddr := evmtest.RandomAddress() + + v := semvers.V1_0_0 + ds := datastore.NewMemoryDataStore() + require.NoError(t, ds.Addresses().Add(datastore.AddressRef{ + ChainSelector: selector, + Address: bypasserAddr.Hex(), + Type: datastore.ContractType(mcmscontracts.BypasserManyChainMultisig), + Version: &v, + })) + require.NoError(t, ds.Addresses().Add(datastore.AddressRef{ + ChainSelector: selector, + Address: cancellerAddr.Hex(), + Type: datastore.ContractType(mcmscontracts.CancellerManyChainMultisig), + Version: &v, + })) + + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + environment.WithLogger(logger.Test(t)), + environment.WithDatastore(ds.Seal()), + )) + require.NoError(t, err) + + err = rt.Exec(runtime.ChangesetTask(deploy.Changeset{}, deploy.Input{ + ConfigByChain: map[uint64]cldfproposalutils.MCMSWithTimelockConfig{ + selector: mcmsConfig(t, 0), + }, + })) + require.NoError(t, err) + + refs, err := rt.State().DataStore.Addresses().Fetch() + require.NoError(t, err) + require.Len(t, refs, 5) + + state, err := evmstate.MaybeLoadMCMSWithTimelockStateDataStore(rt.Environment(), []uint64{selector}) + require.NoError(t, err) + require.NoError(t, state[selector].Validate()) + require.Equal(t, bypasserAddr, state[selector].BypasserMcm.Address()) + require.Equal(t, cancellerAddr, state[selector].CancellerMcm.Address()) +} + +func TestDeployMCMSWithTimelock_MultiChain(t *testing.T) { + t.Parallel() + + selector1 := chain_selectors.TEST_90000001.Selector + selector2 := chain_selectors.TEST_90000002.Selector + selectors := []uint64{selector1, selector2} + + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, selectors), + environment.WithLogger(logger.Test(t)), + )) + require.NoError(t, err) + + err = rt.Exec(runtime.ChangesetTask(deploy.Changeset{}, deploy.Input{ + ConfigByChain: map[uint64]cldfproposalutils.MCMSWithTimelockConfig{ + selector1: mcmsConfig(t, 0), + selector2: mcmsConfig(t, 1), + }, + })) + require.NoError(t, err) + + refs, err := rt.State().DataStore.Addresses().Fetch() + require.NoError(t, err) + require.Len(t, refs, 10, "expected 5 contracts per chain") + + state, err := evmstate.MaybeLoadMCMSWithTimelockStateDataStore(rt.Environment(), selectors) + require.NoError(t, err) + require.NoError(t, state[selector1].Validate()) + require.NoError(t, state[selector2].Validate()) +} diff --git a/mcms/changesets/deploy/doc.go b/mcms/changesets/deploy/doc.go new file mode 100644 index 0000000..04e6536 --- /dev/null +++ b/mcms/changesets/deploy/doc.go @@ -0,0 +1,89 @@ +// Package deploy provides the DeployMCMSWithTimelock changeset and a registry +// for per-chain-family deploy implementations. +// +// # Usage +// +// Import the changeset and at least one registered chain family, then execute +// with a per-chain MCMS+timelock config: +// +// import ( +// "github.com/smartcontractkit/cld-changesets/mcms/changesets/deploy" +// _ "github.com/smartcontractkit/cld-changesets/mcms/evm/deploy" // registers EVM +// ) +// +// Testing: +// rt.Exec(runtime.ChangesetTask(deploy.Changeset{}, deploy.Input{ +// ConfigByChain: map[uint64]cldfproposalutils.MCMSWithTimelockConfig{ +// selector: cfg, +// }, +// })) +// +// CLD: +// registry.Add("deploy_mcms_with_timelock", Configure(deploy.Changeset{}).WithEnvInput()) +// +// [Changeset] groups input chains by chain-selectors family, runs each chain's +// deploy sequence in parallel, merges newly deployed addresses into the +// datastore, and returns operation reports. +// +// # Built-in EVM support +// +// EVM registers itself via init when its package is imported (blank import is +// enough). No call to [RegisterFamilies] is required for EVM-only deployments. +// +// # Adding a new chain family +// +// Implement a family under mcms//deploy (for example mcms/solana/deploy) +// and register it at process startup. Follow this layout: +// +// - register.go — exports Registration() and calls deploy.Register in init() +// - sequence.go — operations.Sequence that deploys contracts for one chain +// - addresses.go — helpers to load existing addresses from the datastore +// +// Step 1: implement Registration +// +// Return a [Registration] with the chain-selectors family string, a deploy +// sequence, and an optional Verify function: +// +// func Registration() deploy.Registration { +// return deploy.Registration{ +// Family: chainselectors.FamilySolana, // or FamilyAptos, etc. +// Sequence: seqDeployMCMSWithTimelock, +// Verify: verifySolanaChains, +// } +// } +// +// Step 2: implement the sequence +// +// The sequence must match [Sequence]: it receives [ChainInput] and [Deps], and +// returns [sequenceutils.OnChainOutput]. Write newly deployed contract addresses +// to output.Metadata.Addresses (see mcms/evm/deploy for AddressRef conventions). +// Skip contracts already present in deps.DataStore for the chain and qualifier. +// +// Step 3: register at startup +// +// Either auto-register from init (recommended, mirrors EVM): +// +// func init() { deploy.Register(Registration()) } +// +// Or register explicitly from the caller's main/setup code: +// +// deploy.RegisterFamilies(solanadeploy.Registration()) +// +// Each family may only be registered once; duplicate registration panics. +// +// # Registration contract +// +// - Family — must match chainselectors.GetSelectorFamily for chains you support +// - Sequence — required; one invocation per chain in ConfigByChain +// - Verify — optional; called during VerifyPreconditions with all chains of +// that family in the input (use to assert chains exist in the environment) +// +// If a chain's family has no registered sequence, Apply returns an error +// listing families that are registered. Use [RegisteredFamilies] to inspect the +// registry in tests. +// +// # Reference implementation +// +// See mcms/evm/deploy for a complete EVM implementation: idempotent deploy from +// the datastore, timelock role grants, and address metadata output. +package deploy diff --git a/mcms/changesets/deploy/families.go b/mcms/changesets/deploy/families.go new file mode 100644 index 0000000..27f882f --- /dev/null +++ b/mcms/changesets/deploy/families.go @@ -0,0 +1,16 @@ +package deploy + +// RegisterFamilies registers one or more chain-family deploy implementations. +// Each family may only be registered once; duplicate registration panics. +// +// External teams call this once at startup from their own module: +// +// deploy.RegisterFamilies(aptosimpl.Registration()) +// +// Built-in families (EVM) register themselves automatically via their package +// init function when imported: +// +// import _ "github.com/smartcontractkit/cld-changesets/mcms/evm/deploy" +func RegisterFamilies(regs ...Registration) { + registerAll(regs...) +} diff --git a/mcms/changesets/deploy/registry.go b/mcms/changesets/deploy/registry.go new file mode 100644 index 0000000..5f1640f --- /dev/null +++ b/mcms/changesets/deploy/registry.go @@ -0,0 +1,103 @@ +package deploy + +import ( + "fmt" + "slices" + "strings" + "sync" + + chainselectors "github.com/smartcontractkit/chain-selectors" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" +) + +var ( + registryMu sync.RWMutex + registry = make(map[string]Registration) +) + +// Register adds a family deploy implementation to the registry. +// It panics if the family string is empty, Sequence is nil, or the family is +// already registered — all of which indicate a programming error at startup. +func Register(reg Registration) { + registryMu.Lock() + defer registryMu.Unlock() + + if reg.Family == "" { + panic("mcms deploy: family is required") + } + if reg.Sequence == nil { + panic(fmt.Sprintf("mcms deploy: sequence is required for family %q", reg.Family)) + } + if _, exists := registry[reg.Family]; exists { + panic(fmt.Sprintf("mcms deploy: family %q already registered", reg.Family)) + } + + registry[reg.Family] = reg +} + +func registerAll(regs ...Registration) { + for _, reg := range regs { + Register(reg) + } +} + +// get returns the Registration for a chain family. +// Returns an error listing registered families when the family is not found. +func get(family string) (Registration, error) { + registryMu.RLock() + defer registryMu.RUnlock() + + reg, ok := registry[family] + if !ok { + registered := registeredFamiliesLocked() + if len(registered) == 0 { + return Registration{}, fmt.Errorf( + "mcms deploy: no sequence registered for family %q (none registered — import a family package or call RegisterFamilies)", + family, + ) + } + + return Registration{}, fmt.Errorf( + "mcms deploy: no sequence registered for family %q (registered: %s)", + family, + strings.Join(registered, ", "), + ) + } + + return reg, nil +} + +// RegisteredFamilies returns the sorted list of registered chain families. +func RegisteredFamilies() []string { + registryMu.RLock() + defer registryMu.RUnlock() + + return registeredFamiliesLocked() +} + +// groupByFamily groups deployment inputs by their chain-selectors family string. +func groupByFamily(cfgByChain map[uint64]cldfproposalutils.MCMSWithTimelockConfig) (map[string][]ChainInput, error) { + byFamily := make(map[string][]ChainInput, len(cfgByChain)) + for selector, cfg := range cfgByChain { + family, err := chainselectors.GetSelectorFamily(selector) + if err != nil { + return nil, fmt.Errorf("chain selector %d: %w", selector, err) + } + byFamily[family] = append(byFamily[family], ChainInput{ + ChainSelector: selector, + Config: cfg, + }) + } + + return byFamily, nil +} + +func registeredFamiliesLocked() []string { + families := make([]string, 0, len(registry)) + for family := range registry { + families = append(families, family) + } + slices.Sort(families) + + return families +} diff --git a/mcms/changesets/deploy/types.go b/mcms/changesets/deploy/types.go new file mode 100644 index 0000000..54d3de4 --- /dev/null +++ b/mcms/changesets/deploy/types.go @@ -0,0 +1,38 @@ +package deploy + +import ( + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/changeset/sequenceutils" + cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" +) + +// ChainInput is the per-chain input passed to every family sequence. +type ChainInput struct { + ChainSelector uint64 + Config cldfproposalutils.MCMSWithTimelockConfig +} + +// Deps is the read-only dependency bundle available to every family sequence. +// Logger and context come from the operations.Bundle passed at execution time. +type Deps struct { + BlockChains chain.BlockChains + DataStore cldfdatastore.DataStore +} + +// Sequence is the required operations sequence type for all family implementations. +type Sequence = operations.Sequence[ChainInput, sequenceutils.OnChainOutput, Deps] + +// Registration describes one chain family's MCMS deploy implementation. +type Registration struct { + // Family is the chain-selectors family string (e.g. chainselectors.FamilyEVM). + Family string + // Sequence executes the per-chain deploy and returns newly deployed addresses + // via OnChainOutput.Metadata.Addresses. + Sequence *Sequence + // Verify performs family-specific validation across all chains in the input. + // It is called during VerifyPreconditions. Optional — nil means no extra checks. + Verify func(env cldf.Environment, chains []ChainInput) error +} diff --git a/mcms/evm/deploy/addresses.go b/mcms/evm/deploy/addresses.go new file mode 100644 index 0000000..4f7b240 --- /dev/null +++ b/mcms/evm/deploy/addresses.go @@ -0,0 +1,74 @@ +// Package evmdeploy provides the EVM chain-family implementation for the +// MCMS deploy changeset (mcms/changesets/deploy). +package evmdeploy + +import ( + "github.com/ethereum/go-ethereum/common" + cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms" + + "github.com/smartcontractkit/cld-changesets/internal/semvers" + opsevm "github.com/smartcontractkit/cld-changesets/pkg/family/evm/operations" +) + +// deployedAddresses holds the on-chain addresses of an MCMS+timelock set on one +// EVM chain. A zero address means the contract has not yet been deployed. +type deployedAddresses struct { + Bypasser common.Address + Canceller common.Address + Proposer common.Address + Timelock common.Address + CallProxy common.Address +} + +func loadDeployedAddresses(ds cldfdatastore.DataStore, chainSelector uint64, qualifier string) deployedAddresses { + if ds == nil { + return deployedAddresses{} + } + + type lookup struct { + contractType cldf.ContractType + dest *common.Address + } + + var addrs deployedAddresses + lookups := []lookup{ + {mcmscontracts.BypasserManyChainMultisig, &addrs.Bypasser}, + {mcmscontracts.CancellerManyChainMultisig, &addrs.Canceller}, + {mcmscontracts.ProposerManyChainMultisig, &addrs.Proposer}, + {mcmscontracts.RBACTimelock, &addrs.Timelock}, + {mcmscontracts.CallProxy, &addrs.CallProxy}, + } + + for _, l := range lookups { + refs := ds.Addresses().Filter( + cldfdatastore.AddressRefByChainSelector(chainSelector), + cldfdatastore.AddressRefByType(cldfdatastore.ContractType(l.contractType)), + cldfdatastore.AddressRefByQualifier(qualifier), + ) + if len(refs) > 0 { + *l.dest = common.HexToAddress(refs[0].Address) + } + } + + return addrs +} + +// newAddressRef constructs a datastore AddressRef for a newly deployed contract. +// label is optional; pass an empty string to omit it. +func newAddressRef(chainSelector uint64, deploy opsevm.EVMDeployOutput, contractType cldf.ContractType, qualifier, label string) cldfdatastore.AddressRef { + v := semvers.V1_0_0 + ref := cldfdatastore.AddressRef{ + ChainSelector: chainSelector, + Address: deploy.Address.Hex(), + Type: cldfdatastore.ContractType(contractType), + Version: &v, + Qualifier: qualifier, + } + if label != "" { + ref.Labels = cldfdatastore.NewLabelSet(label) + } + + return ref +} diff --git a/mcms/evm/deploy/register.go b/mcms/evm/deploy/register.go new file mode 100644 index 0000000..a4d52a1 --- /dev/null +++ b/mcms/evm/deploy/register.go @@ -0,0 +1,44 @@ +package evmdeploy + +import ( + "fmt" + + chainselectors "github.com/smartcontractkit/chain-selectors" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + "github.com/smartcontractkit/cld-changesets/mcms/changesets/deploy" +) + +// init auto-registers the EVM family when this package is imported, unless it +// is already registered. Importing this package (or a blank import) is +// sufficient to enable EVM chain support in [deploy.Changeset]. +func init() { + registered := deploy.RegisteredFamilies() + for _, f := range registered { + if f == chainselectors.FamilyEVM { + return + } + } + deploy.Register(Registration()) +} + +// Registration returns the EVM chain-family deploy registration for MCMS with +// timelock. It is registered automatically via init but can also be passed +// explicitly to [deploy.RegisterFamilies] for test isolation. +func Registration() deploy.Registration { + return deploy.Registration{ + Family: chainselectors.FamilyEVM, + Sequence: seqDeployMCMSWithTimelock, + Verify: verifyEVMChains, + } +} + +func verifyEVMChains(env cldf.Environment, chains []deploy.ChainInput) error { + for _, c := range chains { + if _, ok := env.BlockChains.EVMChains()[c.ChainSelector]; !ok { + return fmt.Errorf("EVM chain %d not found in environment", c.ChainSelector) + } + } + + return nil +} diff --git a/mcms/evm/deploy/sequence.go b/mcms/evm/deploy/sequence.go new file mode 100644 index 0000000..69b9383 --- /dev/null +++ b/mcms/evm/deploy/sequence.go @@ -0,0 +1,300 @@ +package evmdeploy + +import ( + "fmt" + "math/big" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + bindings "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + cldfevm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/changeset/sequenceutils" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + mcmstypes "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/cld-changesets/internal/mcmsrole" + "github.com/smartcontractkit/cld-changesets/mcms/changesets/deploy" + opsevm "github.com/smartcontractkit/cld-changesets/pkg/family/evm/operations" + seqs "github.com/smartcontractkit/cld-changesets/pkg/family/evm/sequences" +) + +var seqDeployMCMSWithTimelock = operations.NewSequence( + "seq-mcms-deploy-with-timelock", + semver.MustParse("1.0.0"), + "Deploy MCMS and timelock contracts on an EVM chain", + deployMCMSWithTimelock, +) + +// deployer accumulates per-chain deployment state within a single sequence run. +type deployer struct { + b operations.Bundle + chain cldfevm.Chain + config cldfproposalutils.MCMSWithTimelockConfig + qualifier string + out sequenceutils.OnChainOutput +} + +// deployMCMSWithTimelock deploys the MCMS-with-timelock stack on an EVM chain, +// skipping contracts already in the datastore: one bypasser MCM, one canceller +// MCM, one proposer MCM (each with signer config), one RBAC timelock, and one +// call proxy. It then grants timelock roles when the deployer key is admin: +// proposer to the proposer MCM, canceller to proposer/canceller/bypasser MCMs, +// bypasser to the bypasser MCM, executor to the call proxy, and admin to the +// timelock itself when it is not yet self-admin. Role grants are skipped if the +// deployer is not admin (e.g. after ownership transfer). +func deployMCMSWithTimelock( + b operations.Bundle, + deps deploy.Deps, + in deploy.ChainInput, +) (sequenceutils.OnChainOutput, error) { + chain, ok := deps.BlockChains.EVMChains()[in.ChainSelector] + if !ok { + return sequenceutils.OnChainOutput{}, fmt.Errorf("EVM chain %d not found in environment", in.ChainSelector) + } + + qualifier := qualifierFromConfig(in.Config.Qualifier) + + existing := loadDeployedAddresses(deps.DataStore, in.ChainSelector, qualifier) + + d := &deployer{b: b, chain: chain, config: in.Config, qualifier: qualifier} + + var err error + if existing.Bypasser, err = d.deployMCMIfNeeded(mcmscontracts.BypasserManyChainMultisig, in.Config.Bypasser, existing.Bypasser); err != nil { + return d.out, err + } + if existing.Canceller, err = d.deployMCMIfNeeded(mcmscontracts.CancellerManyChainMultisig, in.Config.Canceller, existing.Canceller); err != nil { + return d.out, err + } + if existing.Proposer, err = d.deployMCMIfNeeded(mcmscontracts.ProposerManyChainMultisig, in.Config.Proposer, existing.Proposer); err != nil { + return d.out, err + } + if existing.Timelock == (common.Address{}) { + if existing.Timelock, err = d.deployTimelock(existing); err != nil { + return d.out, err + } + } + if existing.CallProxy == (common.Address{}) { + if existing.CallProxy, err = d.deployCallProxy(existing.Timelock); err != nil { + return d.out, err + } + } + + if err = d.grantTimelockRoles(existing); err != nil { + return d.out, err + } + + return d.out, nil +} + +// deployMCMIfNeeded deploys a ManyChainMultiSig contract if addr is zero, +// sets its signers config, and records the new address ref. +func (d *deployer) deployMCMIfNeeded( + contractType cldf.ContractType, + mcmConfig mcmstypes.Config, + existing common.Address, +) (common.Address, error) { + if existing != (common.Address{}) { + return existing, nil + } + + report, err := operations.ExecuteSequence( + d.b, + seqs.SeqEVMDeployMCMWithConfig, + d.chain, + seqs.SeqDeployMCMWithConfigInput{ + ContractType: contractType, + MCMConfig: mcmConfig, + ChainSelector: d.chain.Selector, + GasBoostConfig: d.config.GasBoostConfig, + Qualifier: d.config.Qualifier, + }, + ) + if err != nil { + return common.Address{}, fmt.Errorf("deploy %s: %w", contractType, err) + } + + d.out.Metadata.Addresses = append(d.out.Metadata.Addresses, + newAddressRef(d.chain.Selector, report.Output, contractType, d.qualifier, labelFromConfig(d.config.Label))) + + return report.Output.Address, nil +} + +func (d *deployer) deployTimelock(addrs deployedAddresses) (common.Address, error) { + report, err := operations.ExecuteOperation( + d.b, + opsevm.OpEVMDeployTimelock, + d.chain, + opsevm.EVMDeployInput[opsevm.OpEVMDeployTimelockInput]{ + ChainSelector: d.chain.Selector, + DeployInput: opsevm.OpEVMDeployTimelockInput{ + Admin: d.chain.DeployerKey.From, + Proposers: []common.Address{addrs.Proposer}, + Executors: []common.Address{}, + Cancellers: []common.Address{addrs.Canceller, addrs.Proposer, addrs.Bypasser}, + Bypassers: []common.Address{addrs.Bypasser}, + TimelockMinDelay: d.config.TimelockMinDelay, + }, + }, + opsevm.RetryDeploymentWithGasBoost[opsevm.OpEVMDeployTimelockInput](d.config.GasBoostConfig), + ) + if err != nil { + return common.Address{}, fmt.Errorf("deploy timelock: %w", err) + } + + d.out.Metadata.Addresses = append(d.out.Metadata.Addresses, + newAddressRef(d.chain.Selector, report.Output, mcmscontracts.RBACTimelock, d.qualifier, labelFromConfig(d.config.Label))) + + return report.Output.Address, nil +} + +func (d *deployer) deployCallProxy(timelockAddr common.Address) (common.Address, error) { + report, err := operations.ExecuteOperation( + d.b, + opsevm.OpEVMDeployCallProxy, + d.chain, + opsevm.EVMDeployInput[opsevm.OpEVMDeployCallProxyInput]{ + ChainSelector: d.chain.Selector, + DeployInput: opsevm.OpEVMDeployCallProxyInput{Timelock: timelockAddr}, + }, + opsevm.RetryDeploymentWithGasBoost[opsevm.OpEVMDeployCallProxyInput](d.config.GasBoostConfig), + ) + if err != nil { + return common.Address{}, fmt.Errorf("deploy call proxy: %w", err) + } + + d.out.Metadata.Addresses = append(d.out.Metadata.Addresses, + newAddressRef(d.chain.Selector, report.Output, mcmscontracts.CallProxy, d.qualifier, labelFromConfig(d.config.Label))) + + return report.Output.Address, nil +} + +// grantTimelockRoles grants proposer/canceller/bypasser/executor roles on the +// timelock to the respective MCMS contracts. When the timelock is not yet an +// admin of itself, it also grants the admin role to the timelock. Skips +// gracefully if the deployer key is not a timelock admin (e.g. when re-running +// after admin transfer). +func (d *deployer) grantTimelockRoles(addrs deployedAddresses) error { + isDeployerAdmin, isTimelockAdmin, err := d.timelockAdminStatus(addrs.Timelock) + if err != nil { + return fmt.Errorf("check timelock admin: %w", err) + } + if !isDeployerAdmin { + d.b.Logger.Infow("Deployer key is not timelock admin, skipping role grants", + "chain", d.chain.String(), + "timelock", addrs.Timelock.Hex(), + ) + + return nil + } + + rolesAndAddresses := []seqs.RolesAndAddresses{ + { + Role: mcmsrole.ProposerRole.ID, + Name: mcmsrole.ProposerRole.Name, + Addresses: []common.Address{addrs.Proposer}, + }, + { + Role: mcmsrole.CancellerRole.ID, + Name: mcmsrole.CancellerRole.Name, + Addresses: []common.Address{addrs.Proposer, addrs.Canceller, addrs.Bypasser}, + }, + { + Role: mcmsrole.BypasserRole.ID, + Name: mcmsrole.BypasserRole.Name, + Addresses: []common.Address{addrs.Bypasser}, + }, + { + Role: mcmsrole.ExecutorRole.ID, + Name: mcmsrole.ExecutorRole.Name, + Addresses: []common.Address{addrs.CallProxy}, + }, + } + if !isTimelockAdmin { + rolesAndAddresses = append(rolesAndAddresses, seqs.RolesAndAddresses{ + Role: mcmsrole.AdminRole.ID, + Name: mcmsrole.AdminRole.Name, + Addresses: []common.Address{addrs.Timelock}, + }) + } + + _, err = operations.ExecuteSequence( + d.b, + seqs.SeqGrantRolesTimelock, + seqs.SeqGrantRolesTimelockDeps{Chain: d.chain}, + seqs.SeqGrantRolesTimelockInput{ + ContractType: mcmscontracts.RBACTimelock, + ChainSelector: d.chain.Selector, + Timelock: addrs.Timelock, + RolesAndAddresses: rolesAndAddresses, + IsDeployerKeyAdmin: isDeployerAdmin, + GasBoostConfig: d.config.GasBoostConfig, + }, + ) + + return err +} + +// timelockAdminStatus returns whether the deployer key and the timelock itself +// hold the admin role on the given timelock contract. +func (d *deployer) timelockAdminStatus(timelockAddr common.Address) (isDeployerAdmin, isTimelockAdmin bool, err error) { + admins, err := d.getTimelockAdminAddresses(timelockAddr) + if err != nil { + return false, false, err + } + + for _, admin := range admins { + if admin == d.chain.DeployerKey.From { + isDeployerAdmin = true + } + if admin == timelockAddr { + isTimelockAdmin = true + } + } + + return isDeployerAdmin, isTimelockAdmin, nil +} + +func (d *deployer) getTimelockAdminAddresses(timelockAddr common.Address) ([]common.Address, error) { + timelock, err := bindings.NewRBACTimelock(timelockAddr, d.chain.Client) + if err != nil { + return nil, fmt.Errorf("bind timelock: %w", err) + } + + callOpts := &bind.CallOpts{Context: d.b.GetContext()} + count, err := timelock.GetRoleMemberCount(callOpts, mcmsrole.AdminRole.ID) + if err != nil { + return nil, fmt.Errorf("get admin count: %w", err) + } + + admins := make([]common.Address, 0, count.Uint64()) + for i := range count.Uint64() { + member, err := timelock.GetRoleMember(callOpts, mcmsrole.AdminRole.ID, new(big.Int).SetUint64(i)) + if err != nil { + return nil, fmt.Errorf("get admin member %d: %w", i, err) + } + admins = append(admins, member) + } + + return admins, nil +} + +func qualifierFromConfig(q *string) string { + if q == nil { + return "" + } + + return *q +} + +func labelFromConfig(l *string) string { + if l == nil { + return "" + } + + return *l +}