feat: add liquidity reservation layer for concurrent loan creation (#81)#88
Open
LaPoshBaby wants to merge 1 commit into
Open
Conversation
…loses StepFi-app#81) Introduces a Redis-backed reservation ledger that holds pool funds from credit assessment through on-chain settlement, preventing concurrent loans from over-committing shared liquidity. - Per-pool promise-chain mutex + Lua-atomic acquire/release - 15-min TTL auto-expiry so signed-but-never-submitted funds never lock forever - Drift gauge reconciled against on-chain locked_liquidity every 5min - Release hooks wired into transaction-status-checker (confirm / revert / LOAN_REPAY / TTL paths) - In-memory fallback when LIQUIDITY_RESERVATION_REDIS_URL is unset - Prometheus metrics: inflight, acquired, released, rejected, drift, reconciliations, ttl_expirations Concurrency acceptance test: 10 parallel acquirers against a pool whose capacity fits exactly 5 yields exactly 5 acquisitions / 5 insufficient rejections - proves no double-spend. Verification: - npm run build: clean - npm test: 303 / 303 pass (no regression) - eslint: clean
EmeditWeb
reviewed
Jul 21, 2026
EmeditWeb
left a comment
Member
There was a problem hiding this comment.
Review verdict: ✅ APPROVE (strong)
Well-architected concurrency control for the reserve-then-settle race. Highlights:
- Lua-atomic Redis operations (
ACQUIRE_RESERVATION_LUA/RELEASE_RESERVATION_LUA) — the correct way to make the ZSET + metadata moves atomic under concurrency. - Reserve-before-settle with rollback in
loans.service— reserves liquidity before building the funding XDR and releases on failure (releaseReservationQuietly). @Cron('0 */5 * * * *')cleanup (not BullMQ — matches the repo rule), noany, dual in-memory/Redis store behind an interface,evalsha→evalNOSCRIPT fallback. CI green.
Notes before merge:
- Confirm the added Redis ZSET + Lua-per-reservation load fits the Upstash free-tier budget (the reason BullMQ was removed). The in-memory fallback suggests Redis is optional/configurable — please document the intended production mode.
- Ensure the new
reservation_idfield on the create-loan response DTO has an@ApiPropertydecorator.
Review-depth note: I verified the atomicity design, standards, and reserve/rollback integration path — not every branch of the Lua scripts. Given this guards pooled funds, one focused pass over the two Lua scripts before merge would be worthwhile. Otherwise this is approve-quality.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: add liquidity reservation layer for concurrent loan creation (#81)
Summary
Closes #81. Introduces a Redis-backed reservation ledger that prevents concurrent loan creations from over-committing shared pool funds between credit assessment and on-chain settlement.
Problem Statement
The current implementation of
loans.service.createLoan()has a critical race condition in its loan creation flow:The Race Condition: Between steps 1 and 4, there is no commitment of funds. Two concurrent callers can:
InsufficientLiquidityerrorImpact:
Solution Overview
Implement a per-pool reservation ledger that atomically reserves liquidity during the loan creation process, ensuring that concurrent requests cannot over-commit available funds.
Architecture
The solution introduces a new
ReservationsModulewith the following components:1. Reservation Store Interface
ReservationStore) defining reservation operationsRedisReservationStore: Production implementation using Redis with Lua scripts for atomicityInMemoryReservationStore: Development/testing fallback using in-memory maps2. Liquidity Reservation Service
3. Transaction Status Checker Integration
FUNDED: Transaction succeeded - release reservationFAILED: Transaction reverted - release reservationLOAN_REPAY: Loan repaid - release reservationTTL: Reservation expired - release automatically4. Drift Reconciliation
@Cron('0 */5 * * * *'))locked_liquidity5. Metrics and Observability
Comprehensive Prometheus metrics:
stepfi_liquidity_reservations_inflight: Current number of active reservations (gauge)stepfi_liquidity_reservations_acquired_total: Total reservations acquired (counter)stepfi_liquidity_reservations_released_total: Total reservations released by reason (counter with labels)stepfi_liquidity_reservations_rejected_total: Total reservations rejected by reason (counter with labels)stepfi_liquidity_reservations_drift_stroops: Drift amount in stroops (gauge)stepfi_liquidity_reservations_reconciliations_total: Total reconciliation runs (counter)stepfi_liquidity_reservations_ttl_expirations_total: Total TTL expirations (counter)Technical Implementation Details
Reservation Acquisition Flow
Reservation Release Flow
Reservations are released in the following scenarios:
FUNDEDFAILEDstatusLOAN_REPAY)Redis Implementation Details
Lua Scripts for Atomicity:
acquire.lua: Atomically checks capacity and creates reservationrelease.lua: Atomically removes reservation and updates metricsData Structure:
reservation:{poolId}:{reservationId}In-Memory Fallback
LIQUIDITY_RESERVATION_REDIS_URLis unsetDatabase Schema Changes
New columns added to
loanstable:reservation_id: UUID referencing the reservationreservation_expires_at: Timestamp for TTL trackingAPI Changes
CreateLoanResponseDto Enhancement
Three new optional fields added to the response:
reservationId: Unique identifier for the reservationexpiresAt: ISO 8601 timestamp when reservation expirespoolCapacityUsd: Current available capacity in USDBackwards Compatibility: Existing clients continue to work as these fields are optional. The core contract (returning funding XDR) remains unchanged.
Configuration
Environment Variables
New environment variables added to
docs/setup/environment-variables.md:LIQUIDITY_RESERVATION_REDIS_URL: Redis connection string (optional, defaults to in-memory)LIQUIDITY_RESERVATION_TTL_SECONDS: Reservation TTL in seconds (default: 900)LIQUIDITY_RESERVATION_ENABLED: Feature flag (default: true)Testing
Unit Tests
New test suite:
test/unit/modules/liquidity/reservations/liquidity-reservation.service.spec.tsIntegration Tests
Updated existing loan service tests:
test/unit/modules/loans/loans.service.spec.tstest/unit/modules/loans/loans.controller.spec.tsConcurrency Test
New test specifically validates the race condition fix:
Transaction Status Checker Tests
Rewritten test suite:
test/unit/jobs/transaction-status-checker/transaction-status-checker.service.spec.tsVerification Results
npm run build- Clean, zero TypeScript errorsnpm test- 303/303 tests pass across 26 suites (no regression)npx eslintscoped to changed files - Clean, no violationsAcceptance Criteria Validation
✅ Concurrent loan creations cannot over-commit available liquidity
✅ Reservation acquired before funding XDR is returned
reserveForLoan()called beforebuildCreateLoanTransaction()inloans.service.ts✅ Reservations expire (TTL) and release on failure/settlement
✅ Reservation totals reconciled against on-chain locked liquidity
✅ Concurrency test proves no double-spend under parallel requests
Risk Assessment and Rollback Plan
Risks
Redis Unavailability
Reservation Drift
TTL Expiration Edge Cases
Rollback Procedure
If issues arise, rollback involves:
ReservationsModulefromsrc/app.module.tssrc/modules/loans/loans.service.tsBackwards Compatibility
createLoanstill returns funding XDRPerformance Considerations
Future Enhancements
Potential improvements for future iterations: