Skip to content

Commit c5526e5

Browse files
committed
feat: add support for eip 1967 proxy decoding of implementation contract
1 parent 909e6f4 commit c5526e5

14 files changed

Lines changed: 1129 additions & 126 deletions

File tree

.changeset/floppy-trams-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
Add support to decode proposals that utse EIP-1967 proxies

chain/evm/evm_chain.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ type OnchainClient interface {
2525

2626
BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error)
2727
NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error)
28+
// StorageAt reads a storage slot from the given account at the specified block number.
29+
// This is needed for operations like EIP-1967 proxy detection.
30+
StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error)
2831
}
2932

3033
// Chain represents an EVM chain.

chain/evm/provider/rpcclient/multiclient.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,19 @@ func (mc *MultiClient) BalanceAt(ctx context.Context, account common.Address, bl
309309
return balance, err
310310
}
311311

312+
// StorageAt is a wrapper around the ethclient.StorageAt method that retries on failure.
313+
func (mc *MultiClient) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) {
314+
var storage []byte
315+
err := mc.retryWithBackups(ctx, "StorageAt", func(ct context.Context, client *ethclient.Client) error {
316+
var err error
317+
storage, err = client.StorageAt(ct, account, key, blockNumber)
318+
319+
return err
320+
})
321+
322+
return storage, err
323+
}
324+
312325
// FilterLogs is a wrapper around the ethclient.FilterLogs method that retries on failure.
313326
func (mc *MultiClient) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) {
314327
var logs []types.Log

engine/cld/legacy/cli/commands/durable-pipelines.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func (c Commands) newDurablePipelineRun(
197197
return err
198198
}
199199
for idx, proposal := range out.MCMSTimelockProposals {
200-
describedProposal, err := analyzer.DescribeTimelockProposal(proposalContext, &proposal)
200+
describedProposal, err := analyzer.DescribeTimelockProposal(cmd.Context(), proposalContext, env, &proposal)
201201
if err != nil {
202202
c.lggr.Errorf("failed to describe time lock proposal %d: %w", idx, err)
203203
continue

engine/cld/legacy/cli/commands/migration.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func (c Commands) newMigrationRun(
179179
return err
180180
}
181181
for idx, proposal := range out.MCMSTimelockProposals {
182-
describedProposal, err := analyzer.DescribeTimelockProposal(proposalContext, &proposal)
182+
describedProposal, err := analyzer.DescribeTimelockProposal(cmd.Context(), proposalContext, env, &proposal)
183183
if err != nil {
184184
cmd.PrintErrf("failed to describe time lock proposal %d: %v\n", idx, err)
185185
continue

engine/cld/legacy/cli/mcmsv2/mcms_v2.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -738,9 +738,9 @@ func buildMCMSv2AnalyzeProposalCmd(
738738

739739
var analyzedProposal string
740740
if cfgv2.timelockProposal != nil {
741-
analyzedProposal, err = analyzer.DescribeTimelockProposal(cfgv2.proposalCtx, cfgv2.timelockProposal)
741+
analyzedProposal, err = analyzer.DescribeTimelockProposal(cmd.Context(), cfgv2.proposalCtx, cfgv2.env, cfgv2.timelockProposal)
742742
} else {
743-
analyzedProposal, err = analyzer.DescribeProposal(cfgv2.proposalCtx, &cfgv2.proposal)
743+
analyzedProposal, err = analyzer.DescribeProposal(cmd.Context(), cfgv2.proposalCtx, cfgv2.env, &cfgv2.proposal)
744744
}
745745
if err != nil {
746746
return fmt.Errorf("failed to describe proposal: %w", err)
@@ -882,9 +882,9 @@ func buildMCMSv2ConvertUpf(
882882
var convertedProposal string
883883

884884
if cfgv2.timelockProposal != nil {
885-
convertedProposal, err = upf.UpfConvertTimelockProposal(cfgv2.proposalCtx, cfgv2.timelockProposal, &cfgv2.proposal, signers)
885+
convertedProposal, err = upf.UpfConvertTimelockProposal(cmd.Context(), cfgv2.proposalCtx, cfgv2.env, cfgv2.timelockProposal, &cfgv2.proposal, signers)
886886
} else {
887-
convertedProposal, err = upf.UpfConvertProposal(cfgv2.proposalCtx, &cfgv2.proposal, signers)
887+
convertedProposal, err = upf.UpfConvertProposal(cfgv2.proposalCtx, cfgv2.env, &cfgv2.proposal, signers)
888888
}
889889
if err != nil {
890890
return fmt.Errorf("failed to convert proposal to UPF format: %w", err)

experimental/analyzer/describe.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package analyzer
22

33
import (
4+
"context"
5+
46
"github.com/smartcontractkit/mcms"
7+
8+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
59
)
610

7-
func DescribeTimelockProposal(ctx ProposalContext, proposal *mcms.TimelockProposal) (string, error) {
8-
report, err := BuildTimelockReport(ctx, proposal)
11+
func DescribeTimelockProposal(ctx context.Context, proposalCtx ProposalContext, env deployment.Environment, proposal *mcms.TimelockProposal) (string, error) {
12+
report, err := BuildTimelockReport(ctx, proposalCtx, env, proposal)
913
if err != nil {
1014
return "", err
1115
}
@@ -16,13 +20,13 @@ func DescribeTimelockProposal(ctx ProposalContext, proposal *mcms.TimelockPropos
1620
if len(proposal.Operations) > 0 {
1721
chainSelector = uint64(proposal.Operations[0].ChainSelector)
1822
}
19-
fieldCtx := ctx.FieldsContext(chainSelector)
23+
fieldCtx := proposalCtx.FieldsContext(chainSelector)
2024

21-
return ctx.GetRenderer().RenderTimelockProposal(report, fieldCtx), nil
25+
return proposalCtx.GetRenderer().RenderTimelockProposal(report, fieldCtx), nil
2226
}
2327

24-
func DescribeProposal(ctx ProposalContext, proposal *mcms.Proposal) (string, error) {
25-
report, err := BuildProposalReport(ctx, proposal)
28+
func DescribeProposal(ctx context.Context, proposalContext ProposalContext, env deployment.Environment, proposal *mcms.Proposal) (string, error) {
29+
report, err := BuildProposalReport(ctx, proposalContext, env, proposal)
2630
if err != nil {
2731
return "", err
2832
}
@@ -33,7 +37,7 @@ func DescribeProposal(ctx ProposalContext, proposal *mcms.Proposal) (string, err
3337
if len(proposal.Operations) > 0 {
3438
chainSelector = uint64(proposal.Operations[0].ChainSelector)
3539
}
36-
fieldCtx := ctx.FieldsContext(chainSelector)
40+
fieldCtx := proposalContext.FieldsContext(chainSelector)
3741

38-
return ctx.GetRenderer().RenderProposal(report, fieldCtx), nil
42+
return proposalContext.GetRenderer().RenderProposal(report, fieldCtx), nil
3943
}

experimental/analyzer/describe_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
func TestDescribeProposal(t *testing.T) {
1515
t.Parallel()
1616

17-
ctx := &DefaultProposalContext{
17+
proposalCtx := &DefaultProposalContext{
1818
AddressesByChain: deployment.AddressesByChain{},
1919
renderer: NewMarkdownRenderer(),
2020
}
@@ -88,7 +88,7 @@ func TestDescribeProposal(t *testing.T) {
8888
t.Parallel()
8989

9090
proposal := &mcms.Proposal{Operations: tt.operations}
91-
output, err := DescribeProposal(ctx, proposal)
91+
output, err := DescribeProposal(t.Context(), proposalCtx, deployment.Environment{}, proposal)
9292

9393
if tt.expectError {
9494
require.Error(t, err)
@@ -112,7 +112,7 @@ func TestDescribeProposal(t *testing.T) {
112112
func TestDescribeTimelockProposal(t *testing.T) {
113113
t.Parallel()
114114

115-
ctx := &DefaultProposalContext{
115+
proposalCtx := &DefaultProposalContext{
116116
AddressesByChain: deployment.AddressesByChain{},
117117
renderer: NewMarkdownRenderer(),
118118
}
@@ -194,7 +194,7 @@ func TestDescribeTimelockProposal(t *testing.T) {
194194
t.Parallel()
195195

196196
proposal := &mcms.TimelockProposal{Operations: tt.operations}
197-
output, err := DescribeTimelockProposal(ctx, proposal)
197+
output, err := DescribeTimelockProposal(t.Context(), proposalCtx, deployment.Environment{}, proposal)
198198

199199
if tt.expectError {
200200
require.Error(t, err)

experimental/analyzer/evm_analyzer.go

Lines changed: 159 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
package analyzer
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"fmt"
78
"math/big"
9+
"strings"
810

911
"github.com/ethereum/go-ethereum/accounts/abi"
12+
"github.com/ethereum/go-ethereum/common"
1013

1114
chainsel "github.com/smartcontractkit/chain-selectors"
1215
"github.com/smartcontractkit/mcms/types"
16+
17+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
18+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
19+
"github.com/smartcontractkit/chainlink-deployments-framework/experimental/analyzer/pointer"
1320
)
1421

15-
func AnalyzeEVMTransactions(ctx ProposalContext, chainSelector uint64, txs []types.Transaction) ([]*DecodedCall, error) {
22+
// EIP1967TargetContractStorageSlot is the storage slot for EIP-1967 proxy implementation address
23+
// keccak256("eip1967.proxy.implementation") - 1
24+
var EIP1967TargetContractStorageSlot = common.HexToHash("0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")
25+
26+
func AnalyzeEVMTransactions(ctx context.Context, proposalCtx ProposalContext, env deployment.Environment, chainSelector uint64, txs []types.Transaction) ([]*DecodedCall, error) {
1627
chainFamily, err := chainsel.GetSelectorFamily(chainSelector)
1728
if err != nil {
1829
return nil, fmt.Errorf("failed to get chain family for selector %v: %w", chainSelector, err)
@@ -25,7 +36,7 @@ func AnalyzeEVMTransactions(ctx ProposalContext, chainSelector uint64, txs []typ
2536

2637
decodedTxs := make([]*DecodedCall, len(txs))
2738
for i, op := range txs {
28-
decodedTxs[i], _, _, err = AnalyzeEVMTransaction(ctx, decoder, chainSelector, op)
39+
decodedTxs[i], _, _, err = AnalyzeEVMTransaction(ctx, proposalCtx, env, decoder, chainSelector, op)
2940
if err != nil {
3041
return nil, fmt.Errorf("failed to analyze transaction %d: %w", i, err)
3142
}
@@ -35,14 +46,14 @@ func AnalyzeEVMTransactions(ctx ProposalContext, chainSelector uint64, txs []typ
3546
}
3647

3748
func AnalyzeEVMTransaction(
38-
ctx ProposalContext, decoder *EVMTxCallDecoder, chainSelector uint64, mcmsTx types.Transaction,
49+
ctx context.Context, proposalCtx ProposalContext, env deployment.Environment, decoder *EVMTxCallDecoder, chainSelector uint64, mcmsTx types.Transaction,
3950
) (*DecodedCall, *abi.ABI, string, error) {
4051
// Check if this is a native token transfer
4152
if isNativeTokenTransfer(mcmsTx) {
4253
return createNativeTransferCall(mcmsTx), nil, "", nil
4354
}
4455

45-
evmRegistry := ctx.GetEVMRegistry()
56+
evmRegistry := proposalCtx.GetEVMRegistry()
4657
if evmRegistry == nil {
4758
return nil, nil, "", errors.New("EVM registry is not available")
4859
}
@@ -53,6 +64,19 @@ func AnalyzeEVMTransaction(
5364

5465
analyzeResult, err := decoder.Decode(mcmsTx.To, abi, mcmsTx.Data)
5566
if err != nil {
67+
// Check if this is a "method not found" error - could be a proxy with wrong ABI
68+
if isMethodNotFoundError(err) {
69+
// Try EIP-1967 proxy fallback: query implementation slot and retry with implementation ABI
70+
fallbackResult, fallbackABI, fallbackABIStr, fallbackErr := tryEIP1967ProxyFallback(
71+
ctx, proposalCtx, env, chainSelector, mcmsTx.To, mcmsTx.Data, decoder,
72+
)
73+
if fallbackErr == nil {
74+
// Successfully decoded with implementation ABI
75+
return fallbackResult, fallbackABI, fallbackABIStr, nil
76+
}
77+
// Fallback failed, return original error
78+
}
79+
5680
return nil, nil, "", fmt.Errorf("error analyzing operation: %w", err)
5781
}
5882

@@ -117,3 +141,134 @@ func createNativeTransferCall(mcmsTx types.Transaction) *DecodedCall {
117141
Outputs: []NamedField{},
118142
}
119143
}
144+
145+
// isMethodNotFoundError checks if an error indicates a method not found in ABI.
146+
// This typically happens when trying to decode a transaction with the wrong ABI,
147+
// such as using a proxy ABI when the transaction is actually calling the implementation.
148+
func isMethodNotFoundError(err error) bool {
149+
if err == nil {
150+
return false
151+
}
152+
errStr := strings.ToLower(err.Error())
153+
154+
return strings.Contains(errStr, "no method with id") ||
155+
strings.Contains(errStr, "method not found") ||
156+
strings.Contains(errStr, "invalid method id")
157+
}
158+
159+
// queryEIP1967ImplementationSlot queries the EIP-1967 implementation storage slot
160+
// and returns the implementation address if found.
161+
func queryEIP1967ImplementationSlot(ctx context.Context, evmChain evm.Chain, proxyAddress string) (common.Address, error) {
162+
storageValue, err := evmChain.Client.StorageAt(ctx, common.HexToAddress(proxyAddress), EIP1967TargetContractStorageSlot, nil)
163+
if err != nil {
164+
return common.Address{}, fmt.Errorf("failed to read EIP-1967 storage slot: %w", err)
165+
}
166+
167+
// Extract address from storage (last 20 bytes, right-padded)
168+
implAddress := common.BytesToAddress(storageValue)
169+
170+
return implAddress, nil
171+
}
172+
173+
// tryEIP1967ProxyFallback attempts to decode using implementation ABI if address is EIP-1967 proxy.
174+
// This function orchestrates the full fallback flow:
175+
// 1. Gets EVM chain from environment
176+
// 2. Queries EIP-1967 implementation slot
177+
// 3. Looks up implementation TypeAndVersion from address book
178+
// 4. Gets implementation ABI
179+
// 5. Retries decode with implementation ABI
180+
func tryEIP1967ProxyFallback(
181+
ctx context.Context,
182+
proposalCtx ProposalContext,
183+
env deployment.Environment,
184+
chainSelector uint64,
185+
proxyAddress string,
186+
txData []byte,
187+
decoder *EVMTxCallDecoder,
188+
) (*DecodedCall, *abi.ABI, string, error) {
189+
// Lazily get EVM chain from environment (only when fallback is needed)
190+
evmChains := env.BlockChains.EVMChains()
191+
evmChain, exists := evmChains[chainSelector]
192+
if !exists {
193+
return nil, nil, "", fmt.Errorf("EVM chain not available for selector %d", chainSelector)
194+
}
195+
196+
// Query EIP-1967 implementation slot
197+
implAddress, err := queryEIP1967ImplementationSlot(ctx, evmChain, proxyAddress)
198+
if err != nil {
199+
return nil, nil, "", fmt.Errorf("failed to query EIP-1967 implementation: %w", err)
200+
}
201+
202+
// Check if implementation address is zero (not an EIP-1967 proxy)
203+
if implAddress == (common.Address{}) || implAddress == common.HexToAddress("0x0") {
204+
return nil, nil, "", errors.New("EIP-1967 slot contains zero address (not a proxy)")
205+
}
206+
207+
// Look up implementation TypeAndVersion from address book (checking both ExistingAddresses and DataStore)
208+
implAddressStr := implAddress.Hex()
209+
addressesByChain, err := getAllAddressesByChain(env)
210+
if err != nil {
211+
return nil, nil, "", fmt.Errorf("failed to get addresses: %w", err)
212+
}
213+
214+
addressesForChain, ok := addressesByChain[chainSelector]
215+
if !ok {
216+
return nil, nil, "", fmt.Errorf("no addresses found for chain selector %d", chainSelector)
217+
}
218+
219+
implTypeAndVersion, ok := addressesForChain[implAddressStr]
220+
if !ok {
221+
return nil, nil, "", fmt.Errorf("implementation address %s not found in address book or datastore for chain selector %d", implAddressStr, chainSelector)
222+
}
223+
224+
// Get implementation ABI using existing registry method
225+
evmRegistry := proposalCtx.GetEVMRegistry()
226+
if evmRegistry == nil {
227+
return nil, nil, "", errors.New("EVM registry is not available")
228+
}
229+
230+
implABI, implABIStr, err := evmRegistry.GetABIByType(implTypeAndVersion)
231+
if err != nil {
232+
return nil, nil, "", fmt.Errorf("failed to get ABI for implementation %v: %w", implTypeAndVersion, err)
233+
}
234+
235+
// Retry decode with implementation ABI
236+
decodedResult, err := decoder.Decode(proxyAddress, implABI, txData)
237+
if err != nil {
238+
return nil, nil, "", fmt.Errorf("failed to decode with implementation ABI: %w", err)
239+
}
240+
241+
return decodedResult, implABI, implABIStr, nil
242+
}
243+
244+
// getAllAddressesByChain retrieves addresses from both ExistingAddresses and DataStore,
245+
// merging them into a single map.
246+
func getAllAddressesByChain(env deployment.Environment) (deployment.AddressesByChain, error) {
247+
// Start with addresses from ExistingAddresses
248+
addressesByChain, err := env.ExistingAddresses.Addresses() //nolint:staticcheck
249+
if err != nil {
250+
return nil, fmt.Errorf("failed to get addresses from ExistingAddresses: %w", err)
251+
}
252+
253+
// Fetch addresses from DataStore
254+
dataStoreAddresses, err := env.DataStore.Addresses().Fetch()
255+
if err != nil {
256+
return nil, fmt.Errorf("failed to fetch addresses from DataStore: %w", err)
257+
}
258+
259+
// Merge DataStore addresses into the map
260+
for _, address := range dataStoreAddresses {
261+
chainAddresses, exists := addressesByChain[address.ChainSelector]
262+
if !exists {
263+
chainAddresses = map[string]deployment.TypeAndVersion{}
264+
}
265+
chainAddresses[address.Address] = deployment.TypeAndVersion{
266+
Type: deployment.ContractType(address.Type),
267+
Version: pointer.DerefOrEmpty(address.Version),
268+
Labels: deployment.NewLabelSet(address.Labels.List()...),
269+
}
270+
addressesByChain[address.ChainSelector] = chainAddresses
271+
}
272+
273+
return addressesByChain, nil
274+
}

0 commit comments

Comments
 (0)