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..fe1571c --- /dev/null +++ b/mcms/evm/deploy/register.go @@ -0,0 +1,47 @@ +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 when this package is +// imported; use a blank import in application code. Do not also call +// [deploy.RegisterFamilies](Registration()) after importing this package — +// duplicate registration panics. For registry isolation in tests, register +// explicitly from a test package that does not import this package. +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..b22380e --- /dev/null +++ b/mcms/evm/deploy/sequence.go @@ -0,0 +1,306 @@ +package evmdeploy + +import ( + "fmt" + "math" + "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) + } + + n := count.Uint64() + if n > uint64(math.MaxInt) { + return nil, fmt.Errorf("admin count %d exceeds int range", n) + } + adminCount := int(n) + admins := make([]common.Address, 0, adminCount) + for i := range adminCount { + member, err := timelock.GetRoleMember(callOpts, mcmsrole.AdminRole.ID, big.NewInt(int64(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 +}