Skip to content
Merged
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
11 changes: 11 additions & 0 deletions src/analysis/stellar/inheritance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Soroban Contract Inheritance Analysis Module
*/

export { StellarInheritanceAnalyzer } from "./inheritance-analyzer";
export type {
InheritanceAnalysis,
InheritanceHierarchy,
TraitDefinition,
TraitImplementation,
} from "./types";
182 changes: 182 additions & 0 deletions src/analysis/stellar/inheritance/inheritance-analyzer.spec.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
164 changes: 164 additions & 0 deletions src/analysis/stellar/inheritance/inheritance-analyzer.ts
Original file line number Diff line number Diff line change
@@ -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<string>()): 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}.`;
}
}
32 changes: 32 additions & 0 deletions src/analysis/stellar/inheritance/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading