This PR implements a decorator-based system for enforcing maximum Soroban cross-contract call depth per trade-related endpoint, addressing issue #794.
Trade execution can involve cross-contract calls (e.g., trade_executor calling fee_collector and stake_vault). There was no declared, enforced expectation per endpoint of the maximum acceptable call depth, risking unnoticed regressions that add unexpectedly deep call chains and budget risk.
Added a decorator system that:
- Declares the expected maximum cross-contract call depth for a given endpoint
- Extracts and validates actual call depth from Soroban transaction simulation responses
- Rejects or warns (configurable) when the simulated call depth exceeds the declared maximum
@MaxCallDepth({ maxDepth: number, endpoint?: string, onViolation?: 'reject' | 'warn' })decorator- Declares the maximum allowed cross-contract call depth for an endpoint
- Supports per-endpoint configuration and violation policy
- Validates call depth from request metadata
- Throws
ConflictException(409) when depth exceeds maximum in reject mode - Logs warning but allows request in warn mode
- Gracefully handles missing call depth data
- Extracts call depth from Soroban simulation responses
- Parses auth entries to calculate maximum nesting depth of
subInvocations - Falls back to footprint-based estimation when auth entries unavailable
- Validates depth against declared maximum with configurable policy
STELLAR_MAX_CALL_DEPTH- Default maximum call depth (default: 5)STELLAR_MAX_CALL_DEPTH_POLICY- Global violation policy: 'reject' or 'warn'
src/common/decorators/max-call-depth.decorator.tssrc/common/guards/max-call-depth.guard.tssrc/common/guards/max-call-depth.guard.spec.tssrc/common/services/max-call-depth.service.tssrc/common/services/max-call-depth.service.spec.tssrc/common/max-call-depth.module.ts
src/common/decorators/index.ts- Added MaxCallDepth exportssrc/app.module.ts- Imported MaxCallDepthModulesrc/config/stellar.config.ts- Added maxCallDepth and maxCallDepthViolationPolicysrc/config/stellar.service.ts- Added getters for maxCallDepth configsrc/config/schemas/config.interface.ts- Extended StellarConfig interfacesrc/soroban/soroban.service.ts- Integrated MaxCallDepthService validationsrc/soroban/soroban.module.ts- Added MaxCallDepthModule importsrc/trades/trades.controller.ts- Applied @MaxCallDepth decorator to endpoints.env.example- Added STELLAR_MAX_CALL_DEPTH and STELLAR_MAX_CALL_DEPTH_POLICY
POST /trades/execute- Max depth: 5 (cross-contract calls to trade_executor, fee_collector, stake_vault)POST /trades/close- Max depth: 3 (simpler close operation)POST /trades/validate- No decorator (validation only, no contract calls)POST /trades/partial-close- No decorator (delegated to partial-close service)
max-call-depth.guard.spec.ts- Tests guard behavior for various scenariosmax-call-depth.service.spec.ts- Tests depth extraction and validation
Test coverage includes:
- Depth extraction from auth entries with nested subInvocations
- Footprint-based depth estimation fallback
- Validation within/above/below declared maximum
- Warn mode vs reject mode behavior
- Configuration fallback to defaults
// In trades.controller.ts
@Post('execute')
@UseGuards(MaxCallDepthGuard)
@MaxCallDepth({ maxDepth: 5, endpoint: 'execute-trade', onViolation: 'reject' })
async executeTrade(@Body() dto: ExecuteTradeDto): Promise<TradeResultDto> {
return this.commandBus.execute(new ExecuteTradeCommand(dto));
}# Maximum cross-contract call depth (default: 5)
STELLAR_MAX_CALL_DEPTH=5
# Violation policy: 'reject' (throw 409) or 'warn' (log only)
STELLAR_MAX_CALL_DEPTH_POLICY=reject- Add a decorator declaring the expected maximum cross-contract call depth for a given trade-related endpoint
- Verify the actual call depth from the transaction simulation result against the declared maximum before submission
- Reject or warn (configurable) when the simulated call depth exceeds the declared maximum
- Add unit tests covering simulated results at, below, and above the declared maximum depth