From 9c4bfe2a5955bfb0dd36bf6554759284f692aed7 Mon Sep 17 00:00:00 2001 From: Annakaee Date: Fri, 26 Jun 2026 04:02:31 +0100 Subject: [PATCH] feat: implement Soroban Contract Inheritance Analyzer (#485) --- src/analysis/stellar/inheritance/index.ts | 11 ++ .../inheritance/inheritance-analyzer.spec.ts | 182 ++++++++++++++++++ .../inheritance/inheritance-analyzer.ts | 164 ++++++++++++++++ src/analysis/stellar/inheritance/types.ts | 32 +++ 4 files changed, 389 insertions(+) create mode 100644 src/analysis/stellar/inheritance/index.ts create mode 100644 src/analysis/stellar/inheritance/inheritance-analyzer.spec.ts create mode 100644 src/analysis/stellar/inheritance/inheritance-analyzer.ts create mode 100644 src/analysis/stellar/inheritance/types.ts diff --git a/src/analysis/stellar/inheritance/index.ts b/src/analysis/stellar/inheritance/index.ts new file mode 100644 index 0000000..b622978 --- /dev/null +++ b/src/analysis/stellar/inheritance/index.ts @@ -0,0 +1,11 @@ +/** + * Soroban Contract Inheritance Analysis Module + */ + +export { StellarInheritanceAnalyzer } from "./inheritance-analyzer"; +export type { + InheritanceAnalysis, + InheritanceHierarchy, + TraitDefinition, + TraitImplementation, +} from "./types"; \ No newline at end of file diff --git a/src/analysis/stellar/inheritance/inheritance-analyzer.spec.ts b/src/analysis/stellar/inheritance/inheritance-analyzer.spec.ts new file mode 100644 index 0000000..7147b9c --- /dev/null +++ b/src/analysis/stellar/inheritance/inheritance-analyzer.spec.ts @@ -0,0 +1,182 @@ +/** + * Tests for Soroban Contract Ownership Analyzer + */ + +import { describe, it, expect } from "@jest/globals"; +import { StellarOwnershipAnalyzer } from "./ownership-analyzer"; + +describe("StellarOwnershipAnalyzer", () => { + const singleOwnerContract = ` +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; + +#[contracttype] +pub struct TokenContract { + pub owner: Address, + pub total_supply: u64, +} + +#[contractimpl] +impl TokenContract { + pub fn new(owner: Address) -> Self { + Self { owner, total_supply: 0 } + } + + pub fn transfer(&mut self, to: Address, amount: u64) -> Result<(), Error> { + if self.owner != to { + return Err(Error::Unauthorized); + } + Ok(()) + } + + pub fn set_owner(&mut self, new_owner: Address) { + self.owner.require_auth(); + self.owner = new_owner; + } + + pub fn pause(&mut self, caller: Address) -> Result<(), Error> { + if caller != self.owner { + return Err(Error::Unauthorized); + } + Ok(()) + } +} + +pub enum Error { + Unauthorized, +} +`; + + const roleBasedContract = ` +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Map}; + +#[contracttype] +pub struct Roles { + pub admin: Address, + pub minter: Address, + pub pauser: Address, +} + +#[contractimpl] +impl RoleContract { + pub fn new(admin: Address) -> Self { + Self { admin } + } + + pub fn grant_role(&mut self, role: String, user: Address) { + self.admin.require_auth(); + } + + pub fn mint(&mut self, to: Address, amount: u64) { + self.minter.require_auth(); + } + + pub fn pause(&mut self) { + self.pauser.require_auth(); + } +} +`; + + const timeLockedContract = ` +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; + +#[contracttype] +pub struct TimelockContract { + pub owner: Address, + pub delay: u64, +} + +#[contractimpl] +impl TimelockContract { + pub fn new(owner: Address) -> Self { + Self { owner, delay: 86400 } + } + + pub fn schedule_upgrade(&mut self, env: Env) { + self.owner.require_auth(); + let deadline = env.ledger().timestamp() + self.delay; + } +} +`; + + it("should detect single-owner pattern", () => { + const analyzer = new StellarOwnershipAnalyzer( + singleOwnerContract, + "test.rs", + ); + const analysis = analyzer.analyze(); + + expect(analysis.detectedPattern).toBe("single-owner"); + expect(analysis.contractName).toBe("TokenContract"); + }); + + it("should find admin functions", () => { + const analyzer = new StellarOwnershipAnalyzer( + singleOwnerContract, + "test.rs", + ); + const analysis = analyzer.analyze(); + + expect(analysis.adminFunctions.length).toBeGreaterThan(0); + const adminNames = analysis.adminFunctions.map((f) => f.name); + expect(adminNames).toContain("set_owner"); + expect(adminNames).toContain("pause"); + }); + + it("should detect owner checks", () => { + const analyzer = new StellarOwnershipAnalyzer( + singleOwnerContract, + "test.rs", + ); + const analysis = analyzer.analyze(); + + expect(analysis.ownerChecks.length).toBeGreaterThan(0); + for (const check of analysis.ownerChecks) { + expect(check.checkExpression).toBeTruthy(); + } + }); + + it("should detect role-based ownership", () => { + const analyzer = new StellarOwnershipAnalyzer( + roleBasedContract, + "test.rs", + ); + const analysis = analyzer.analyze(); + + expect(analysis.detectedPattern).toBe("role-based"); + expect(analysis.roleAssignments.length).toBeGreaterThan(0); + }); + + it("should detect time-locked ownership", () => { + const analyzer = new StellarOwnershipAnalyzer( + timeLockedContract, + "test.rs", + ); + const analysis = analyzer.analyze(); + + expect(analysis.detectedPattern).toBe("time-locked"); + expect(analysis.timeLocks.length).toBeGreaterThan(0); + }); + + it("should calculate risk level correctly", () => { + const analyzer = new StellarOwnershipAnalyzer( + singleOwnerContract, + "test.rs", + ); + const analysis = analyzer.analyze(); + + expect(["low", "medium", "high", "critical"]).toContain( + analysis.riskLevel, + ); + }); + + it("should generate a summary", () => { + const analyzer = new StellarOwnershipAnalyzer( + singleOwnerContract, + "test.rs", + ); + const analysis = analyzer.analyze(); + + expect(analysis.summary).toContain("single-owner"); + expect(analysis.summary).toContain("TokenContract"); + }); +}); diff --git a/src/analysis/stellar/inheritance/inheritance-analyzer.ts b/src/analysis/stellar/inheritance/inheritance-analyzer.ts new file mode 100644 index 0000000..d3e54f1 --- /dev/null +++ b/src/analysis/stellar/inheritance/inheritance-analyzer.ts @@ -0,0 +1,164 @@ +/** + * Soroban Contract Inheritance Analyzer + * + * Analyzes trait definitions, implementations, and hierarchies in Soroban contracts. + * Detects deep inheritance that can impact gas usage and auditability. + */ + +import { InheritanceAnalysis, InheritanceHierarchy, TraitDefinition, TraitImplementation } from './types'; + +export class StellarInheritanceAnalyzer { + private source: string; + private filePath: string; + + constructor(source: string, filePath: string) { + this.source = source; + this.filePath = filePath; + } + + analyze(): InheritanceAnalysis { + const contractName = this.extractContractName(); + const traits = this.extractTraitDefinitions(); + const implementations = this.extractImplementations(); + const hierarchies = this.buildHierarchies(traits, implementations); + + const maxDepth = Math.max(0, ...hierarchies.map(h => h.depth)); + const issues = this.identifyIssues(hierarchies); + + return { + contractName, + traitsUsed: [...new Set([...traits.map(t => t.name), ...implementations.map(i => i.traitName)])], + hierarchies, + maxDepth, + totalImplementations: implementations.length, + issues, + summary: this.generateSummary(hierarchies, maxDepth), + }; + } + + private extractContractName(): string { + const match = this.source.match(/pub struct (\w+)/) || this.source.match(/contract\s+(\w+)/); + return match ? match[1] : "UnknownContract"; + } + + private extractTraitDefinitions(): TraitDefinition[] { + const traits: TraitDefinition[] = []; + const traitRegex = /trait\s+(\w+)(?:\s*:\s*([\w\s,]+))?\s*\{/g; + let match; + + while ((match = traitRegex.exec(this.source)) !== null) { + const name = match[1]; + const superTraitsStr = match[2] || ''; + const superTraits = superTraitsStr.split(',').map(s => s.trim()).filter(Boolean); + + const bodyStart = this.source.indexOf('{', match.index) + 1; + const body = this.extractBlock(this.source, bodyStart); + + traits.push({ + name, + methods: this.extractMethods(body), + superTraits, + line: this.getLineNumber(match.index), + }); + } + return traits; + } + + private extractImplementations(): TraitImplementation[] { + const impls: TraitImplementation[] = []; + const implRegex = /impl\s+(?:<[^>]+>\s+)?(\w+)\s+for\s+(\w+)/g; + let match; + + while ((match = implRegex.exec(this.source)) !== null) { + impls.push({ + traitName: match[1], + contractName: match[2], + methodsImplemented: [], + line: this.getLineNumber(match.index), + }); + } + return impls; + } + + private buildHierarchies(traits: TraitDefinition[], implementations: TraitImplementation[]): InheritanceHierarchy[] { + return traits.map(trait => { + const impls = implementations.filter(i => i.traitName === trait.name); + const depth = this.calculateDepth(trait, traits); + + let riskLevel: 'low' | 'medium' | 'high' = 'low'; + let recommendation = "Trait usage looks good."; + + if (depth > 3) { + riskLevel = 'high'; + recommendation = "Deep inheritance detected. Consider using composition to reduce complexity and gas costs."; + } else if (depth > 2) { + riskLevel = 'medium'; + recommendation = "Moderate hierarchy depth. Good for now, but monitor during audits."; + } + + return { + trait: trait.name, + depth, + implementations: impls, + subTraits: trait.superTraits, + riskLevel, + recommendation, + }; + }); + } + + private calculateDepth(trait: TraitDefinition, allTraits: TraitDefinition[], visited = new Set()): number { + if (visited.has(trait.name)) return 1; // cycle + visited.add(trait.name); + + if (trait.superTraits.length === 0) return 1; + + let maxDepth = 1; + for (const superName of trait.superTraits) { + const superTrait = allTraits.find(t => t.name === superName); + if (superTrait) { + maxDepth = Math.max(maxDepth, 1 + this.calculateDepth(superTrait, allTraits, new Set(visited))); + } + } + return maxDepth; + } + + private extractMethods(body: string): string[] { + const methods: string[] = []; + const methodRegex = /fn\s+(\w+)/g; + let match; + while ((match = methodRegex.exec(body)) !== null) { + methods.push(match[1]); + } + return methods; + } + + private extractBlock(code: string, startIndex: number): string { + let braceCount = 1; + let result = ""; + for (let i = startIndex; i < code.length; i++) { + result += code[i]; + if (code[i] === '{') braceCount++; + if (code[i] === '}') { + braceCount--; + if (braceCount === 0) break; + } + } + return result; + } + + private getLineNumber(offset: number): number { + return (this.source.substring(0, offset).match(/\n/g) || []).length + 1; + } + + private identifyIssues(hierarchies: InheritanceHierarchy[]): string[] { + return hierarchies + .filter(h => h.riskLevel === 'high') + .map(h => `Deep trait hierarchy for ${h.trait} (depth: ${h.depth})`); + } + + private generateSummary(hierarchies: InheritanceHierarchy[], maxDepth: number): string { + if (hierarchies.length === 0) return "No traits found in contract."; + return `Analyzed ${hierarchies.length} trait(s). Max inheritance depth: ${maxDepth}.`; + } +} \ No newline at end of file diff --git a/src/analysis/stellar/inheritance/types.ts b/src/analysis/stellar/inheritance/types.ts new file mode 100644 index 0000000..ee58cff --- /dev/null +++ b/src/analysis/stellar/inheritance/types.ts @@ -0,0 +1,32 @@ +export interface TraitDefinition { + name: string; + methods: string[]; + superTraits: string[]; + line: number; +} + +export interface TraitImplementation { + traitName: string; + contractName: string; + methodsImplemented: string[]; + line: number; +} + +export interface InheritanceHierarchy { + trait: string; + depth: number; + implementations: TraitImplementation[]; + subTraits: string[]; + riskLevel: 'low' | 'medium' | 'high'; + recommendation: string; +} + +export interface InheritanceAnalysis { + contractName: string; + traitsUsed: string[]; + hierarchies: InheritanceHierarchy[]; + maxDepth: number; + totalImplementations: number; + issues: string[]; + summary: string; +} \ No newline at end of file