From 96fcdacdccb0c287f81f42b5ffd14d45a2f307e8 Mon Sep 17 00:00:00 2001 From: serichoi65 Date: Mon, 24 Feb 2025 15:25:59 -0600 Subject: [PATCH] feat(sidecar-extensibility): detect upgradeable contracts and pull ABIs from IPFS (#237) * first draft * clean up * clean up * fix * test working * add test * successful test * check for error * add GetProxyContractForAddress * connect to ipfs * update * move around code * fix postgres contract store * update quiknode url for tests * update abi fetcher * using valid address for test * add using storage slot test * fixing baseUrl for restaked strategies test * add abiFetcher to all NewContractManager * create contract using all info * add abiFetcher test * patch abiFetcher in ContractManager * gofmt * add http client as an argument to abifetcher * SetHttpClient * renaming FetchMetadataFromAddress to FetchContractDetails --- cmd/database.go | 7 +- cmd/debugger/main.go | 8 +- cmd/operatorRestakedStrategies.go | 7 +- cmd/run.go | 6 +- go.mod | 3 + go.sum | 29 +++ internal/config/config.go | 11 + pkg/abiFetcher/abiFetcher.go | 158 +++++++++++++ pkg/abiFetcher/abiFetcher_test.go | 109 +++++++++ pkg/clients/ethereum/client.go | 4 + .../contractCaller_test.go | 2 +- pkg/contractManager/contractManager.go | 117 ++++++++++ pkg/contractManager/contractManager_test.go | 208 ++++++++++++++++++ pkg/contractStore/contractStore.go | 3 + .../postgresContractStore.go | 86 ++++++-- .../postgresContractStore_test.go | 13 ++ pkg/indexer/restakedStrategies_test.go | 6 +- pkg/pipeline/pipelineIntegration_test.go | 7 +- 18 files changed, 758 insertions(+), 26 deletions(-) create mode 100644 pkg/abiFetcher/abiFetcher.go create mode 100644 pkg/abiFetcher/abiFetcher_test.go create mode 100644 pkg/contractManager/contractManager_test.go diff --git a/cmd/database.go b/cmd/database.go index e12873c5..d7ff4976 100644 --- a/cmd/database.go +++ b/cmd/database.go @@ -6,6 +6,7 @@ import ( "github.com/Layr-Labs/sidecar/internal/logger" "github.com/Layr-Labs/sidecar/internal/metrics" "github.com/Layr-Labs/sidecar/internal/version" + "github.com/Layr-Labs/sidecar/pkg/abiFetcher" "github.com/Layr-Labs/sidecar/pkg/clients/ethereum" "github.com/Layr-Labs/sidecar/pkg/contractCaller/sequentialContractCaller" "github.com/Layr-Labs/sidecar/pkg/contractManager" @@ -29,6 +30,8 @@ import ( "github.com/spf13/viper" "go.uber.org/zap" "log" + "net/http" + "time" ) var runDatabaseCmd = &cobra.Command{ @@ -59,6 +62,8 @@ var runDatabaseCmd = &cobra.Command{ client := ethereum.NewClient(ethereum.ConvertGlobalConfigToEthereumConfig(&cfg.EthereumRpcConfig), l) + af := abiFetcher.NewAbiFetcher(client, &http.Client{Timeout: 5 * time.Second}, l, cfg) + pgConfig := postgres.PostgresConfigFromDbConfig(&cfg.DatabaseConfig) pg, err := postgres.NewPostgres(pgConfig) @@ -81,7 +86,7 @@ var runDatabaseCmd = &cobra.Command{ log.Fatalf("Failed to initialize core contracts: %v", err) } - cm := contractManager.NewContractManager(contractStore, client, sdc, l) + cm := contractManager.NewContractManager(contractStore, client, af, sdc, l) mds := pgStorage.NewPostgresBlockStore(grm, l, cfg) if err != nil { diff --git a/cmd/debugger/main.go b/cmd/debugger/main.go index 5a7fe0ff..7f87fa12 100644 --- a/cmd/debugger/main.go +++ b/cmd/debugger/main.go @@ -3,6 +3,10 @@ package main import ( "context" "fmt" + "net/http" + "time" + + "github.com/Layr-Labs/sidecar/pkg/abiFetcher" "github.com/Layr-Labs/sidecar/pkg/clients/ethereum" sidecarClient "github.com/Layr-Labs/sidecar/pkg/clients/sidecar" "github.com/Layr-Labs/sidecar/pkg/contractCaller/sequentialContractCaller" @@ -55,6 +59,8 @@ func main() { client := ethereum.NewClient(ethereum.ConvertGlobalConfigToEthereumConfig(&cfg.EthereumRpcConfig), l) + af := abiFetcher.NewAbiFetcher(client, &http.Client{Timeout: 5 * time.Second}, l, cfg) + pgConfig := postgres.PostgresConfigFromDbConfig(&cfg.DatabaseConfig) pg, err := postgres.NewPostgres(pgConfig) @@ -77,7 +83,7 @@ func main() { log.Fatalf("Failed to initialize core contracts: %v", err) } - cm := contractManager.NewContractManager(contractStore, client, sdc, l) + cm := contractManager.NewContractManager(contractStore, client, af, sdc, l) mds := pgStorage.NewPostgresBlockStore(grm, l, cfg) if err != nil { diff --git a/cmd/operatorRestakedStrategies.go b/cmd/operatorRestakedStrategies.go index 1ec32227..179e0160 100644 --- a/cmd/operatorRestakedStrategies.go +++ b/cmd/operatorRestakedStrategies.go @@ -6,6 +6,7 @@ import ( "github.com/Layr-Labs/sidecar/internal/config" "github.com/Layr-Labs/sidecar/internal/logger" "github.com/Layr-Labs/sidecar/internal/metrics" + "github.com/Layr-Labs/sidecar/pkg/abiFetcher" "github.com/Layr-Labs/sidecar/pkg/clients/ethereum" "github.com/Layr-Labs/sidecar/pkg/contractCaller/sequentialContractCaller" "github.com/Layr-Labs/sidecar/pkg/contractManager" @@ -22,6 +23,8 @@ import ( "github.com/spf13/viper" "go.uber.org/zap" "log" + "net/http" + "time" ) var runOperatorRestakedStrategiesCmd = &cobra.Command{ @@ -46,6 +49,8 @@ var runOperatorRestakedStrategiesCmd = &cobra.Command{ client := ethereum.NewClient(ethereum.ConvertGlobalConfigToEthereumConfig(&cfg.EthereumRpcConfig), l) + af := abiFetcher.NewAbiFetcher(client, &http.Client{Timeout: 5 * time.Second}, l, cfg) + pgConfig := postgres.PostgresConfigFromDbConfig(&cfg.DatabaseConfig) pg, err := postgres.NewPostgres(pgConfig) @@ -68,7 +73,7 @@ var runOperatorRestakedStrategiesCmd = &cobra.Command{ log.Fatalf("Failed to initialize core contracts: %v", err) } - cm := contractManager.NewContractManager(contractStore, client, sdc, l) + cm := contractManager.NewContractManager(contractStore, client, af, sdc, l) mds := pgStorage.NewPostgresBlockStore(grm, l, cfg) if err != nil { diff --git a/cmd/run.go b/cmd/run.go index 9b4e1f8c..028e91c7 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/Layr-Labs/sidecar/internal/metrics/prometheus" "github.com/Layr-Labs/sidecar/internal/version" + "github.com/Layr-Labs/sidecar/pkg/abiFetcher" "github.com/Layr-Labs/sidecar/pkg/clients/ethereum" sidecarClient "github.com/Layr-Labs/sidecar/pkg/clients/sidecar" "github.com/Layr-Labs/sidecar/pkg/contractCaller/sequentialContractCaller" @@ -29,6 +30,7 @@ import ( "github.com/Layr-Labs/sidecar/pkg/sidecar" pgStorage "github.com/Layr-Labs/sidecar/pkg/storage/postgres" "log" + "net/http" "time" "github.com/Layr-Labs/sidecar/internal/config" @@ -74,6 +76,8 @@ var runCmd = &cobra.Command{ client := ethereum.NewClient(ethereum.ConvertGlobalConfigToEthereumConfig(&cfg.EthereumRpcConfig), l) + af := abiFetcher.NewAbiFetcher(client, &http.Client{Timeout: 5 * time.Second}, l, cfg) + pgConfig := postgres.PostgresConfigFromDbConfig(&cfg.DatabaseConfig) pg, err := postgres.NewPostgres(pgConfig) @@ -96,7 +100,7 @@ var runCmd = &cobra.Command{ log.Fatalf("Failed to initialize core contracts: %v", err) } - cm := contractManager.NewContractManager(contractStore, client, sink, l) + cm := contractManager.NewContractManager(contractStore, client, af, sink, l) mds := pgStorage.NewPostgresBlockStore(grm, l, cfg) if err != nil { diff --git a/go.mod b/go.mod index 27374f9f..7f07a692 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,15 @@ require ( github.com/Layr-Labs/eigenlayer-contracts v0.4.1-holesky-pepe.0.20240813143901-00fc4b95e9c1 github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.13 github.com/Layr-Labs/protocol-apis v1.7.0 + github.com/agiledragon/gomonkey/v2 v2.13.0 + github.com/btcsuite/btcutil v1.0.2 github.com/ethereum/go-ethereum v1.15.2 github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 github.com/habx/pg-commands v0.6.1 + github.com/jarcoal/httpmock v1.3.1 github.com/lib/pq v1.10.9 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.21.0 diff --git a/go.sum b/go.sum index 84c02927..50dafdb6 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,9 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/agiledragon/gomonkey/v2 v2.13.0 h1:B24Jg6wBI1iB8EFR1c+/aoTg7QN/Cum7YffG8KMIyYo= +github.com/agiledragon/gomonkey/v2 v2.13.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/akuity/grpc-gateway-client v0.0.0-20240912082144-55a48e8b4b89 h1:TNZN2oYHs6ymIMyDeVu6RnwNowChc/q4U/p7ruHA/GM= github.com/akuity/grpc-gateway-client v0.0.0-20240912082144-55a48e8b4b89/go.mod h1:0MZqOxL+zq+hGedAjYhkm1tOKuZyjUmE/xA8nqXa9q0= github.com/alevinval/sse v1.0.1 h1:cFubh2lMNdHT6niFLCsyTuhAgljaAWbdmceAe6qPIfo= @@ -32,6 +35,16 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI= github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/bufbuild/protovalidate-go v0.4.0 h1:ModSkCLEW07fiyGtdtMXKY+Gz3oPFKSfiaSCgL+FtpU= github.com/bufbuild/protovalidate-go v0.4.0/go.mod h1:QqeUPLVYEKQc+/rkoUXFqXW03zPBfrEfIbX+zmA0VxA= github.com/bufbuild/protoyaml-go v0.1.5 h1:Vc3KTOPRoDbTT/FqqUSJl+jGaVesX9/M3tFCfbgBIHc= @@ -68,6 +81,7 @@ github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOV github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -188,15 +202,20 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -225,6 +244,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= @@ -238,8 +259,10 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -291,8 +314,10 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -377,8 +402,10 @@ go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= @@ -457,6 +484,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -501,6 +529,7 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/config/config.go b/internal/config/config.go index 609e0081..6c3b5274 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -98,6 +98,10 @@ type SidecarPrimaryConfig struct { IsPrimary bool } +type IpfsConfig struct { + Url string +} + type Config struct { Debug bool EthereumRpcConfig EthereumRpcConfig @@ -109,6 +113,7 @@ type Config struct { DataDogConfig DataDogConfig PrometheusConfig PrometheusConfig SidecarPrimaryConfig SidecarPrimaryConfig + IpfsConfig IpfsConfig } func StringWithDefault(value, defaultValue string) string { @@ -147,6 +152,8 @@ var ( PrometheusPort = "prometheus.port" SidecarPrimaryUrl = "sidecar-primary.url" + + IpfsUrl = "ipfs.url" ) func NewConfig() *Config { @@ -202,6 +209,10 @@ func NewConfig() *Config { SidecarPrimaryConfig: SidecarPrimaryConfig{ Url: viper.GetString(normalizeFlagName(SidecarPrimaryUrl)), }, + + IpfsConfig: IpfsConfig{ + Url: StringWithDefault(viper.GetString(normalizeFlagName(IpfsUrl)), "https://ipfs.io/ipfs"), + }, } } diff --git a/pkg/abiFetcher/abiFetcher.go b/pkg/abiFetcher/abiFetcher.go new file mode 100644 index 00000000..ec1dfe5f --- /dev/null +++ b/pkg/abiFetcher/abiFetcher.go @@ -0,0 +1,158 @@ +package abiFetcher + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/pkg/clients/ethereum" + "github.com/btcsuite/btcutil/base58" + "go.uber.org/zap" +) + +type AbiFetcher struct { + EthereumClient *ethereum.Client + httpClient *http.Client + Logger *zap.Logger + Config *config.Config +} + +type Response struct { + Output struct { + ABI json.RawMessage `json:"abi"` // Use json.RawMessage to capture the ABI JSON + } `json:"output"` +} + +func NewAbiFetcher( + e *ethereum.Client, + hc *http.Client, + l *zap.Logger, + cfg *config.Config, +) *AbiFetcher { + return &AbiFetcher{ + EthereumClient: e, + httpClient: hc, + Logger: l, + Config: cfg, + } +} + +func (af *AbiFetcher) FetchContractDetails(ctx context.Context, address string) (string, string, error) { + bytecode, err := af.EthereumClient.GetCode(ctx, address) + if err != nil { + af.Logger.Sugar().Errorw("Failed to get the contract bytecode", + zap.Error(err), + zap.String("address", address), + ) + return "", "", err + } + + bytecodeHash := ethereum.HashBytecode(bytecode) + af.Logger.Sugar().Debug("Fetched the contract bytecodeHash", + zap.String("address", address), + zap.String("bytecodeHash", bytecodeHash), + ) + + // fetch ABI using IPFS + // TODO: add a fallback method using Etherscan + abi, err := af.FetchAbiFromIPFS(address, bytecode) + if err != nil { + af.Logger.Sugar().Errorw("Failed to fetch ABI from IPFS", + zap.Error(err), + zap.String("address", address), + ) + return "", "", err + } + + return bytecodeHash, abi, nil +} + +func (af *AbiFetcher) GetIPFSUrlFromBytecode(bytecode string) (string, error) { + markerSequence := "a264697066735822" + index := strings.Index(strings.ToLower(bytecode), markerSequence) + + if index == -1 { + return "", fmt.Errorf("CBOR marker sequence not found") + } + + // Extract the IPFS hash (34 bytes = 68 hex characters) + startIndex := index + len(markerSequence) + if len(bytecode) < startIndex+68 { + return "", fmt.Errorf("bytecode too short to contain complete IPFS hash") + } + + ipfsHash := bytecode[startIndex : startIndex+68] + + // Decode the hex string to bytes + // Skip the 1220 prefix when decoding + bytes, err := hex.DecodeString(ipfsHash) + if err != nil { + return "", fmt.Errorf("failed to decode IPFS hash: %v", err) + } + + // Convert to base58 + base58Hash := base58.Encode(bytes) + + return fmt.Sprintf("%s/%s", af.Config.IpfsConfig.Url, base58Hash), nil +} + +func (af *AbiFetcher) FetchAbiFromIPFS(address string, bytecode string) (string, error) { + url, err := af.GetIPFSUrlFromBytecode(bytecode) + if err != nil { + af.Logger.Sugar().Errorw("Failed to get IPFS URL from bytecode", + zap.Error(err), + zap.String("address", address), + ) + return "", err + } + af.Logger.Sugar().Debug("Successfully retrieved IPFS URL", + zap.String("address", address), + zap.String("ipfsUrl", url), + ) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + af.Logger.Sugar().Errorw("Failed to create a new HTTP request with context", + zap.Error(err), + zap.String("address", address), + ) + return "", err + } + + resp, err := af.httpClient.Do(req) + if err != nil { + af.Logger.Sugar().Errorw("Failed to perform HTTP request", + zap.Error(err), + zap.String("address", address), + ) + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("gateway returned status: %d", resp.StatusCode) + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var result Response + if err := json.Unmarshal(content, &result); err != nil { + af.Logger.Sugar().Errorw("Failed to parse json from IPFS URL content", + zap.Error(err), + ) + return "", err + } + + af.Logger.Sugar().Debug("Successfully fetched ABI from IPFS", + zap.String("address", address), + ) + return string(result.Output.ABI), nil +} diff --git a/pkg/abiFetcher/abiFetcher_test.go b/pkg/abiFetcher/abiFetcher_test.go new file mode 100644 index 00000000..d516212e --- /dev/null +++ b/pkg/abiFetcher/abiFetcher_test.go @@ -0,0 +1,109 @@ +package abiFetcher + +import ( + "context" + "net/http" + "reflect" + "testing" + "time" + + "os" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/logger" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/clients/ethereum" + "github.com/Layr-Labs/sidecar/pkg/postgres" + "github.com/agiledragon/gomonkey/v2" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func setup() ( + string, + *gorm.DB, + *zap.Logger, + *config.Config, + error, +) { + cfg := config.NewConfig() + cfg.Chain = config.Chain_Mainnet + cfg.Debug = os.Getenv(config.Debug) == "true" + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return dbname, nil, nil, nil, err + } + + return dbname, grm, l, cfg, nil +} + +func Test_AbiFetcher(t *testing.T) { + _, _, l, cfg, err := setup() + + if err != nil { + t.Fatal(err) + } + + httpClient := &http.Client{ + Timeout: 5 * time.Second, + } + + baseUrl := "http://72.46.85.253:8545" + ethConfig := ethereum.DefaultNativeCallEthereumClientConfig() + ethConfig.BaseUrl = baseUrl + + client := ethereum.NewClient(ethConfig, l) + + t.Run("Test getting IPFS url from bytecode", func(t *testing.T) { + af := NewAbiFetcher(client, httpClient, l, cfg) + + address := "0x29a954e9e7f12936db89b183ecdf879fbbb99f14" + bytecode, err := af.EthereumClient.GetCode(context.Background(), address) + assert.Nil(t, err) + + url, err := af.GetIPFSUrlFromBytecode(bytecode) + assert.Nil(t, err) + + expectedUrl := "https://ipfs.io/ipfs/QmeuBk6fmBdgW3B3h11LRkFw8shYLbMb4w7ko82jCxg6jR" + assert.Equal(t, expectedUrl, url) + }) + t.Run("Test fetching ABI from IPFS", func(t *testing.T) { + mockUrl := "https://test" + patches := gomonkey.ApplyMethod(reflect.TypeOf(&AbiFetcher{}), "GetIPFSUrlFromBytecode", + func(_ *AbiFetcher, _ string) (string, error) { + return mockUrl, nil + }) + defer patches.Reset() + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + mockAbiResponse := `{ + "output": { + "abi": "[{\"type\":\"function\",\"name\":\"test\"}]" + } + }` + + httpmock.RegisterResponder("GET", mockUrl, + httpmock.NewStringResponder(200, mockAbiResponse)) + + mockHttpClient := &http.Client{ + Transport: httpmock.DefaultTransport, + } + + af := NewAbiFetcher(client, mockHttpClient, l, cfg) + + address := "0x29a954e9e7f12936db89b183ecdf879fbbb99f14" + abi, err := af.FetchAbiFromIPFS(address, "mocked") + assert.Nil(t, err) + + expectedAbi := `"[{\"type\":\"function\",\"name\":\"test\"}]"` + assert.Equal(t, abi, expectedAbi) + }) +} diff --git a/pkg/clients/ethereum/client.go b/pkg/clients/ethereum/client.go index 96a4b509..b09e0095 100644 --- a/pkg/clients/ethereum/client.go +++ b/pkg/clients/ethereum/client.go @@ -110,6 +110,10 @@ func NewClient(cfg *EthereumClientConfig, l *zap.Logger) *Client { } } +func (c *Client) SetHttpClient(client *http.Client) { + c.httpClient = client +} + func (c *Client) GetEthereumContractCaller() (*ethclient.Client, error) { d, err := ethclient.Dial(c.clientConfig.BaseUrl) if err != nil { diff --git a/pkg/contractCaller/sequentialContractCaller/contractCaller_test.go b/pkg/contractCaller/sequentialContractCaller/contractCaller_test.go index 7ab30b06..f461c3ab 100644 --- a/pkg/contractCaller/sequentialContractCaller/contractCaller_test.go +++ b/pkg/contractCaller/sequentialContractCaller/contractCaller_test.go @@ -21,7 +21,7 @@ func setup() ( ) { cfg := config.NewConfig() cfg.Chain = config.Chain_Mainnet - cfg.EthereumRpcConfig.BaseUrl = "https://tame-fabled-liquid.quiknode.pro/f27d4be93b4d7de3679f5c5ae881233f857407a0" + cfg.EthereumRpcConfig.BaseUrl = "http://72.46.85.253:8545" cfg.Debug = os.Getenv(config.Debug) == "true" cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() diff --git a/pkg/contractManager/contractManager.go b/pkg/contractManager/contractManager.go index 9c007f12..50b4a104 100644 --- a/pkg/contractManager/contractManager.go +++ b/pkg/contractManager/contractManager.go @@ -1,16 +1,23 @@ package contractManager import ( + "context" "fmt" + "github.com/Layr-Labs/sidecar/internal/metrics" + "github.com/Layr-Labs/sidecar/pkg/abiFetcher" "github.com/Layr-Labs/sidecar/pkg/clients/ethereum" "github.com/Layr-Labs/sidecar/pkg/contractStore" + "github.com/Layr-Labs/sidecar/pkg/parser" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "go.uber.org/zap" ) type ContractManager struct { ContractStore contractStore.ContractStore EthereumClient *ethereum.Client + AbiFetcher *abiFetcher.AbiFetcher metricsSink *metrics.MetricsSink Logger *zap.Logger } @@ -18,12 +25,14 @@ type ContractManager struct { func NewContractManager( cs contractStore.ContractStore, e *ethereum.Client, + af *abiFetcher.AbiFetcher, ms *metrics.MetricsSink, l *zap.Logger, ) *ContractManager { return &ContractManager{ ContractStore: cs, EthereumClient: e, + AbiFetcher: af, metricsSink: ms, Logger: l, } @@ -43,3 +52,111 @@ func (cm *ContractManager) GetContractWithProxy( return contract, nil } + +// HandleContractUpgrade parses an Upgraded contract log and inserts the new upgraded implementation into the database +func (cm *ContractManager) HandleContractUpgrade(ctx context.Context, blockNumber uint64, upgradedLog *parser.DecodedLog) error { + // the new address that the contract points to + newProxiedAddress := "" + + // Check the arguments for the new address. EIP-1967 contracts include this as an argument. + // Otherwise, we'll check the storage slot + for _, arg := range upgradedLog.Arguments { + if arg.Name == "implementation" && arg.Value != "" && arg.Value != nil { + newProxiedAddress = arg.Value.(common.Address).String() + break + } + } + + if newProxiedAddress == "" { + // check the storage slot at the provided block number of the transaction + storageValue, err := cm.EthereumClient.GetStorageAt(ctx, upgradedLog.Address, ethereum.EIP1967_STORAGE_SLOT, hexutil.EncodeUint64(blockNumber)) + if err != nil || storageValue == "" { + cm.Logger.Sugar().Errorw("Failed to get storage value", + zap.Error(err), + zap.Uint64("block", blockNumber), + zap.String("upgradedLogAddress", upgradedLog.Address), + ) + return err + } + if len(storageValue) != 66 { + cm.Logger.Sugar().Errorw("Invalid storage value", + zap.Uint64("block", blockNumber), + zap.String("storageValue", storageValue), + ) + return err + } + + newProxiedAddress = "0x" + storageValue[26:] + } + + if newProxiedAddress == "" { + cm.Logger.Sugar().Debugw("No new proxied address found", zap.String("address", upgradedLog.Address)) + return fmt.Errorf("no new proxied address found for %s during the 'Upgraded' event", upgradedLog.Address) + } + + err := cm.CreateUpgradedProxyContract(ctx, blockNumber, upgradedLog.Address, newProxiedAddress) + if err != nil { + cm.Logger.Sugar().Errorw("Failed to create proxy contract", zap.Error(err)) + return err + } + cm.Logger.Sugar().Infow("Upgraded proxy contract", zap.String("contractAddress", upgradedLog.Address), zap.String("proxyContractAddress", newProxiedAddress)) + return nil +} + +func (cm *ContractManager) CreateUpgradedProxyContract( + ctx context.Context, + blockNumber uint64, + contractAddress string, + proxyContractAddress string, +) error { + // Check if proxy contract already exists + proxyContract, _ := cm.ContractStore.GetProxyContractForAddress(blockNumber, contractAddress) + if proxyContract != nil { + cm.Logger.Sugar().Debugw("Found existing proxy contract when trying to create one", + zap.String("contractAddress", contractAddress), + zap.String("proxyContractAddress", proxyContractAddress), + ) + return nil + } + + // Create a proxy contract + _, err := cm.ContractStore.CreateProxyContract(blockNumber, contractAddress, proxyContractAddress) + if err != nil { + cm.Logger.Sugar().Errorw("Failed to create proxy contract", + zap.Error(err), + zap.String("contractAddress", contractAddress), + zap.String("proxyContractAddress", proxyContractAddress), + ) + return err + } + + // Fetch ABIs + bytecodeHash, abi, err := cm.AbiFetcher.FetchContractDetails(ctx, proxyContractAddress) + if err != nil { + cm.Logger.Sugar().Errorw("Failed to fetch metadata from proxy contract", + zap.Error(err), + zap.String("proxyContractAddress", proxyContractAddress), + ) + return err + } + + // Create contract + _, err = cm.ContractStore.CreateContract( + proxyContractAddress, + abi, + true, + bytecodeHash, + "", + true, + ) + if err != nil { + cm.Logger.Sugar().Errorw("Failed to create new contract for proxy contract", + zap.Error(err), + zap.String("proxyContractAddress", proxyContractAddress), + ) + return err + } + cm.Logger.Sugar().Debugf("Created new contract for proxy contract", zap.String("proxyContractAddress", proxyContractAddress)) + + return nil +} diff --git a/pkg/contractManager/contractManager_test.go b/pkg/contractManager/contractManager_test.go new file mode 100644 index 00000000..aabf161e --- /dev/null +++ b/pkg/contractManager/contractManager_test.go @@ -0,0 +1,208 @@ +package contractManager + +import ( + "context" + "database/sql" + "log" + "net/http" + "reflect" + "testing" + "time" + + "os" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/logger" + "github.com/Layr-Labs/sidecar/internal/metrics" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/abiFetcher" + "github.com/Layr-Labs/sidecar/pkg/clients/ethereum" + "github.com/Layr-Labs/sidecar/pkg/contractStore" + "github.com/Layr-Labs/sidecar/pkg/contractStore/postgresContractStore" + "github.com/Layr-Labs/sidecar/pkg/parser" + "github.com/Layr-Labs/sidecar/pkg/postgres" + "github.com/agiledragon/gomonkey/v2" + "github.com/ethereum/go-ethereum/common" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func setup() ( + string, + *gorm.DB, + *zap.Logger, + *config.Config, + error, +) { + cfg := config.NewConfig() + cfg.Chain = config.Chain_Mainnet + cfg.Debug = os.Getenv(config.Debug) == "true" + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return dbname, nil, nil, nil, err + } + + return dbname, grm, l, cfg, nil +} + +func Test_ContractManager(t *testing.T) { + dbName, grm, l, cfg, err := setup() + if err != nil { + t.Fatal(err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", "http://72.46.85.253:8545", + httpmock.NewStringResponder(200, `{"result": "0x0000000000000000000000004567890123456789012345678901234567890123"}`)) + + mockHttpClient := &http.Client{ + Transport: httpmock.DefaultTransport, + } + + baseUrl := "http://72.46.85.253:8545" + ethConfig := ethereum.DefaultNativeCallEthereumClientConfig() + ethConfig.BaseUrl = baseUrl + + client := ethereum.NewClient(ethConfig, l) + client.SetHttpClient(mockHttpClient) + + af := abiFetcher.NewAbiFetcher(client, &http.Client{Timeout: 5 * time.Second}, l, cfg) + + metricsClients, err := metrics.InitMetricsSinksFromConfig(cfg, l) + if err != nil { + l.Sugar().Fatal("Failed to setup metrics sink", zap.Error(err)) + } + + contract := &contractStore.Contract{ + ContractAddress: "0x1234567890abcdef1234567890abcdef12345678", + ContractAbi: "[]", + Verified: true, + BytecodeHash: "bdb91271fe8c69b356d8f42eaa7e00d0e119258706ae4179403aa2ea45caffed", + MatchingContractAddress: "", + } + proxyContract := &contractStore.ProxyContract{ + BlockNumber: 1, + ContractAddress: contract.ContractAddress, + ProxyContractAddress: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", + } + + sdc, err := metrics.NewMetricsSink(&metrics.MetricsSinkConfig{}, metricsClients) + if err != nil { + l.Sugar().Fatal("Failed to setup metrics sink", zap.Error(err)) + } + + contractStore := postgresContractStore.NewPostgresContractStore(grm, l, cfg) + if err := contractStore.InitializeCoreContracts(); err != nil { + log.Fatalf("Failed to initialize core contracts: %v", err) + } + + t.Run("Test indexing contract upgrades", func(t *testing.T) { + // Create a contract + _, err := contractStore.CreateContract(contract.ContractAddress, contract.ContractAbi, contract.Verified, contract.BytecodeHash, contract.MatchingContractAddress, false) + assert.Nil(t, err) + + // Create a proxy contract + _, err = contractStore.CreateProxyContract(uint64(proxyContract.BlockNumber), proxyContract.ContractAddress, proxyContract.ProxyContractAddress) + assert.Nil(t, err) + + // Check if contract and proxy contract exist + var contractCount int + contractAddress := contract.ContractAddress + res := grm.Raw(`select count(*) from contracts where contract_address=@contractAddress`, sql.Named("contractAddress", contractAddress)).Scan(&contractCount) + assert.Nil(t, res.Error) + assert.Equal(t, 1, contractCount) + + proxyContractAddress := proxyContract.ContractAddress + res = grm.Raw(`select count(*) from contracts where contract_address=@proxyContractAddress`, sql.Named("proxyContractAddress", proxyContractAddress)).Scan(&contractCount) + assert.Nil(t, res.Error) + assert.Equal(t, 1, contractCount) + + var proxyContractCount int + res = grm.Raw(`select count(*) from proxy_contracts where contract_address=@contractAddress`, sql.Named("contractAddress", contractAddress)).Scan(&proxyContractCount) + assert.Nil(t, res.Error) + assert.Equal(t, 1, proxyContractCount) + + // An upgrade event + upgradedLog := &parser.DecodedLog{ + LogIndex: 0, + Address: contract.ContractAddress, + EventName: "Upgraded", + Arguments: []parser.Argument{ + { + Name: "implementation", + Type: "address", + Value: common.HexToAddress("0x7890123456789012345678901234567890123456"), + Indexed: true, + }, + }, + } + + // Patch abiFetcher + patches := gomonkey.ApplyMethod(reflect.TypeOf(af), "FetchContractDetails", + func(_ *abiFetcher.AbiFetcher, _ context.Context, _ string) (string, string, error) { + return "mockedBytecodeHash", "mockedAbi", nil + }) + defer patches.Reset() + + // Perform the upgrade + blockNumber := 5 + cm := NewContractManager(contractStore, client, af, sdc, l) + err = cm.HandleContractUpgrade(context.Background(), uint64(blockNumber), upgradedLog) + assert.Nil(t, err) + + // Verify database state after upgrade + newProxyContractAddress := upgradedLog.Arguments[0].Value.(common.Address).Hex() + res = grm.Raw(`select count(*) from contracts where contract_address=@newProxyContractAddress`, sql.Named("newProxyContractAddress", newProxyContractAddress)).Scan(&contractCount) + assert.Nil(t, res.Error) + assert.Equal(t, 1, contractCount) + + res = grm.Raw(`select count(*) from proxy_contracts where contract_address=@contractAddress`, sql.Named("contractAddress", contractAddress)).Scan(&proxyContractCount) + assert.Nil(t, res.Error) + assert.Equal(t, 2, proxyContractCount) + }) + t.Run("Test getting address from storage slot", func(t *testing.T) { + // An upgrade event without implementation argument + upgradedLog := &parser.DecodedLog{ + LogIndex: 0, + Address: contract.ContractAddress, + EventName: "Upgraded", + Arguments: []parser.Argument{}, + } + + // Patch abiFetcher + patches := gomonkey.ApplyMethod(reflect.TypeOf(af), "FetchContractDetails", + func(_ *abiFetcher.AbiFetcher, _ context.Context, _ string) (string, string, error) { + return "mockedBytecodeHash", "mockedAbi", nil + }) + defer patches.Reset() + + // Perform the upgrade + blockNumber := 10 + cm := NewContractManager(contractStore, client, af, sdc, l) + err = cm.HandleContractUpgrade(context.Background(), uint64(blockNumber), upgradedLog) + assert.Nil(t, err) + + // Verify database state after upgrade + var contractCount int + var proxyContractCount int + newProxyContractAddress := "0x4567890123456789012345678901234567890123" + res := grm.Raw(`select count(*) from contracts where contract_address=@newProxyContractAddress`, sql.Named("newProxyContractAddress", newProxyContractAddress)).Scan(&contractCount) + assert.Nil(t, res.Error) + assert.Equal(t, 1, contractCount) + + res = grm.Raw(`select count(*) from proxy_contracts where contract_address=@contractAddress`, sql.Named("contractAddress", contract.ContractAddress)).Scan(&proxyContractCount) + assert.Nil(t, res.Error) + assert.Equal(t, 3, proxyContractCount) + }) + t.Cleanup(func() { + postgres.TeardownTestDatabase(dbName, cfg, grm, l) + }) +} diff --git a/pkg/contractStore/contractStore.go b/pkg/contractStore/contractStore.go index 5d5ff5b9..0bcaf8b8 100644 --- a/pkg/contractStore/contractStore.go +++ b/pkg/contractStore/contractStore.go @@ -12,7 +12,10 @@ var CoreContracts embed.FS type ContractStore interface { GetContractForAddress(address string) (*Contract, error) + GetProxyContractForAddress(blockNumber uint64, address string) (*ProxyContract, error) + CreateContract(address string, abiJson string, verified bool, bytecodeHash string, matchingContractAddress string, checkedForAbi bool) (*Contract, error) FindOrCreateContract(address string, abiJson string, verified bool, bytecodeHash string, matchingContractAddress string, checkedForAbi bool) (*Contract, bool, error) + CreateProxyContract(blockNumber uint64, contractAddress string, proxyContractAddress string) (*ProxyContract, error) FindOrCreateProxyContract(blockNumber uint64, contractAddress string, proxyContractAddress string) (*ProxyContract, bool, error) GetContractWithProxyContract(address string, atBlockNumber uint64) (*ContractsTree, error) SetContractCheckedForProxy(address string) (*Contract, error) diff --git a/pkg/contractStore/postgresContractStore/postgresContractStore.go b/pkg/contractStore/postgresContractStore/postgresContractStore.go index 09c22918..0aa6bdc1 100644 --- a/pkg/contractStore/postgresContractStore/postgresContractStore.go +++ b/pkg/contractStore/postgresContractStore/postgresContractStore.go @@ -45,6 +45,46 @@ func (s *PostgresContractStore) GetContractForAddress(address string) (*contract return contract, nil } +func (s *PostgresContractStore) GetProxyContractForAddress(blockNumber uint64, address string) (*contractStore.ProxyContract, error) { + var proxyContract *contractStore.ProxyContract + + result := s.Db.First(&proxyContract, "contract_address = ? and block_number = ?", address, blockNumber) + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + s.Logger.Sugar().Debugf("proxyContract not found in store '%s'", address) + return nil, nil + } + return nil, result.Error + } + + return proxyContract, nil +} + +func (s *PostgresContractStore) CreateContract( + address string, + abiJson string, + verified bool, + bytecodeHash string, + matchingContractAddress string, + checkedForAbi bool, +) (*contractStore.Contract, error) { + contract := &contractStore.Contract{ + ContractAddress: strings.ToLower(address), + ContractAbi: abiJson, + Verified: verified, + BytecodeHash: bytecodeHash, + MatchingContractAddress: matchingContractAddress, + CheckedForAbi: checkedForAbi, + } + + result := s.Db.Create(contract) + if result.Error != nil { + return nil, result.Error + } + + return contract, nil +} + func (s *PostgresContractStore) FindOrCreateContract( address string, abiJson string, @@ -66,18 +106,11 @@ func (s *PostgresContractStore) FindOrCreateContract( found = true return contract, nil } - contract = &contractStore.Contract{ - ContractAddress: strings.ToLower(address), - ContractAbi: abiJson, - Verified: verified, - BytecodeHash: bytecodeHash, - MatchingContractAddress: matchingContractAddress, - CheckedForAbi: checkedForAbi, - } - result = s.Db.Create(contract) - if result.Error != nil { - return nil, result.Error + contract, err := s.CreateContract(address, abiJson, verified, bytecodeHash, matchingContractAddress, checkedForAbi) + if err != nil { + s.Logger.Sugar().Errorw("Failed to create contract", zap.Error(err), zap.String("address", address)) + return nil, err } return contract, nil @@ -85,6 +118,25 @@ func (s *PostgresContractStore) FindOrCreateContract( return upsertedContract, found, err } +func (s *PostgresContractStore) CreateProxyContract( + blockNumber uint64, + contractAddress string, + proxyContractAddress string, +) (*contractStore.ProxyContract, error) { + proxyContract := &contractStore.ProxyContract{ + BlockNumber: int64(blockNumber), + ContractAddress: contractAddress, + ProxyContractAddress: proxyContractAddress, + } + + result := s.Db.Model(&contractStore.ProxyContract{}).Clauses(clause.Returning{}).Create(&proxyContract) + if result.Error != nil { + return nil, result.Error + } + + return proxyContract, nil +} + func (s *PostgresContractStore) FindOrCreateProxyContract( blockNumber uint64, contractAddress string, @@ -107,15 +159,11 @@ func (s *PostgresContractStore) FindOrCreateProxyContract( found = true return contract, nil } - proxyContract := &contractStore.ProxyContract{ - BlockNumber: int64(blockNumber), - ContractAddress: contractAddress, - ProxyContractAddress: proxyContractAddress, - } - result = tx.Model(&contractStore.ProxyContract{}).Clauses(clause.Returning{}).Create(&proxyContract) - if result.Error != nil { - return nil, result.Error + proxyContract, err := s.CreateProxyContract(blockNumber, contractAddress, proxyContractAddress) + if err != nil { + s.Logger.Sugar().Errorw("Failed to create proxy contract", zap.Error(err), zap.String("contractAddress", contractAddress)) + return nil, err } return proxyContract, nil diff --git a/pkg/contractStore/postgresContractStore/postgresContractStore_test.go b/pkg/contractStore/postgresContractStore/postgresContractStore_test.go index b32b625a..1783f962 100644 --- a/pkg/contractStore/postgresContractStore/postgresContractStore_test.go +++ b/pkg/contractStore/postgresContractStore/postgresContractStore_test.go @@ -196,6 +196,19 @@ func Test_PostgresContractStore(t *testing.T) { assert.Equal(t, "", contractsTree.BaseLikeAddress) assert.Equal(t, "", contractsTree.BaseLikeAbi) }) + t.Run("Get proxy contract from address", func(t *testing.T) { + proxyContract := &contractStore.ProxyContract{ + BlockNumber: 1, + ContractAddress: createdContracts[0].ContractAddress, + ProxyContractAddress: "0x456", + } + + proxy, err := cs.GetProxyContractForAddress(uint64(proxyContract.BlockNumber), proxyContract.ContractAddress) + assert.Nil(t, err) + assert.Equal(t, proxyContract.BlockNumber, proxy.BlockNumber) + assert.Equal(t, proxyContract.ContractAddress, proxy.ContractAddress) + assert.Equal(t, proxyContract.ProxyContractAddress, proxy.ProxyContractAddress) + }) t.Run("Set contract checked for proxy", func(t *testing.T) { address := createdContracts[0].ContractAddress diff --git a/pkg/indexer/restakedStrategies_test.go b/pkg/indexer/restakedStrategies_test.go index 0759492b..55a9b1d4 100644 --- a/pkg/indexer/restakedStrategies_test.go +++ b/pkg/indexer/restakedStrategies_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "net/http" "testing" "time" @@ -13,6 +14,7 @@ import ( "github.com/Layr-Labs/sidecar/internal/logger" "github.com/Layr-Labs/sidecar/internal/metrics" "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/abiFetcher" "github.com/Layr-Labs/sidecar/pkg/clients/ethereum" "github.com/Layr-Labs/sidecar/pkg/contractCaller/multicallContractCaller" "github.com/Layr-Labs/sidecar/pkg/contractCaller/sequentialContractCaller" @@ -75,6 +77,8 @@ func Test_IndexerRestakedStrategies(t *testing.T) { client := ethereum.NewClient(ethConfig, l) + af := abiFetcher.NewAbiFetcher(client, &http.Client{Timeout: 5 * time.Second}, l, cfg) + metricsClients, err := metrics.InitMetricsSinksFromConfig(cfg, l) if err != nil { l.Sugar().Fatal("Failed to setup metrics sink", zap.Error(err)) @@ -98,7 +102,7 @@ func Test_IndexerRestakedStrategies(t *testing.T) { scc := sequentialContractCaller.NewSequentialContractCaller(client, cfg, 10, l) - cm := contractManager.NewContractManager(contractStore, client, sdc, l) + cm := contractManager.NewContractManager(contractStore, client, af, sdc, l) t.Run("Integration - gets restaked strategies for avs/operator with multicall contract caller", func(t *testing.T) { avs := "0xD4A7E1Bd8015057293f0D0A557088c286942e84b" diff --git a/pkg/pipeline/pipelineIntegration_test.go b/pkg/pipeline/pipelineIntegration_test.go index b60f014d..53cce5bf 100644 --- a/pkg/pipeline/pipelineIntegration_test.go +++ b/pkg/pipeline/pipelineIntegration_test.go @@ -6,10 +6,13 @@ import ( "fmt" "github.com/Layr-Labs/sidecar/pkg/metaState" "log" + "net/http" "testing" + "time" "os" + "github.com/Layr-Labs/sidecar/pkg/abiFetcher" "github.com/Layr-Labs/sidecar/pkg/clients/ethereum" "github.com/Layr-Labs/sidecar/pkg/contractCaller/sequentialContractCaller" "github.com/Layr-Labs/sidecar/pkg/contractManager" @@ -77,6 +80,8 @@ func setup(ethConfig *ethereum.EthereumClientConfig) ( ethConfig.BaseUrl = rpcUrl client := ethereum.NewClient(ethConfig, l) + af := abiFetcher.NewAbiFetcher(client, &http.Client{Timeout: 5 * time.Second}, l, cfg) + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) if err != nil { log.Fatal(err) @@ -87,7 +92,7 @@ func setup(ethConfig *ethereum.EthereumClientConfig) ( log.Fatalf("Failed to initialize core contracts: %v", err) } - cm := contractManager.NewContractManager(contractStore, client, sdc, l) + cm := contractManager.NewContractManager(contractStore, client, af, sdc, l) mds := pgStorage.NewPostgresBlockStore(grm, l, cfg)