Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,8 @@ jobs:
command: size-check
- run-contracts-check:
command: unused-imports-check-no-build
- run-contracts-check:
command: strict-pragma-check-no-build
- run-contracts-check:
command: validate-spacers-no-build
- run-contracts-check:
Expand Down
8 changes: 8 additions & 0 deletions packages/contracts-bedrock/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,13 @@ unused-imports-check-no-build:
# Checks for unused imports in Solidity contracts.
unused-imports-check: build unused-imports-check-no-build

# Checks that contracts use strict pragma versions. Does not build contracts.
strict-pragma-check-no-build:
go run ./scripts/checks/strict-pragma

# Checks that contracts use strict pragma versions.
strict-pragma-check: build strict-pragma-check-no-build

# Checks that the semver of contracts are valid. Does not build contracts.
valid-semver-check-no-build:
go run ./scripts/checks/valid-semver-check/main.go
Expand Down Expand Up @@ -332,6 +339,7 @@ check:
lint-check \
snapshots-check-no-build \
unused-imports-check-no-build \
strict-pragma-check-no-build \
valid-semver-check-no-build \
semver-diff-check-no-build \
validate-deploy-configs \
Expand Down
184 changes: 184 additions & 0 deletions packages/contracts-bedrock/scripts/checks/strict-pragma/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package main

import (
"bufio"
"fmt"
"os"
"regexp"
"strings"

"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/common"
)

// Patterns to detect contract types and pragma
var (
// Matches "pragma solidity X.Y.Z;" (strict) vs "pragma solidity ^X.Y.Z;" or ">=X.Y.Z" (non-strict)
pragmaPattern = regexp.MustCompile(`pragma\s+solidity\s+([^;]+);`)

// Matches "contract Name" but not "abstract contract Name"
// Uses \s* to allow indentation at start of line
contractPattern = regexp.MustCompile(`(?m)^\s*contract\s+\w+`)

// Matches "abstract contract Name"
abstractPattern = regexp.MustCompile(`(?m)^\s*abstract\s+contract\s+\w+`)

// Matches "library Name"
libraryPattern = regexp.MustCompile(`(?m)^\s*library\s+\w+`)

// Matches "interface Name"
interfacePattern = regexp.MustCompile(`(?m)^\s*interface\s+\w+`)
)

// Files that are grandfathered in (already have non-strict pragma)
// These should be fixed over time, but we don't want to block CI on them
var excludedFiles = []string{
"src/integration/EventLogger.sol",
"src/integration/GameHelper.sol",
"src/libraries/TransientContext.sol",
"src/periphery/AssetReceiver.sol",
"src/periphery/Transactor.sol",
"src/periphery/monitoring/DisputeMonitorHelper.sol",
"src/universal/SafeSend.sol",
}

func main() {
if _, err := common.ProcessFilesGlob(
[]string{"src/**/*.sol"},
excludedFiles,
processFile,
); err != nil {
fmt.Printf("error: %v\n", err)
os.Exit(1)
}
}

func processFile(filePath string) (*common.Void, []error) {
content, err := os.ReadFile(filePath)
if err != nil {
return nil, []error{fmt.Errorf("failed to read file: %w", err)}
}

contentStr := string(content)

// Check if file contains a concrete contract (not abstract, not library, not interface)
if !hasConcreteContract(contentStr) {
return nil, nil
}

// Check if pragma is strict
pragma := extractPragma(contentStr)
if pragma == "" {
return nil, []error{fmt.Errorf("no pragma found")}
}

if !isStrictPragma(pragma) {
return nil, []error{fmt.Errorf("non-strict pragma '%s' - contracts must use exact version (e.g., '0.8.15' not '^0.8.15')", pragma)}
}

return nil, nil
}

// hasConcreteContract returns true if the file contains at least one concrete contract
// (not abstract, not library, not interface)
func hasConcreteContract(content string) bool {
// Remove comments to avoid false positives
content = removeComments(content)

// Check for concrete contract definition
hasContract := contractPattern.MatchString(content)
if !hasContract {
return false
}

// Make sure it's not just abstract contracts, libraries, or interfaces
// by checking if we have a "contract X" that isn't preceded by "abstract"
lines := strings.Split(content, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
// Skip if it's an abstract contract, library, or interface
if abstractPattern.MatchString(trimmed) ||
libraryPattern.MatchString(trimmed) ||
interfacePattern.MatchString(trimmed) {
continue
}
// Check for concrete contract
if contractPattern.MatchString(trimmed) {
return true
}
}

return false
}

// extractPragma extracts the pragma version string from the content
func extractPragma(content string) string {
matches := pragmaPattern.FindStringSubmatch(content)
if len(matches) > 1 {
return strings.TrimSpace(matches[1])
}
return ""
}

// isStrictPragma returns true if the pragma is a strict version (no ^ or >= or other operators)
func isStrictPragma(pragma string) bool {
// Strict pragma should be just a version number like "0.8.15"
// Non-strict examples: "^0.8.0", ">=0.8.0", ">=0.8.0 <0.9.0", "0.8.x"

// Check for common non-strict indicators
nonStrictIndicators := []string{"^", ">=", "<=", ">", "<", "~", "x", "X", "*", " "}
for _, indicator := range nonStrictIndicators {
if strings.Contains(pragma, indicator) {
return false
}
}

// Should match a simple version pattern like "0.8.15"
strictPattern := regexp.MustCompile(`^\d+\.\d+\.\d+$`)
return strictPattern.MatchString(pragma)
}

// removeComments removes single-line and multi-line comments from Solidity code
func removeComments(content string) string {
var result strings.Builder
scanner := bufio.NewScanner(strings.NewReader(content))
inMultiLineComment := false

for scanner.Scan() {
line := scanner.Text()

// Handle multi-line comments
if inMultiLineComment {
if idx := strings.Index(line, "*/"); idx != -1 {
line = line[idx+2:]
inMultiLineComment = false
} else {
continue
}
}

// Remove multi-line comment starts
for {
startIdx := strings.Index(line, "/*")
if startIdx == -1 {
break
}
endIdx := strings.Index(line[startIdx:], "*/")
if endIdx == -1 {
line = line[:startIdx]
inMultiLineComment = true
break
}
line = line[:startIdx] + line[startIdx+endIdx+2:]
}

// Remove single-line comments
if idx := strings.Index(line, "//"); idx != -1 {
line = line[:idx]
}

result.WriteString(line)
result.WriteString("\n")
}

return result.String()
}
Loading