Skip to content

feat: new vaults#65

Draft
tamtamchik wants to merge 12 commits intomainfrom
feat/new-vault
Draft

feat: new vaults#65
tamtamchik wants to merge 12 commits intomainfrom
feat/new-vault

Conversation

@tamtamchik
Copy link
Member

No description provided.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-missing flag 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
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +50
const errorMessage = `Expected "${expectedValue}" but got "${actual}"`;
logHandle.failure(errorMessage);
incErrors(errorMessage);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 40 to 43
// 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");
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Expected is longer than 42 chars but still needs padding (e.g., partial values)
  2. Expected is a short numeric value that needs left-padding with zeros
  3. 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.

Copilot uses AI. Check for mistakes.
override async validateSection(contractEntry: ContractEntry, contractAlias: string) {
void contractAlias; // Used for interface compatibility
const { address } = contractEntry;
const storage = (contractEntry as { storage?: StorageCheckEntry[] }).storage;
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
await this.map.get(EntryField.checks)!.validateSection(contractEntry, contractAlias);
}

if (needCheck(CheckLevel.checksType, EntryField.storage)) {
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (needCheck(CheckLevel.checksType, EntryField.storage)) {
if (needCheck(CheckLevel.checksType, EntryField.storage)) {
logHeader2(EntryField.storage);

Copilot uses AI. Check for mistakes.
Comment on lines +174 to +181
// 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));
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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}`);
});

Copilot uses AI. Check for mistakes.
Comment on lines +177 to +181
// 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));
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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;

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 27
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;
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}.

Suggested change
logSubHeader(`Role:${role}`, isLastRole);
logSubHeader(`Role: ${role}`, isLastRole);

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +32
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;
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant