Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds storage slot validation capabilities to state-mate and introduces a new --update-abi-missing CLI option for selective ABI updates. The PR also includes comprehensive configuration for Mellow Flexible Vaults on Ethereum mainnet and extensive documentation for AI agents.
Changes:
- Adds storage slot verification feature allowing validation of raw storage values (useful for proxy admin/implementation checks)
- Introduces
--update-abi-missingflag to download only missing ABIs without overwriting existing ones - Adds Mellow vaults configuration with 1355 lines of contract verification rules
Reviewed changes
Copilot reviewed 14 out of 16 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/typebox.ts | Adds TypeScript type definitions for storage check entries |
| src/state-mate.ts | Integrates ABI existence check to skip downloads when using --update-abi-missing |
| src/section-validators/storage.ts | New validator that reads storage slots and compares with expected values |
| src/section-validators/contract.ts | Integrates storage validator into contract validation flow |
| src/common.ts | Adds storage field to EntryField enum |
| src/cli-parser.ts | Adds --update-abi-missing CLI option |
| src/abi-provider.ts | Implements abiExistsForAddress() and selective ABI update logic |
| configs/meta/eth/mellow-vaults.yml | Comprehensive config for Mellow vault contracts with storage checks |
| configs/meta/eth/mellow-vaults.seed.yml | Seed configuration for generating full config |
| configs/meta/eth/abis.json.gz | Binary ABI file (compressed JSON) |
| README.md | Documents new --update-abi-missing option |
| CLAUDE.md | New file redirecting to AGENTS.md for Claude Code compatibility |
| AGENTS.md | Comprehensive agent instructions for working with state-mate |
| .skills/state-mate/* | Detailed skills and reference documentation |
| .gitignore | Adds .claude directory to ignore list |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| override async validateSection(contractEntry: ContractEntry, contractAlias: string) { | ||
| void contractAlias; // Used for interface compatibility |
There was a problem hiding this comment.
The void statement void contractAlias; is used but should be accompanied by an ESLint disable comment like other validators. See src/section-validators/oz-acl.ts:1 which has /* eslint-disable @typescript-eslint/no-unused-vars */. This would prevent linter warnings about the unused variable.
| const errorMessage = `Expected "${expectedValue}" but got "${actual}"`; | ||
| logHandle.failure(errorMessage); | ||
| incErrors(errorMessage); |
There was a problem hiding this comment.
The error message construction could be clearer. When comparing storage values, displaying both the actual and expected values in a consistent format would help debugging. Consider formatting both as 66-character hex strings (with padding shown) so users can clearly see the difference. Currently, the message shows raw values which may have different formats after the padding logic.
src/section-validators/storage.ts
Outdated
| // If expected is a 20-byte address (42 chars with 0x), pad it to 32 bytes | ||
| if (normalizedExpected.length === 42 && normalizedActual.length === 66) { | ||
| normalizedExpected = "0x" + normalizedExpected.slice(2).padStart(64, "0"); | ||
| } |
There was a problem hiding this comment.
The padding logic only handles the specific case of 20-byte addresses (42 chars with 0x) being padded to 32 bytes (66 chars with 0x). However, other value types may need padding as well. Consider what happens if:
- Expected is longer than 42 chars but still needs padding (e.g., partial values)
- Expected is a short numeric value that needs left-padding with zeros
- Both values need padding in different ways
The current implementation may produce false negatives where values are semantically equal but have different string representations. Consider a more robust normalization that handles all 32-byte values consistently.
src/section-validators/storage.ts
Outdated
| override async validateSection(contractEntry: ContractEntry, contractAlias: string) { | ||
| void contractAlias; // Used for interface compatibility | ||
| const { address } = contractEntry; | ||
| const storage = (contractEntry as { storage?: StorageCheckEntry[] }).storage; |
There was a problem hiding this comment.
The type cast to extract the storage property is potentially unsafe. The code casts contractEntry as { storage?: StorageCheckEntry[] } even though the TypeScript schema already defines this through StorageChecksTB. Consider using the proper type from the schema or checking with isTypeOfTB if a type guard exists for this field. This would provide better type safety and consistency with the rest of the codebase.
| await this.map.get(EntryField.checks)!.validateSection(contractEntry, contractAlias); | ||
| } | ||
|
|
||
| if (needCheck(CheckLevel.checksType, EntryField.storage)) { |
There was a problem hiding this comment.
The storage checks section is executed without printing a header (unlike other sections). While line 81 sets the error context, there's no corresponding logHeader2(EntryField.storage) call before line 82. This makes the output inconsistent - compare with how checks (lines 74-78) and other sections are handled. Users won't see a clear "storage" section header in the validation output.
| if (needCheck(CheckLevel.checksType, EntryField.storage)) { | |
| if (needCheck(CheckLevel.checksType, EntryField.storage)) { | |
| logHeader2(EntryField.storage); |
| // Check if any key contains this address | ||
| return Object.keys(abis).some((key) => key.toLowerCase().includes(address)); | ||
| } else if (mode === "individual") { | ||
| // Check if any file in the abi directory contains this address | ||
| if (!fs.existsSync(g_Arguments.abiDirPath)) return false; | ||
| try { | ||
| const files = fs.readdirSync(g_Arguments.abiDirPath); | ||
| return files.some((file) => file.toLowerCase().includes(address)); |
There was a problem hiding this comment.
The abiExistsForAddress function checks if an address exists in ABI filenames but doesn't validate whether the ABI is actually valid or loadable. In consolidated mode, it checks key.toLowerCase().includes(address) which could produce false positives if an address substring matches part of a contract name. Consider using more precise matching logic, such as checking if the key ends with the address or matches the exact pattern contractName-address.
| // Check if any key contains this address | |
| return Object.keys(abis).some((key) => key.toLowerCase().includes(address)); | |
| } else if (mode === "individual") { | |
| // Check if any file in the abi directory contains this address | |
| if (!fs.existsSync(g_Arguments.abiDirPath)) return false; | |
| try { | |
| const files = fs.readdirSync(g_Arguments.abiDirPath); | |
| return files.some((file) => file.toLowerCase().includes(address)); | |
| // Check if any key matches this address more precisely (ends with "-address" or equals "address") | |
| return Object.keys(abis).some((key) => { | |
| const lowerKey = key.toLowerCase(); | |
| const matchesAddress = lowerKey === address || lowerKey.endsWith(`-${address}`); | |
| return matchesAddress && isValidAbi(abis[key]); | |
| }); | |
| } else if (mode === "individual") { | |
| // Check if any file in the abi directory corresponds to this address | |
| if (!fs.existsSync(g_Arguments.abiDirPath)) return false; | |
| try { | |
| const files = fs.readdirSync(g_Arguments.abiDirPath); | |
| return files.some((file) => { | |
| const baseName = path.basename(file, path.extname(file)).toLowerCase(); | |
| return baseName === address || baseName.endsWith(`-${address}`); | |
| }); |
| // Check if any file in the abi directory contains this address | ||
| if (!fs.existsSync(g_Arguments.abiDirPath)) return false; | ||
| try { | ||
| const files = fs.readdirSync(g_Arguments.abiDirPath); | ||
| return files.some((file) => file.toLowerCase().includes(address)); |
There was a problem hiding this comment.
In individual mode, the function checks if any filename contains the address, but doesn't verify the file is actually loadable or valid JSON. A corrupted or empty file would still be detected as "existing", causing --update-abi-missing to skip it. Consider adding a try-catch that attempts to verify the file is valid, similar to how _checkAbi loads and validates the ABI before determining existence.
| // Check if any file in the abi directory contains this address | |
| if (!fs.existsSync(g_Arguments.abiDirPath)) return false; | |
| try { | |
| const files = fs.readdirSync(g_Arguments.abiDirPath); | |
| return files.some((file) => file.toLowerCase().includes(address)); | |
| // Check if any file in the abi directory contains this address and holds a valid ABI | |
| if (!fs.existsSync(g_Arguments.abiDirPath)) return false; | |
| try { | |
| const files = fs.readdirSync(g_Arguments.abiDirPath); | |
| for (const file of files) { | |
| if (!file.toLowerCase().includes(address)) continue; | |
| const fullPath = path.join(g_Arguments.abiDirPath, file); | |
| try { | |
| const contents = fs.readFileSync(fullPath, "utf8"); | |
| if (!contents.trim()) continue; | |
| const parsed = JSON.parse(contents); | |
| if (isValidAbi(parsed)) { | |
| return true; | |
| } | |
| } catch { | |
| // Ignore files that cannot be read or parsed, or that contain invalid ABI | |
| continue; | |
| } | |
| } | |
| return false; |
src/section-validators/storage.ts
Outdated
| override async validateSection(contractEntry: ContractEntry, contractAlias: string) { | ||
| void contractAlias; // Used for interface compatibility | ||
| const { address } = contractEntry; | ||
| const storage = (contractEntry as { storage?: StorageCheckEntry[] }).storage; | ||
|
|
||
| if (!storage || storage.length === 0) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
The storage validator is missing a logHeader2 call at the beginning of validateSection, which is inconsistent with other validators like OzAclSectionValidator and ProxyCheckSectionValidator. This means storage checks won't have a visual header in the output, making it harder to see when storage validation is being performed.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 23 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (let index = 0; index < roles.length; index++) { | ||
| const [role, expectedAddrs] = roles[index]; | ||
| const isLastRole = index === roles.length - 1; | ||
| logSubHeader(`Role:${role}`, isLastRole); |
There was a problem hiding this comment.
Missing space in the log output. The role name should be separated from "Role:" with a space for better readability. Change Role:${role} to Role: ${role}.
| logSubHeader(`Role:${role}`, isLastRole); | |
| logSubHeader(`Role: ${role}`, isLastRole); |
| function normalizeToBytes32(value: string): string { | ||
| const lower = value.toLowerCase(); | ||
|
|
||
| // If already 66 chars (32 bytes), return as-is | ||
| if (lower.length === 66) { | ||
| return lower; | ||
| } | ||
|
|
||
| // For any hex value shorter than 32 bytes, left-pad with zeros | ||
| if (lower.startsWith("0x") && lower.length < 66) { | ||
| return "0x" + lower.slice(2).padStart(64, "0"); | ||
| } | ||
|
|
||
| // Return as-is for non-hex values or unexpected formats | ||
| return lower; | ||
| } |
There was a problem hiding this comment.
The normalizeToBytes32 function does not validate that the input is a valid hex string before processing. If a non-hex string longer than 66 characters is passed, it will be returned as-is without normalization, which could lead to incorrect comparisons. Consider adding validation to ensure the input starts with "0x" and contains only valid hex characters before processing.
No description provided.