Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions engine/cld/mcms/proposalanalysis/analyzer/annotated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package analyzer

import "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalanalysis/types"

var _ types.Annotation = &annotation{}

type annotation struct {
name string
atype string
value any
analyzerID string
}

func (a annotation) Name() string {
return a.name
}

func (a annotation) Type() string {
return a.atype
}

func (a annotation) Value() any {
return a.value
}

// NewAnnotation creates a new annotation with the given name, type, and value
func NewAnnotation(name, atype string, value any) types.Annotation {
return &annotation{
name: name,
atype: atype,
value: value,
}
}

// NewAnnotationWithAnalyzer creates a new annotation with analyzer ID tracking
func NewAnnotationWithAnalyzer(name, atype string, value any, analyzerID string) types.Annotation {
return &annotation{
name: name,
atype: atype,
value: value,
analyzerID: analyzerID,
}
}

// ---------------------------------------------------------------------

var _ types.Annotated = &Annotated{}

type Annotated struct {
annotations types.Annotations
}

func (a *Annotated) AddAnnotations(annotations ...types.Annotation) {
a.annotations = append(a.annotations, annotations...)
}

func (a Annotated) Annotations() types.Annotations {
return a.annotations
}

// GetAnnotationsByName returns all annotations with the given name
func (a Annotated) GetAnnotationsByName(name string) types.Annotations {
var result types.Annotations
for _, ann := range a.annotations {
if ann.Name() == name {
result = append(result, ann)
}
}
return result
}

// GetAnnotationsByType returns all annotations with the given type
func (a Annotated) GetAnnotationsByType(atype string) types.Annotations {
var result types.Annotations
for _, ann := range a.annotations {
if ann.Type() == atype {
result = append(result, ann)
}
}
return result
}

// GetAnnotationsByAnalyzer returns all annotations created by the given analyzer ID
func (a Annotated) GetAnnotationsByAnalyzer(analyzerID string) types.Annotations {
var result types.Annotations
for _, ann := range a.annotations {
// Try to cast to our internal annotation type to access analyzerID
if internalAnn, ok := ann.(*annotation); ok {
if internalAnn.analyzerID == analyzerID {
result = append(result, ann)
}
}
}
return result
}
122 changes: 122 additions & 0 deletions engine/cld/mcms/proposalanalysis/analyzer/annotations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package analyzer

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
)

func TestAnnotations(t *testing.T) {
ctx := context.Background()
_ = ctx

t.Run("NewAnnotation", func(t *testing.T) {
ann := NewAnnotation("test", "INFO", "value")
assert.Equal(t, "test", ann.Name())
assert.Equal(t, "INFO", ann.Type())
assert.Equal(t, "value", ann.Value())
})

t.Run("NewAnnotationWithAnalyzer", func(t *testing.T) {
ann := NewAnnotationWithAnalyzer("test", "WARN", "warning", "analyzer-1")
assert.Equal(t, "test", ann.Name())
assert.Equal(t, "WARN", ann.Type())
assert.Equal(t, "warning", ann.Value())
})

t.Run("AddAnnotations", func(t *testing.T) {
a := &Annotated{}
ann1 := NewAnnotation("ann1", "INFO", "v1")
ann2 := NewAnnotation("ann2", "WARN", "v2")

a.AddAnnotations(ann1)
assert.Len(t, a.Annotations(), 1)

a.AddAnnotations(ann2)
assert.Len(t, a.Annotations(), 2)
})

t.Run("GetAnnotationsByName", func(t *testing.T) {
a := &Annotated{}
ann1 := NewAnnotation("gas-estimate", "INFO", 100)
ann2 := NewAnnotation("security-check", "WARN", "vulnerable")
ann3 := NewAnnotation("gas-estimate", "INFO", 200)

a.AddAnnotations(ann1, ann2, ann3)

results := a.GetAnnotationsByName("gas-estimate")
assert.Len(t, results, 2)
assert.Equal(t, "gas-estimate", results[0].Name())
assert.Equal(t, "gas-estimate", results[1].Name())

results = a.GetAnnotationsByName("security-check")
assert.Len(t, results, 1)
assert.Equal(t, "security-check", results[0].Name())

results = a.GetAnnotationsByName("nonexistent")
assert.Len(t, results, 0)
})

t.Run("GetAnnotationsByType", func(t *testing.T) {
a := &Annotated{}
ann1 := NewAnnotation("ann1", "INFO", "v1")
ann2 := NewAnnotation("ann2", "WARN", "v2")
ann3 := NewAnnotation("ann3", "INFO", "v3")
ann4 := NewAnnotation("ann4", "ERROR", "v4")

a.AddAnnotations(ann1, ann2, ann3, ann4)

results := a.GetAnnotationsByType("INFO")
assert.Len(t, results, 2)

results = a.GetAnnotationsByType("WARN")
assert.Len(t, results, 1)

results = a.GetAnnotationsByType("ERROR")
assert.Len(t, results, 1)

results = a.GetAnnotationsByType("DIFF")
assert.Len(t, results, 0)
})

t.Run("GetAnnotationsByAnalyzer", func(t *testing.T) {
a := &Annotated{}
ann1 := NewAnnotationWithAnalyzer("ann1", "INFO", "v1", "analyzer-1")
ann2 := NewAnnotationWithAnalyzer("ann2", "WARN", "v2", "analyzer-2")
ann3 := NewAnnotationWithAnalyzer("ann3", "INFO", "v3", "analyzer-1")
ann4 := NewAnnotation("ann4", "ERROR", "v4") // No analyzer ID

a.AddAnnotations(ann1, ann2, ann3, ann4)

results := a.GetAnnotationsByAnalyzer("analyzer-1")
assert.Len(t, results, 2)

results = a.GetAnnotationsByAnalyzer("analyzer-2")
assert.Len(t, results, 1)

results = a.GetAnnotationsByAnalyzer("analyzer-3")
assert.Len(t, results, 0)
})

t.Run("Combined queries", func(t *testing.T) {
a := &Annotated{}
ann1 := NewAnnotationWithAnalyzer("gas-estimate", "INFO", 100, "gas-analyzer")
ann2 := NewAnnotationWithAnalyzer("gas-estimate", "WARN", 500, "gas-analyzer")
ann3 := NewAnnotationWithAnalyzer("security", "WARN", "issue", "security-analyzer")

a.AddAnnotations(ann1, ann2, ann3)

// Get all gas-estimate annotations
gasAnnotations := a.GetAnnotationsByName("gas-estimate")
assert.Len(t, gasAnnotations, 2)

// Get all WARN annotations
warnings := a.GetAnnotationsByType("WARN")
assert.Len(t, warnings, 2)

// Get all annotations from gas-analyzer
gasAnalyzerAnnotations := a.GetAnnotationsByAnalyzer("gas-analyzer")
assert.Len(t, gasAnalyzerAnnotations, 2)
})
}
40 changes: 40 additions & 0 deletions engine/cld/mcms/proposalanalysis/analyzer_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package proposalanalysis

import (
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalanalysis/types"
)

var _ types.AnalyzerContext = &analyzerContext{}

type analyzerContext struct {
proposal types.AnalyzedProposal
batchOperation types.AnalyzedBatchOperation
call types.AnalyzedCall
}

func (ac *analyzerContext) Proposal() types.AnalyzedProposal {
return ac.proposal
}

func (ac *analyzerContext) BatchOperation() types.AnalyzedBatchOperation {
return ac.batchOperation
}

func (ac *analyzerContext) Call() types.AnalyzedCall {
return ac.call
}

// GetAnnotationsFrom returns annotations from a specific analyzer
func (ac *analyzerContext) GetAnnotationsFrom(analyzerID string) types.Annotations {
var annotations types.Annotations
if ac.call != nil {
annotations = append(annotations, ac.call.GetAnnotationsByAnalyzer(analyzerID)...)
}
if ac.batchOperation != nil {
annotations = append(annotations, ac.batchOperation.GetAnnotationsByAnalyzer(analyzerID)...)
}
if ac.proposal != nil {
annotations = append(annotations, ac.proposal.GetAnnotationsByAnalyzer(analyzerID)...)
}
return annotations
}
79 changes: 79 additions & 0 deletions engine/cld/mcms/proposalanalysis/decoder/decoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package decoder

import (
"context"
"fmt"

"github.com/smartcontractkit/mcms"

"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalanalysis/types"
experimentalanalyzer "github.com/smartcontractkit/chainlink-deployments-framework/experimental/analyzer"
)

// ProposalDecoder decodes MCMS proposals into structured DecodedTimelockProposal
type ProposalDecoder interface {
Decode(ctx context.Context, env deployment.Environment, proposal *mcms.TimelockProposal) (types.DecodedTimelockProposal, error)
}

// legacyDecoder adapts the legacy experimental/analyzer package to the new decoder interface
type legacyDecoder struct {
evmABIMappings map[string]string
solanaDecoders map[string]experimentalanalyzer.DecodeInstructionFn
}

// NewLegacyDecoder creates a decoder that wraps legacy experimental/analyzer decoding logic.
// Use functional options to configure:
// - WithEVMABIMappings: override proposal context EVM ABI mappings
// - WithSolanaDecoders: override proposal context Solana decoder mappings
func NewLegacyDecoder(opts ...DecoderOption) ProposalDecoder {
decoder := &legacyDecoder{}

for _, opt := range opts {
opt(decoder)
}

return decoder
}

// DecoderOption is a functional option for configuring the decoder
type DecoderOption func(*legacyDecoder)

// WithEVMABIMappings overrides the proposal context EVM ABI mappings used during decoding.
func WithEVMABIMappings(mappings map[string]string) DecoderOption {
return func(d *legacyDecoder) {
d.evmABIMappings = mappings
}
}

// WithSolanaDecoders overrides the proposal context Solana decoder mappings used during decoding.
func WithSolanaDecoders(decoders map[string]experimentalanalyzer.DecodeInstructionFn) DecoderOption {
return func(d *legacyDecoder) {
d.solanaDecoders = decoders
}
}

func (d *legacyDecoder) Decode(
ctx context.Context,
env deployment.Environment,
proposal *mcms.TimelockProposal,
) (types.DecodedTimelockProposal, error) {
proposalCtx, err := experimentalanalyzer.NewDefaultProposalContext(env,
experimentalanalyzer.WithEVMABIMappings(d.evmABIMappings),
experimentalanalyzer.WithSolanaDecoders(d.solanaDecoders),
)
if err != nil {
return nil, fmt.Errorf("failed to create proposal context: %w", err)
}

// Build the report using legacy experimental analyzer
report, err := experimentalanalyzer.BuildTimelockReport(ctx, proposalCtx, env, proposal)
if err != nil {
return nil, fmt.Errorf("failed to build timelock report: %w", err)
}

// Convert to our DecodedTimelockProposal interface
return &decodedTimelockProposal{
report: report,
}, nil
}
24 changes: 24 additions & 0 deletions engine/cld/mcms/proposalanalysis/decoder/decoder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package decoder_test

import (
"testing"

"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalanalysis/decoder"
"github.com/stretchr/testify/require"
)

// TestDecoderOptions verifies that decoder options work correctly
func TestDecoderOptions(t *testing.T) {
t.Run("can create decoder with no options", func(t *testing.T) {
d := decoder.NewLegacyDecoder()
require.NotNil(t, d)
})

t.Run("can configure registry options", func(t *testing.T) {
d := decoder.NewLegacyDecoder(
decoder.WithEVMABIMappings(nil),
decoder.WithSolanaDecoders(nil),
)
require.NotNil(t, d)
})
}
Loading
Loading