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
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"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.
func parseVariableInfo(variable map[string]interface{}) (variableInfo, error) {
var info variableInfo
var err error
"github.com/ethereum-optimism/optimism/op-chain-ops/solc"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/common"
)
info.name = variable["label"].(string)
info.slot, err = strconv.Atoi(variable["slot"].(string))
if err != nil {
return info, err
func parseVariableLength(variableType string, types map[string]solc.StorageLayoutType) (int, error) {
if t, exists := types[variableType]; exists {
return int(t.NumberOfBytes), nil
}
info.offset = int(variable["offset"].(float64))
variableType := variable["type"].(string)
if strings.HasPrefix(variableType, "t_mapping") {
info.length = 32
return 32, nil
} else if strings.HasPrefix(variableType, "t_uint") {
re := regexp.MustCompile(`uint(\d+)`)
matches := re.FindStringSubmatch(variableType)
if len(matches) > 1 {
bitSize, _ := strconv.Atoi(matches[1])
info.length = bitSize / 8
return bitSize / 8, nil
}
} else if strings.HasPrefix(variableType, "t_bytes_") {
info.length = 32
return 32, nil
} else if strings.HasPrefix(variableType, "t_bytes") {
re := regexp.MustCompile(`bytes(\d+)`)
matches := re.FindStringSubmatch(variableType)
if len(matches) > 1 {
info.length, _ = strconv.Atoi(matches[1])
return strconv.Atoi(matches[1])
}
} else if strings.HasPrefix(variableType, "t_address") {
info.length = 20
return 20, nil
} else if strings.HasPrefix(variableType, "t_bool") {
info.length = 1
return 1, nil
} else if strings.HasPrefix(variableType, "t_array") {
re := regexp.MustCompile(`^t_array\((\w+)\)(\d+)`)
matches := re.FindStringSubmatch(variableType)
if len(matches) > 2 {
innerType := matches[1]
size, _ := strconv.Atoi(matches[2])
innerInfo, err := parseVariableInfo(map[string]interface{}{
"label": variable["label"],
"offset": variable["offset"],
"slot": variable["slot"],
"type": innerType,
})
length, err := parseVariableLength(innerType, types)
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() {
err := filepath.Walk(directoryPath, func(path string, info os.FileInfo, err error) 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
}
func validateSpacer(variable solc.StorageLayoutEntry, types map[string]solc.StorageLayoutType) []error {
var errors []error
storage, ok := storageLayout["storage"].([]interface{})
if !ok {
return nil
}
parts := strings.Split(variable.Label, "_")
if len(parts) != 4 {
return []error{fmt.Errorf("invalid spacer name format: %s", variable.Label)}
}
for _, v := range storage {
variable := v.(map[string]interface{})
fqn := variable["contract"].(string)
expectedSlot, _ := strconv.Atoi(parts[1])
expectedOffset, _ := strconv.Atoi(parts[2])
expectedLength, _ := strconv.Atoi(parts[3])
if skipped(fqn) {
continue
}
actualLength, err := parseVariableLength(variable.Type, types)
if err != nil {
return []error{err}
}
label := variable["label"].(string)
if strings.HasPrefix(label, "spacer_") {
parts := strings.Split(label, "_")
if len(parts) != 4 {
return fmt.Errorf("invalid spacer name format: %s", label)
}
if int(variable.Slot) != expectedSlot {
errors = append(errors, fmt.Errorf("%s %s is in slot %d but should be in %d",
variable.Contract, variable.Label, variable.Slot, expectedSlot))
}
slot, _ := strconv.Atoi(parts[1])
offset, _ := strconv.Atoi(parts[2])
length, _ := strconv.Atoi(parts[3])
if int(variable.Offset) != expectedOffset {
errors = append(errors, fmt.Errorf("%s %s is at offset %d but should be at %d",
variable.Contract, variable.Label, variable.Offset, expectedOffset))
}
variableInfo, err := parseVariableInfo(variable)
if err != nil {
return err
}
if actualLength != expectedLength {
errors = append(errors, fmt.Errorf("%s %s is %d bytes long but should be %d",
variable.Contract, variable.Label, actualLength, expectedLength))
}
if slot != variableInfo.slot {
return fmt.Errorf("%s %s is in slot %d but should be in %d", fqn, label, variableInfo.slot, slot)
}
return errors
}
if offset != variableInfo.offset {
return fmt.Errorf("%s %s is at offset %d but should be at %d", fqn, label, variableInfo.offset, offset)
}
func processFile(path string) []error {
artifact, err := common.ReadForgeArtifact(path)
if err != nil {
return []error{err}
}
if length != variableInfo.length {
return fmt.Errorf("%s %s is %d bytes long but should be %d", fqn, label, variableInfo.length, length)
}
if artifact.StorageLayout == nil {
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)
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