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
6 changes: 6 additions & 0 deletions src/analysis/stellar/lifecycle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Soroban Contract Lifecycle Analysis Module
*/

export { StellarLifecycleAnalyzer } from './lifecycle-analyzer';
export type { InitFlow, UpgradeFlow, LifecycleIssue, LifecycleReport } from './types';
114 changes: 114 additions & 0 deletions src/analysis/stellar/lifecycle/lifecycle-analyzer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Tests for Soroban Contract Lifecycle Analyzer
*/

import { describe, it, expect } from '@jest/globals';
import { StellarLifecycleAnalyzer } from './lifecycle-analyzer';

const guardedInitContract = `
use soroban_sdk::{contract, contractimpl, Address, Env};

pub struct TokenContract {
pub owner: Address,
}

impl TokenContract {
pub fn initialize(env: Env, owner: Address) {
if env.storage().has(&"initialized") {
panic!("already initialized");
}
env.storage().set(&"initialized", &true);
env.storage().set(&"owner", &owner);
}
}
`;

const ungardedInitContract = `
pub struct SimpleContract;

impl SimpleContract {
pub fn init(owner: Address) {
storage().set("owner", owner);
}
}
`;

const upgradeContract = `
pub struct UpgradeableContract {
pub admin: Address,
}

impl UpgradeableContract {
pub fn upgrade(env: Env, wasm_hash: BytesN<32>) {
self.admin.require_auth();
env.deployer().update_current_contract_wasm(wasm_hash);
}
}
`;

const unsafeUpgradeContract = `
pub struct UnsafeContract;

impl UnsafeContract {
pub fn upgrade(env: Env, wasm_hash: BytesN<32>) {
env.deployer().update_current_contract_wasm(wasm_hash);
}
}
`;

describe('StellarLifecycleAnalyzer', () => {
describe('initialization flows', () => {
it('detects guarded init flow', () => {
const a = new StellarLifecycleAnalyzer(guardedInitContract, 'test.rs');
const r = a.analyze();
expect(r.initFlows).toHaveLength(1);
expect(r.initFlows[0].functionName).toBe('initialize');
expect(r.initFlows[0].hasGuard).toBe(true);
});

it('detects unguarded init and raises high-severity issue', () => {
const a = new StellarLifecycleAnalyzer(ungardedInitContract, 'test.rs');
const r = a.analyze();
expect(r.initFlows[0].hasGuard).toBe(false);
const high = r.issues.filter(i => i.severity === 'high' && i.message.includes('init'));
expect(high.length).toBeGreaterThan(0);
});
});

describe('upgrade flows', () => {
it('detects access-controlled upgrade', () => {
const a = new StellarLifecycleAnalyzer(upgradeContract, 'test.rs');
const r = a.analyze();
expect(r.upgradeFlows).toHaveLength(1);
expect(r.upgradeFlows[0].hasAccessControl).toBe(true);
expect(r.upgradeFlows[0].isWasm).toBe(true);
});

it('flags upgrade without access control', () => {
const a = new StellarLifecycleAnalyzer(unsafeUpgradeContract, 'test.rs');
const r = a.analyze();
expect(r.upgradeFlows[0].hasAccessControl).toBe(false);
const issue = r.issues.find(i => i.message.includes('upgrade'));
expect(issue).toBeDefined();
expect(issue!.severity).toBe('high');
});
});

describe('report generation', () => {
it('generates a lifecycle report with summary', () => {
const a = new StellarLifecycleAnalyzer(guardedInitContract, 'test.rs');
const r = a.analyze();
expect(r.contractName).toBeTruthy();
expect(r.summary).toContain(r.contractName);
expect(r.summary).toContain('init flow');
});

it('raises medium issue when no init flow present', () => {
const noInit = `pub struct Empty; impl Empty {}`;
const a = new StellarLifecycleAnalyzer(noInit, 'test.rs');
const r = a.analyze();
const medium = r.issues.find(i => i.severity === 'medium');
expect(medium).toBeDefined();
});
});
});
149 changes: 149 additions & 0 deletions src/analysis/stellar/lifecycle/lifecycle-analyzer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* Soroban Contract Lifecycle Analyzer
*
* Detects initialization and upgrade flows in Soroban contracts,
* flags missing guards, and generates lifecycle reports.
*/

import { InitFlow, LifecycleIssue, LifecycleReport, UpgradeFlow } from './types';

export class StellarLifecycleAnalyzer {
private source: string;
private filePath: string;

constructor(source: string, filePath: string) {
this.source = source;
this.filePath = filePath;
}

analyze(): LifecycleReport {
const contractName = this.extractContractName();
const initFlows = this.extractInitFlows();
const upgradeFlows = this.extractUpgradeFlows();
const issues = this.detectIssues(initFlows, upgradeFlows);

return {
contractName,
initFlows,
upgradeFlows,
issues,
summary: this.buildSummary(contractName, initFlows, upgradeFlows, issues),
};
}

private extractContractName(): string {
const match = this.source.match(/pub struct (\w+)/) || this.source.match(/impl\s+(\w+)/);
return match ? match[1] : 'UnknownContract';
}

private extractInitFlows(): InitFlow[] {
const flows: InitFlow[] = [];
const initRegex = /fn\s+(init(?:ialize)?|setup|constructor)\s*\([^)]*\)/g;
let match;

while ((match = initRegex.exec(this.source)) !== null) {
const fnName = match[1];
const line = this.lineOf(match.index);
const bodyStart = this.source.indexOf('{', match.index);
const body = bodyStart !== -1 ? this.extractBlock(bodyStart + 1) : '';

const hasGuard =
body.includes('require_auth') ||
body.includes('panic!') ||
body.includes('storage().has') ||
/if\s+.*already/.test(body);

const storageKeys = [...body.matchAll(/storage\(\)\.\w+\s*\(\s*["']?(\w+)["']?/g)].map(m => m[1]);

flows.push({ functionName: fnName, hasGuard, storageKeys, line });
}
return flows;
}

private extractUpgradeFlows(): UpgradeFlow[] {
const flows: UpgradeFlow[] = [];
const upgradeRegex = /fn\s+(upgrade|update(?:_wasm)?|migrate)\s*\([^)]*\)/g;
let match;

while ((match = upgradeRegex.exec(this.source)) !== null) {
const fnName = match[1];
const line = this.lineOf(match.index);
const bodyStart = this.source.indexOf('{', match.index);
const body = bodyStart !== -1 ? this.extractBlock(bodyStart + 1) : '';

const hasAccessControl =
body.includes('require_auth') ||
body.includes('assert_admin') ||
/if\s+.*!=\s*admin/.test(body);

const isWasm =
body.includes('update_current_contract_wasm') ||
body.includes('wasm') ||
fnName.includes('wasm');

flows.push({ functionName: fnName, hasAccessControl, isWasm, line });
}
return flows;
}

private detectIssues(initFlows: InitFlow[], upgradeFlows: UpgradeFlow[]): LifecycleIssue[] {
const issues: LifecycleIssue[] = [];

for (const flow of initFlows) {
if (!flow.hasGuard) {
issues.push({
severity: 'high',
message: `Initialization function '${flow.functionName}' lacks a re-initialization guard`,
line: flow.line,
});
}
}

for (const flow of upgradeFlows) {
if (!flow.hasAccessControl) {
issues.push({
severity: 'high',
message: `Upgrade function '${flow.functionName}' missing access control`,
line: flow.line,
});
}
}

if (initFlows.length === 0) {
issues.push({ severity: 'medium', message: 'No initialization flow detected' });
}

return issues;
}

private buildSummary(
contractName: string,
initFlows: InitFlow[],
upgradeFlows: UpgradeFlow[],
issues: LifecycleIssue[],
): string {
const high = issues.filter(i => i.severity === 'high').length;
return (
`Contract '${contractName}': ` +
`${initFlows.length} init flow(s), ` +
`${upgradeFlows.length} upgrade flow(s), ` +
`${high} high-severity issue(s).`
);
}

private extractBlock(start: number): string {
let depth = 1;
let result = '';
for (let i = start; i < this.source.length && depth > 0; i++) {
const ch = this.source[i];
if (ch === '{') depth++;
else if (ch === '}') { depth--; if (depth === 0) break; }
result += ch;
}
return result;
}

private lineOf(offset: number): number {
return (this.source.slice(0, offset).match(/\n/g) ?? []).length + 1;
}
}
27 changes: 27 additions & 0 deletions src/analysis/stellar/lifecycle/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface InitFlow {
functionName: string;
hasGuard: boolean;
storageKeys: string[];
line: number;
}

export interface UpgradeFlow {
functionName: string;
hasAccessControl: boolean;
isWasm: boolean;
line: number;
}

export interface LifecycleIssue {
severity: 'low' | 'medium' | 'high';
message: string;
line?: number;
}

export interface LifecycleReport {
contractName: string;
initFlows: InitFlow[];
upgradeFlows: UpgradeFlow[];
issues: LifecycleIssue[];
summary: string;
}
Loading