From afd77e594aebb221cbe5cb9191b2c45139716310 Mon Sep 17 00:00:00 2001 From: Tyler Date: Sun, 28 Jun 2026 00:55:21 +0000 Subject: [PATCH] Add Soroban storage growth analyzer --- src/analysis/stellar/storage-growth/index.ts | 10 ++ .../storage-growth-analyzer.spec.ts | 56 +++++++++ .../storage-growth/storage-growth-analyzer.ts | 118 ++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 src/analysis/stellar/storage-growth/index.ts create mode 100644 src/analysis/stellar/storage-growth/storage-growth-analyzer.spec.ts create mode 100644 src/analysis/stellar/storage-growth/storage-growth-analyzer.ts diff --git a/src/analysis/stellar/storage-growth/index.ts b/src/analysis/stellar/storage-growth/index.ts new file mode 100644 index 0000000..45d4a0c --- /dev/null +++ b/src/analysis/stellar/storage-growth/index.ts @@ -0,0 +1,10 @@ +/** + * Soroban Storage Growth Analysis Module + */ + +export { StellarStorageGrowthAnalyzer } from './storage-growth-analyzer'; +export type { + StorageGrowthPattern, + StorageGrowthWarning, + StorageGrowthReport, +} from './storage-growth-analyzer'; diff --git a/src/analysis/stellar/storage-growth/storage-growth-analyzer.spec.ts b/src/analysis/stellar/storage-growth/storage-growth-analyzer.spec.ts new file mode 100644 index 0000000..1581562 --- /dev/null +++ b/src/analysis/stellar/storage-growth/storage-growth-analyzer.spec.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from '@jest/globals'; +import { StellarStorageGrowthAnalyzer } from './storage-growth-analyzer'; + +describe('StellarStorageGrowthAnalyzer', () => { + it('detects growth-prone storage operations and emits warnings', () => { + const source = ` +use soroban_sdk::{contract, contractimpl, Env, Symbol, Vec}; + +#[contract] +pub struct GrowthContract; + +#[contractimpl] +impl GrowthContract { + pub fn add_item(env: Env, item: Symbol) { + let mut items: Vec = env.storage().instance().get(&Symbol::new(&env, "items")).unwrap_or_default(); + items.push_back(item); + env.storage().instance().set(&Symbol::new(&env, "items"), &items); + } + + pub fn store_many(env: Env, values: Vec) { + env.storage().instance().set(&Symbol::new(&env, "values"), &values); + } +} +`; + + const analyzer = new StellarStorageGrowthAnalyzer(source, 'growth.rs'); + const report = analyzer.analyze(); + + expect(report.contractName).toBe('GrowthContract'); + expect(report.growthPatterns.length).toBeGreaterThan(0); + expect(report.warnings.length).toBeGreaterThan(0); + expect(report.summary).toContain('growth'); + }); + + it('returns no warnings for bounded storage patterns', () => { + const source = ` +use soroban_sdk::{contract, contractimpl, Env, Symbol}; + +#[contract] +pub struct BoundedContract; + +#[contractimpl] +impl BoundedContract { + pub fn set_limit(env: Env) { + env.storage().instance().set(&Symbol::new(&env, "limit"), &10u32); + } +} +`; + + const analyzer = new StellarStorageGrowthAnalyzer(source, 'bounded.rs'); + const report = analyzer.analyze(); + + expect(report.growthPatterns).toHaveLength(0); + expect(report.warnings).toHaveLength(0); + }); +}); diff --git a/src/analysis/stellar/storage-growth/storage-growth-analyzer.ts b/src/analysis/stellar/storage-growth/storage-growth-analyzer.ts new file mode 100644 index 0000000..d1bdb71 --- /dev/null +++ b/src/analysis/stellar/storage-growth/storage-growth-analyzer.ts @@ -0,0 +1,118 @@ +export interface StorageGrowthPattern { + functionName: string; + lineNumber: number; + description: string; + riskLevel: 'medium' | 'high'; +} + +export interface StorageGrowthWarning { + message: string; + lineNumber: number; + severity: 'medium' | 'high'; +} + +export interface StorageGrowthReport { + contractName: string; + growthPatterns: StorageGrowthPattern[]; + warnings: StorageGrowthWarning[]; + summary: string; +} + +export class StellarStorageGrowthAnalyzer { + private source: string; + private filePath: string; + + constructor(source: string, filePath: string) { + this.source = source; + this.filePath = filePath; + } + + analyze(): StorageGrowthReport { + const contractName = this.extractContractName(); + const growthPatterns = this.detectGrowthPatterns(); + const warnings = this.buildWarnings(growthPatterns); + + return { + contractName, + growthPatterns, + warnings, + summary: this.buildSummary(contractName, growthPatterns, warnings), + }; + } + + private extractContractName(): string { + const match = this.source.match(/pub struct (\w+)/); + return match ? match[1] : 'UnknownContract'; + } + + private detectGrowthPatterns(): StorageGrowthPattern[] { + const patterns: StorageGrowthPattern[] = []; + const functionRegex = /fn\s+(\w+)\s*\(/g; + let match: RegExpExecArray | null; + + while ((match = functionRegex.exec(this.source)) !== null) { + const functionName = match[1]; + const body = this.extractFunctionBody(functionName); + const lineNumber = this.getLineNumber(match.index); + + const hasStorageMutation = /storage\(\)\.(instance|persistent|temporary)\(\)\.set/.test(body); + const hasGrowthMutation = /push_back|push\(|insert\(|extend\(|append\(|Vec<|Map<|vec!/.test(body); + const isGrowthPattern = hasStorageMutation && hasGrowthMutation; + + if (isGrowthPattern) { + patterns.push({ + functionName, + lineNumber, + description: 'Function appears to append or persist data in a way that may cause storage growth over time.', + riskLevel: body.includes('push_back') || body.includes('push(') ? 'high' : 'medium', + }); + } + } + + return patterns; + } + + private buildWarnings(patterns: StorageGrowthPattern[]): StorageGrowthWarning[] { + return patterns.map(pattern => ({ + message: `Storage growth risk detected in function '${pattern.functionName}': ${pattern.description}`, + lineNumber: pattern.lineNumber, + severity: pattern.riskLevel, + })); + } + + private buildSummary( + contractName: string, + patterns: StorageGrowthPattern[], + warnings: StorageGrowthWarning[], + ): string { + return `${contractName} storage growth analysis: ${patterns.length} growth pattern(s) detected, ${warnings.length} warning(s) generated.`; + } + + private extractFunctionBody(functionName: string): string { + const fnPattern = new RegExp(`fn\\s+${functionName}\\s*\\(`); + const match = this.source.match(fnPattern); + if (!match) return ''; + + const start = match.index ?? 0; + const bodyStart = this.source.indexOf('{', start); + if (bodyStart === -1) return ''; + + let depth = 1; + let i = bodyStart + 1; + let body = ''; + + while (i < this.source.length && depth > 0) { + const ch = this.source[i]; + if (ch === '{') depth++; + else if (ch === '}') depth--; + if (depth > 0) body += ch; + i++; + } + + return body; + } + + private getLineNumber(offset: number): number { + return (this.source.slice(0, offset).match(/\n/g) ?? []).length + 1; + } +}