Skip to content
Open
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
5 changes: 5 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,10 @@ workflows:
- run-tool:
name: check-chainlist
tool: check_chainlist
- run-tool:
name: check-addresses
tool: check_addresses
args: "-all"
check_diff: true
- run-staging-report:
name: run-staging-report
49 changes: 49 additions & 0 deletions ops/cmd/check_addresses/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Address Checksum Validator

This tool helps ensure that all Ethereum addresses in TOML files are properly checksummed.

## What's an Ethereum Checksum Address?

Ethereum addresses use a mixed-case checksum format (EIP-55) to help detect input errors. The correct format has both uppercase and lowercase letters, and the specific case pattern serves as a checksum. For example:
- `0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed` (correct)
- `0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed` (incorrect, all lowercase)

Using checksummed addresses helps prevent errors when importing them from Solidity or other systems.

## Usage

```
go run ./cmd/check_addresses/main.go [options]
```

### Options

- `-all`: Check all TOML files in the validation directory (default: only checks standard-versions-sepolia.toml)
- `-fix`: Fix incorrect addresses (default: only reports incorrect addresses)
- `-file=<path>`: Check a specific file (default: checks standard-versions-sepolia.toml)

### Examples

Check the default file:
```
go run ./cmd/check_addresses/main.go
```

Check all TOML files:
```
go run ./cmd/check_addresses/main.go -all
```

Check a specific file:
```
go run ./cmd/check_addresses/main.go -file=path/to/file.toml
```

Fix addresses in all TOML files:
```
go run ./cmd/check_addresses/main.go -all -fix
```

## CI Integration

This tool is integrated into the CI pipeline and will fail builds if it detects incorrectly checksummed addresses.
163 changes: 163 additions & 0 deletions ops/cmd/check_addresses/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/ethereum/go-ethereum/common"
)

// addressRegex matches Ethereum addresses in the format 0x...
var addressRegex = regexp.MustCompile(`0x[0-9a-fA-F]{40}`)

// ContractData represents the structure of contract entries in the TOML file
type ContractData struct {
Version string `toml:"version"`
Address string `toml:"address,omitempty"`
ImplementationAddress string `toml:"implementation_address,omitempty"`
}

// ReleaseData maps contract names to their ContractData
type ReleaseData map[string]ContractData

// VersionsData maps release versions to their respective ReleaseData
type VersionsData map[string]ReleaseData

func main() {
// Parse command line arguments
var (
fixFlag = flag.Bool("fix", false, "Fix incorrect addresses")
allFlag = flag.Bool("all", false, "Check all TOML files in validation directory")
fileFlag = flag.String("file", "", "Check specific file")
ciMode = os.Getenv("CI") != "" // Detect if running in CI environment
)
flag.Parse()

// In CI mode with check_diff, we should fix files
if ciMode && !*fixFlag {
fmt.Println("Running in CI mode with check_diff, enabling auto-fix")
*fixFlag = true
}

// Get the root directory of the repository
rootDir, err := findRepoRoot()
if err != nil {
log.Fatalf("Error finding repository root: %v", err)
}

// Determine which files to check
var filesToCheck []string
if *allFlag {
err := filepath.Walk(filepath.Join(rootDir, "validation"), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(path, ".toml") {
filesToCheck = append(filesToCheck, path)
}
return nil
})
if err != nil {
log.Fatalf("Error walking validation directory: %v", err)
}
} else if *fileFlag != "" {
filesToCheck = append(filesToCheck, *fileFlag)
} else {
// Default file
filesToCheck = append(filesToCheck, filepath.Join(rootDir, "validation", "standard", "standard-versions-sepolia.toml"))
}

// Check each file
totalIncorrect := 0
for _, file := range filesToCheck {
incorrectCount := checkFile(file, *fixFlag)
totalIncorrect += incorrectCount
}

// Exit with error code if incorrect addresses were found and not fixed
if totalIncorrect > 0 && !*fixFlag {
os.Exit(1)
}
}

// checkFile checks a single file for incorrectly checksummed addresses
func checkFile(tomlFile string, fix bool) int {
// Read the TOML file content
content, err := os.ReadFile(tomlFile)
if err != nil {
log.Fatalf("Error reading TOML file %s: %v", tomlFile, err)
}

// Original content as string
originalContent := string(content)

// Use regex to find all addresses
addresses := addressRegex.FindAllString(originalContent, -1)

// Map to keep track of incorrect addresses and their checksummed versions
incorrectAddresses := make(map[string]string)

// Check each address for correct checksum
for _, addr := range addresses {
checksumAddr := common.HexToAddress(addr).Hex()
if addr != checksumAddr {
incorrectAddresses[addr] = checksumAddr
fmt.Printf("File %s: Incorrect address: %s should be %s\n", tomlFile, addr, checksumAddr)
}
}

// If no incorrect addresses found, we're done
if len(incorrectAddresses) == 0 {
fmt.Printf("File %s: All addresses are correctly checksummed!\n", tomlFile)
return 0
}

fmt.Printf("File %s: Found %d incorrectly checksummed addresses\n", tomlFile, len(incorrectAddresses))

// Fix incorrect addresses if requested
if fix {
// Fix incorrect addresses in the content
correctedContent := originalContent
for incorrect, correct := range incorrectAddresses {
correctedContent = strings.ReplaceAll(correctedContent, incorrect, correct)
}

// Write corrected content back to file
err = os.WriteFile(tomlFile, []byte(correctedContent), 0644)
if err != nil {
log.Fatalf("Error writing corrected content to %s: %v", tomlFile, err)
}

fmt.Printf("File %s: Fixed %d addresses\n", tomlFile, len(incorrectAddresses))
}

return len(incorrectAddresses)
}

// findRepoRoot tries to find the repository root by looking for .repo-root file
func findRepoRoot() (string, error) {
// Start from the current directory
dir, err := os.Getwd()
if err != nil {
return "", err
}

// First try: check if we're already in the root (has .repo-root file)
if _, err := os.Stat(filepath.Join(dir, ".repo-root")); err == nil {
return dir, nil
}

// Second try: go up one level (we might be in the ops directory)
parentDir := filepath.Dir(dir)
if _, err := os.Stat(filepath.Join(parentDir, ".repo-root")); err == nil {
return parentDir, nil
}

// If both fail, return the best guess (parent directory)
return parentDir, nil
}
145 changes: 145 additions & 0 deletions ops/cmd/check_addresses/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package main

import (
"os"
"path/filepath"
"testing"

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

func TestAddressRegex(t *testing.T) {
// Test that the regex matches Ethereum addresses correctly
validAddresses := []string{
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed",
"0x0000000000000000000000000000000000000000",
"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
}

for _, addr := range validAddresses {
match := addressRegex.MatchString(addr)
assert.True(t, match, "Address should match: %s", addr)
}

invalidAddresses := []string{
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeA", // Too short
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAedFF", // Too long
"5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", // Missing 0x prefix
"0xGAAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", // Invalid character
}

for _, addr := range invalidAddresses {
match := addressRegex.MatchString(addr)
assert.False(t, match, "Address should not match: %s", addr)
}
}

func TestFindRepoRoot(t *testing.T) {
// Create a temporary test directory structure
tmpDir, err := os.MkdirTemp("", "check_addresses_test")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)

// Create a .repo-root file in the temp directory
repoRootFile := filepath.Join(tmpDir, ".repo-root")
err = os.WriteFile(repoRootFile, []byte("test repo root"), 0644)
assert.NoError(t, err)

// Create a subdirectory
subDir := filepath.Join(tmpDir, "ops")
err = os.Mkdir(subDir, 0755)
assert.NoError(t, err)

// Test findRepoRoot from the tmpDir
oldWd, err := os.Getwd()
assert.NoError(t, err)
defer os.Chdir(oldWd)

err = os.Chdir(tmpDir)
assert.NoError(t, err)

root, err := findRepoRoot()
assert.NoError(t, err)
assert.Equal(t, tmpDir, root)

// Test findRepoRoot from the subDir
err = os.Chdir(subDir)
assert.NoError(t, err)

root, err = findRepoRoot()
assert.NoError(t, err)
assert.Equal(t, tmpDir, root)
}

func TestCheckFile(t *testing.T) {
// Create a temporary copy of our test file
tmpDir, err := os.MkdirTemp("", "check_addresses_test")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)

// Ensure testdata directory exists
testdataDir := filepath.Join("testdata")
if _, err := os.Stat(testdataDir); os.IsNotExist(err) {
err = os.Mkdir(testdataDir, 0755)
assert.NoError(t, err)
}

// Define paths
srcFilePath := filepath.Join("testdata", "test_addresses.toml")
tmpFilePath := filepath.Join(tmpDir, "test_addresses.toml")

// Create the test file if it doesn't exist
if _, err := os.Stat(srcFilePath); os.IsNotExist(err) {
testContent := `# Test file for address validation

# Incorrect addresses (all lowercase)
[version1.contracts.contract1]
version = "1.0.0"
address = "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"

[version1.contracts.contract2]
version = "1.0.0"
implementation_address = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

# Correct addresses (checksummed)
[version2.contracts.contract1]
version = "2.0.0"
address = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"

[version2.contracts.contract2]
version = "2.0.0"
implementation_address = "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa"`
err = os.WriteFile(srcFilePath, []byte(testContent), 0644)
assert.NoError(t, err)
}

// Copy test file to temp directory
content, err := os.ReadFile(srcFilePath)
assert.NoError(t, err)
err = os.WriteFile(tmpFilePath, content, 0644)
assert.NoError(t, err)

// Test checking without fixing
incorrectCount := checkFile(tmpFilePath, false)
assert.Equal(t, 2, incorrectCount, "Should find two incorrect addresses")

// Test checking with fixing
incorrectCount = checkFile(tmpFilePath, true)
assert.Equal(t, 2, incorrectCount, "Should find and fix two incorrect addresses")

// Verify fix was applied
incorrectCount = checkFile(tmpFilePath, false)
assert.Equal(t, 0, incorrectCount, "Should find no incorrect addresses after fixing")

// Read the fixed file and verify the content
fixedContent, err := os.ReadFile(tmpFilePath)
assert.NoError(t, err)
fixedContentStr := string(fixedContent)

// Check if the specific addresses were fixed
assert.Contains(t, fixedContentStr, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
assert.NotContains(t, fixedContentStr, "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
assert.Contains(t, fixedContentStr, "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa")
assert.NotContains(t, fixedContentStr, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
}
19 changes: 19 additions & 0 deletions ops/cmd/check_addresses/testdata/test_addresses.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Test file for address validation

# Incorrect addresses (all lowercase)
[version1.contracts.contract1]
version = "1.0.0"
address = "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"

[version1.contracts.contract2]
version = "1.0.0"
implementation_address = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

# Correct addresses (checksummed)
[version2.contracts.contract1]
version = "2.0.0"
address = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"

[version2.contracts.contract2]
version = "2.0.0"
implementation_address = "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa"