Commit e253b19d authored by smartcontracts's avatar smartcontracts Committed by GitHub

maint: rewrite spacers check to use new framework (#13232)

Updates the spacers check to use the new framework for contracts
checks written in Go. Adds tests for the functions that this
check uses.
parent e4713361
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
)
// directoryPath is the path to the artifacts directory.
// It can be configured as the first argument to the script or
// defaults to the forge-artifacts directory.
var directoryPath string
func init() {
if len(os.Args) > 1 {
directoryPath = os.Args[1]
} else {
currentDir, _ := os.Getwd()
directoryPath = filepath.Join(currentDir, "forge-artifacts")
}
}
// skipped returns true if the contract should be skipped when inspecting its storage layout.
func skipped(contractName string) bool {
return strings.Contains(contractName, "CrossDomainMessengerLegacySpacer")
}
// variableInfo represents the parsed variable information.
type variableInfo struct {
name string
slot int
offset int
length int
}
// parseVariableInfo parses out variable info from the variable structure in standard compiler json output. "github.com/ethereum-optimism/optimism/op-chain-ops/solc"
func parseVariableInfo(variable map[string]interface{}) (variableInfo, error) { "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/common"
var info variableInfo )
var err error
info.name = variable["label"].(string) func parseVariableLength(variableType string, types map[string]solc.StorageLayoutType) (int, error) {
info.slot, err = strconv.Atoi(variable["slot"].(string)) if t, exists := types[variableType]; exists {
if err != nil { return int(t.NumberOfBytes), nil
return info, err
} }
info.offset = int(variable["offset"].(float64))
variableType := variable["type"].(string)
if strings.HasPrefix(variableType, "t_mapping") { if strings.HasPrefix(variableType, "t_mapping") {
info.length = 32 return 32, nil
} else if strings.HasPrefix(variableType, "t_uint") { } else if strings.HasPrefix(variableType, "t_uint") {
re := regexp.MustCompile(`uint(\d+)`) re := regexp.MustCompile(`uint(\d+)`)
matches := re.FindStringSubmatch(variableType) matches := re.FindStringSubmatch(variableType)
if len(matches) > 1 { if len(matches) > 1 {
bitSize, _ := strconv.Atoi(matches[1]) bitSize, _ := strconv.Atoi(matches[1])
info.length = bitSize / 8 return bitSize / 8, nil
} }
} else if strings.HasPrefix(variableType, "t_bytes_") { } else if strings.HasPrefix(variableType, "t_bytes_") {
info.length = 32 return 32, nil
} else if strings.HasPrefix(variableType, "t_bytes") { } else if strings.HasPrefix(variableType, "t_bytes") {
re := regexp.MustCompile(`bytes(\d+)`) re := regexp.MustCompile(`bytes(\d+)`)
matches := re.FindStringSubmatch(variableType) matches := re.FindStringSubmatch(variableType)
if len(matches) > 1 { if len(matches) > 1 {
info.length, _ = strconv.Atoi(matches[1]) return strconv.Atoi(matches[1])
} }
} else if strings.HasPrefix(variableType, "t_address") { } else if strings.HasPrefix(variableType, "t_address") {
info.length = 20 return 20, nil
} else if strings.HasPrefix(variableType, "t_bool") { } else if strings.HasPrefix(variableType, "t_bool") {
info.length = 1 return 1, nil
} else if strings.HasPrefix(variableType, "t_array") { } else if strings.HasPrefix(variableType, "t_array") {
re := regexp.MustCompile(`^t_array\((\w+)\)(\d+)`) re := regexp.MustCompile(`^t_array\((\w+)\)(\d+)`)
matches := re.FindStringSubmatch(variableType) matches := re.FindStringSubmatch(variableType)
if len(matches) > 2 { if len(matches) > 2 {
innerType := matches[1] innerType := matches[1]
size, _ := strconv.Atoi(matches[2]) size, _ := strconv.Atoi(matches[2])
innerInfo, err := parseVariableInfo(map[string]interface{}{ length, err := parseVariableLength(innerType, types)
"label": variable["label"],
"offset": variable["offset"],
"slot": variable["slot"],
"type": innerType,
})
if err != nil { if err != nil {
return info, err return 0, err
} }
info.length = innerInfo.length * size return length * size, nil
} }
} else {
return info, fmt.Errorf("%s: unsupported type %s, add it to the script", info.name, variableType)
} }
return info, nil return 0, fmt.Errorf("unsupported type %s, add it to the script", variableType)
} }
func main() { func validateSpacer(variable solc.StorageLayoutEntry, types map[string]solc.StorageLayoutType) []error {
err := filepath.Walk(directoryPath, func(path string, info os.FileInfo, err error) error { var errors []error
if err != nil {
return err
}
if info.IsDir() || strings.Contains(path, "t.sol") {
return nil
}
raw, err := os.ReadFile(path)
if err != nil {
return err
}
var artifact map[string]interface{}
err = json.Unmarshal(raw, &artifact)
if err != nil {
return err
}
storageLayout, ok := artifact["storageLayout"].(map[string]interface{})
if !ok {
return nil
}
storage, ok := storageLayout["storage"].([]interface{}) parts := strings.Split(variable.Label, "_")
if !ok { if len(parts) != 4 {
return nil return []error{fmt.Errorf("invalid spacer name format: %s", variable.Label)}
} }
for _, v := range storage { expectedSlot, _ := strconv.Atoi(parts[1])
variable := v.(map[string]interface{}) expectedOffset, _ := strconv.Atoi(parts[2])
fqn := variable["contract"].(string) expectedLength, _ := strconv.Atoi(parts[3])
if skipped(fqn) { actualLength, err := parseVariableLength(variable.Type, types)
continue if err != nil {
} return []error{err}
}
label := variable["label"].(string) if int(variable.Slot) != expectedSlot {
if strings.HasPrefix(label, "spacer_") { errors = append(errors, fmt.Errorf("%s %s is in slot %d but should be in %d",
parts := strings.Split(label, "_") variable.Contract, variable.Label, variable.Slot, expectedSlot))
if len(parts) != 4 { }
return fmt.Errorf("invalid spacer name format: %s", label)
}
slot, _ := strconv.Atoi(parts[1]) if int(variable.Offset) != expectedOffset {
offset, _ := strconv.Atoi(parts[2]) errors = append(errors, fmt.Errorf("%s %s is at offset %d but should be at %d",
length, _ := strconv.Atoi(parts[3]) variable.Contract, variable.Label, variable.Offset, expectedOffset))
}
variableInfo, err := parseVariableInfo(variable) if actualLength != expectedLength {
if err != nil { errors = append(errors, fmt.Errorf("%s %s is %d bytes long but should be %d",
return err variable.Contract, variable.Label, actualLength, expectedLength))
} }
if slot != variableInfo.slot { return errors
return fmt.Errorf("%s %s is in slot %d but should be in %d", fqn, label, variableInfo.slot, slot) }
}
if offset != variableInfo.offset { func processFile(path string) []error {
return fmt.Errorf("%s %s is at offset %d but should be at %d", fqn, label, variableInfo.offset, offset) artifact, err := common.ReadForgeArtifact(path)
} if err != nil {
return []error{err}
}
if length != variableInfo.length { if artifact.StorageLayout == nil {
return fmt.Errorf("%s %s is %d bytes long but should be %d", fqn, label, variableInfo.length, length) return nil
} }
fmt.Printf("%s.%s is valid\n", fqn, label) var errors []error
for _, variable := range artifact.StorageLayout.Storage {
if strings.HasPrefix(variable.Label, "spacer_") {
if errs := validateSpacer(variable, artifact.StorageLayout.Types); len(errs) > 0 {
errors = append(errors, errs...)
continue
} }
} }
}
return nil return errors
}) }
if err != nil { func main() {
if err := common.ProcessFilesGlob(
[]string{"forge-artifacts/**/*.json"},
[]string{"forge-artifacts/**/CrossDomainMessengerLegacySpacer{0,1}.json"},
processFile,
); err != nil {
fmt.Printf("Error: %v\n", err) fmt.Printf("Error: %v\n", err)
os.Exit(1) os.Exit(1)
} }
......
package main
import (
"testing"
"github.com/ethereum-optimism/optimism/op-chain-ops/solc"
"github.com/stretchr/testify/require"
)
func Test_parseVariableLength(t *testing.T) {
tests := []struct {
name string
variableType string
types map[string]solc.StorageLayoutType
expected int
expectError bool
}{
{
name: "uses type from map",
variableType: "t_custom",
types: map[string]solc.StorageLayoutType{
"t_custom": {NumberOfBytes: 16},
},
expected: 16,
},
{
name: "mapping type",
variableType: "t_mapping(address,uint256)",
expected: 32,
},
{
name: "uint type",
variableType: "t_uint256",
expected: 32,
},
{
name: "bytes_ type",
variableType: "t_bytes_storage",
expected: 32,
},
{
name: "bytes type",
variableType: "t_bytes32",
expected: 32,
},
{
name: "address type",
variableType: "t_address",
expected: 20,
},
{
name: "bool type",
variableType: "t_bool",
expected: 1,
},
{
name: "array type",
variableType: "t_array(t_uint256)2",
expected: 64, // 2 * 32
},
{
name: "unsupported type",
variableType: "t_unknown",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
length, err := parseVariableLength(tt.variableType, tt.types)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expected, length)
}
})
}
}
func Test_validateSpacer(t *testing.T) {
tests := []struct {
name string
variable solc.StorageLayoutEntry
types map[string]solc.StorageLayoutType
expectedErrs int
errorContains string
}{
{
name: "valid spacer",
variable: solc.StorageLayoutEntry{
Contract: "TestContract",
Label: "spacer_1_2_32",
Slot: 1,
Offset: 2,
Type: "t_uint256",
},
types: map[string]solc.StorageLayoutType{
"t_uint256": {NumberOfBytes: 32},
},
expectedErrs: 0,
},
{
name: "invalid name format",
variable: solc.StorageLayoutEntry{
Label: "spacer_invalid",
},
expectedErrs: 1,
errorContains: "invalid spacer name format",
},
{
name: "wrong slot",
variable: solc.StorageLayoutEntry{
Contract: "TestContract",
Label: "spacer_1_2_32",
Slot: 2,
Offset: 2,
Type: "t_uint256",
},
types: map[string]solc.StorageLayoutType{
"t_uint256": {NumberOfBytes: 32},
},
expectedErrs: 1,
errorContains: "is in slot",
},
{
name: "wrong offset",
variable: solc.StorageLayoutEntry{
Contract: "TestContract",
Label: "spacer_1_2_32",
Slot: 1,
Offset: 3,
Type: "t_uint256",
},
types: map[string]solc.StorageLayoutType{
"t_uint256": {NumberOfBytes: 32},
},
expectedErrs: 1,
errorContains: "is at offset",
},
{
name: "wrong length",
variable: solc.StorageLayoutEntry{
Contract: "TestContract",
Label: "spacer_1_2_32",
Slot: 1,
Offset: 2,
Type: "t_uint128",
},
types: map[string]solc.StorageLayoutType{
"t_uint128": {NumberOfBytes: 16},
},
expectedErrs: 1,
errorContains: "bytes long",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errors := validateSpacer(tt.variable, tt.types)
require.Len(t, errors, tt.expectedErrs)
if tt.errorContains != "" {
require.Contains(t, errors[0].Error(), tt.errorContains)
}
})
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment