Skip to content

Fix/1530 1531 1545 1546 - #1647

Merged
Junirezz merged 6 commits into
Junirezz:mainfrom
prizncezz-cell:fix/1530-1531-1545-1546
Oct 18, 2026
Merged

Junirezz merged 6 commits into
Junirezz:mainfrom
prizncezz-cell:fix/1530-1531-1545-1546

Conversation

@prizncezz-cell

Copy link
Copy Markdown
Contributor

🚀 Overview

This Pull Request delivers comprehensive smart contract security hardening, automated database schema drift validation, and Express router refactoring for Junirezz/YieldVault-RWA:

  1. Strategy Response Bounds Hardening ([contract] Harden validate_strategy_response to full bounds + consistent use #1530): Enforces bounds checking in validate_strategy_response (rejecting zero-address strategies, asset tokens exceeding 30 decimals, and strategy valuations outside $0 \le \text{value} \le \text{MAX_STRATEGY_VALUE}$) across all strategy state reads, harvested yields, and rebalancing operations.
  2. Cryptographic Multi-Signer Authorization ([contract] Make MultiSignerValidator return VaultError + require_auth per approver #1531): Refactors MultiSignerValidator to return typed VaultError variants, enforces Soroban approver.require_auth() cryptographic signatures per signer, and replaces $O(n^2)$ array comparisons with map-based set duplicate detection.
  3. Dev/Prod Database Schema Alignment & Drift Guard ([backend] SQLite dev schema vs Postgres prod types will diverge silently #1546): Formulates a formal Postgres-to-Prisma type mapping specification (prisma/postgres-type-mapping.json), documents numeric precision constraints (docs/DB_TYPE_MAPPING.md), adds high-precision decimal helpers (src/numericColumns.ts), and wires an automated CI schema drift check (npm run db:check-types).
  4. Backend Express Monolith Modularization ([backend] Monolith index.ts (5.3k lines) blocks safe review / test isolation #1545): Deconstructs the ~5,600-line monolithic backend/src/index.ts into isolated feature routers (routes/vault.ts, transactions.ts, webhooks.ts, exports.ts, admin.ts, shared.ts) with zero route table or middleware chain behavioral drift.

🔑 Linked Issues


🛠️ Summary of Changes

🛡️ 1. Strategy Valuation Bounds & Consistency (contracts/vault/src/) (#1530)

  • Strict Parameter Bounds: validate_strategy_response now explicitly validates against zero-address strategy targets, asset token decimals $> 30$, and valuations exceeding MAX_STRATEGY_VALUE (returning VaultError::MathOverflow).
  • Rebalance & Harvest Rules: Applied bounds checking to Korean strategy harvested yield calls and enforced a post-deposit check during rebalancing to confirm strategy value increases.
  • Boundary Unit Tests: Added coverage for boundary edge cases: $0$, MAX_STRATEGY_VALUE, MAX_STRATEGY_VALUE + 1, i128::MAX, $-1$, zero-address targets, and total_assets evaluations.

🔐 2. Multi-Signer Authentication & Set-Based Dedup (contracts/vault/src/permissions.rs) (#1531)

  • Cryptographic Signatures: Invokes approver.require_auth() per signer to ensure authorization payloads cannot be spoofed.
  • Map-Based $O(1)$ Dedup: Replaced $O(n^2)$ pairwise loops with map-based set logic for membership and duplicate checking.
  • Error Granularity: Replaced generic GovernanceThresholdNotMet errors with specific VaultError::InvalidGovernanceThreshold and VaultError::DuplicateVote variants.
  • Migration Order: Pre-selects the signer set prior to checking signatures during signer-set migrations so each approver signs exactly once. Added contract tests verifying rejection of unauthenticated signer lists.

🗄️ 3. Postgres/Prisma Type Mapping & Drift Check (backend/) (#1546)

  • Type Mapping & Specs: Created prisma/postgres-type-mapping.json and docs/DB_TYPE_MAPPING.md mapping every Postgres native DECIMAL, JSONB, and TIMESTAMPTZ column to its corresponding Prisma field.
  • CI Drift Guard: Introduced npm run db:check-types (running in the Backend Governance CI pipeline) to verify schema alignment without requiring an active database instance.
  • 38-Digit Decimal Precision: Added src/numericColumns.ts and unit tests ensuring Decimal.js default precision caps (20 digits) are extended to handle full 38-digit integer and 18-decimal Postgres precision without truncation.

🧩 4. Express Monolith Router Decomposition (backend/src/) (#1545)

  • Feature Router Extraction: Reduced index.ts from ~5,600 to ~880 lines by extracting handlers into:
    • routes/vault.ts: Vault management endpoints.
    • routes/transactions.ts: Deposit/withdrawal flows.
    • routes/webhooks.ts: Off-chain and bridge webhook event handlers.
    • routes/exports.ts: Report generation routes.
    • routes/admin.ts: Administrative and impersonation routes.
    • routes/shared.ts: Shared middleware and route utility helpers.
  • Zero Behavioral Drift: Captured and verified pre- and post-refactor route tables; paths, middleware chains, and execution ordering remain identical.

📁 Key Files Touched

File / Path Change Summary
contracts/vault/src/lib.rs Enforced strategy validation bounds on harvests, rebalances, and valuation reads.
contracts/vault/src/permissions.rs Added approver.require_auth(), map-based dedup, and granular VaultError returns.
backend/src/index.ts Reduced monolith to a thin composition root (~880 lines).
backend/src/routes/ Created feature routers (vault.ts, transactions.ts, webhooks.ts, exports.ts, admin.ts, shared.ts).
backend/prisma/postgres-type-mapping.json Mapping specification for Postgres native types vs Prisma schema fields.
backend/docs/DB_TYPE_MAPPING.md Architectural documentation detailing decimal precision and type alignment contracts.
backend/src/numericColumns.ts Helper utilities preserving 38-digit integer / 18-decimal precision.

🧪 Verification Logs

  • Vault Contracts: All strategy validation bounds and multisig authentication unit/integration tests passed cleanly (cargo test -p vault). Existing baseline test failures on main (36 tests) remain unchanged.
  • Backend Suite: Preserved existing backend test baseline (1,511 pass, 107 pre-existing failures reproduce identically before and after router extraction).
  • Database Drift: npm run db:check-types passed cleanly.

… approvals

- vault: validate_strategy_response rejects the zero strategy address,
  asset tokens with more than 30 decimals and values outside
  0..=MAX_STRATEGY_VALUE; harvested Korean yield is bounded and rebalance
  checks the target strategy's deposit delta with StrategyValidator (Junirezz#1530)
- permissions: MultiSignerValidator returns VaultError, uses Map-backed sets
  for duplicate / membership checks and calls require_auth on every
  approver; require_governance_threshold picks the signer set without auth
  so each approver signs once (Junirezz#1531)
- fix pre-existing compile errors in rounding.rs, total_assets, the wasm
  zero-address check and vault lib tests so the tests run

Closes Junirezz#1530
Closes Junirezz#1531
Same repairs as the OpenAPI branch: decode base64-encoded healthProbe.ts and
apiKeyAuth.ts, drop duplicated merge blocks in eventOutbox, idempotency,
apiError, pagination, tracing, outboxPoller, vaultEndpoints and
routes/vaults, fix the @yieldvault/api-schemas dependency name, sync the
lockfile and remove a trailing comma in the root package.json.
- prisma/postgres-type-mapping.json maps every PostgreSQL NUMERIC / JSONB /
  TIMESTAMPTZ / DATE column to its Prisma (SQLite) field or null
- scripts/check-type-mapping.js (npm run db:check-types) fails when a
  divergent column is unmapped, a mapped column/field disappears, or a
  Prisma type no longer matches; runs in Backend Governance CI
- src/numericColumns.ts: NUMERIC(38, 18) string encoding with 38-digit
  Decimal precision, plus precision and drift tests
- docs/DB_TYPE_MAPPING.md

Closes Junirezz#1546
Move route handlers out of the 5.6k-line src/index.ts into feature routers:

- routes/vault.ts: vault summary / metrics / APY / health (legacy, v1, v2)
- routes/transactions.ts: transaction exports and admin backfill
- routes/webhooks.ts: webhook verification, /api/v1/webhooks and
  /admin/webhooks/* (incl. deduplication)
- routes/exports.ts: /admin/exports/* and /admin/reports/exports/*
- routes/admin.ts: remaining /admin/* operational endpoints
- routes/shared.ts: helpers and the health cache used by several modules

Handler bodies are moved verbatim. index.ts (now ~880 lines) keeps the
middleware stack, health/ops routes and startup wiring, and mounts the
routers where the routes used to be registered, so the effective route
table, handler chains and ordering of overlapping routes are unchanged;
the full Jest suite gives identical per-test results before and after.

Closes Junirezz#1545
@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

@prizncezz-cell Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Junirezz
Junirezz merged commit 2aee07f into Junirezz:main Oct 18, 2026
8 of 20 checks passed
Comment on lines +338 to +364
router.post('/admin/reports/exports/manifests/:id/verify', validateApiKey, validate({ params: IdParamSchema, body: ChecksumVerifyBodySchema }), async (req: Request, res: Response) => {
const checksum = String(req.body?.checksum || '').trim();
if (!checksum) {
res.status(400).json({
error: 'Bad Request',
status: 400,
message: 'checksum is required',
});
return;
}

const result = await verifyExportManifestChecksum(String(req.params.id), checksum);
if (!result.manifest) {
res.status(404).json({
error: 'Not Found',
status: 404,
message: 'Export manifest not found',
});
return;
}

res.status(200).json({
match: result.match,
manifest: result.manifest,
timestamp: new Date().toISOString(),
});
});
const walletAddress =
typeof req.query.walletAddress === 'string' ? normalizeWalletAddress(req.query.walletAddress) : undefined;

const apiKeyMatch = authHeader.match(/^ApiKey\s+(.+)$/i);
};
}

const bearerMatch = authHeader.match(/^Bearer\s+(.+)$/i);
* POST /api/v1/webhooks — authenticated clients register an HTTPS callback
* instead of polling for transaction/vault events.
*/
apiV1WebhookRoutes.post('/webhooks', requireAuth, validate({ body: WebhookRegisterSchema }), (req: Request, res: Response) => {
}
});

apiV1WebhookRoutes.get('/webhooks', requireAuth, validate({ query: PaginationQuerySchema }), (req: Request, res: Response) => {
Comment on lines +58 to +89
router.post('/webhooks/verify', validate({ body: WebhookVerifyBodySchema }), (req: Request, res: Response) => {
const { secret, payload, signature } = req.body || {};
if (typeof secret !== 'string' || !secret.trim()) {
res.status(400).json({
error: 'Bad Request',
status: 400,
message: 'secret is required and must be a non-empty string',
});
return;
}

if (typeof payload === 'undefined') {
res.status(400).json({
error: 'Bad Request',
status: 400,
message: 'payload is required',
});
return;
}

const computedSignature = createWebhookSignature(secret, payload);
const verified =
typeof signature === 'string' && signature.length > 0
? verifyWebhookSignature(secret, payload, signature)
: null;

res.status(200).json({
algorithm: 'HMAC-SHA256',
signature: computedSignature,
verified,
});
});
Comment on lines +113 to +145
router.post('/admin/webhooks/:id/verify', validateApiKey, validate({ params: IdParamSchema }), async (req: Request, res: Response) => {
try {
const endpoint = await verifyWebhookEndpoint(req.params.id);
if (!endpoint) {
res.status(404).json({
error: 'Not Found',
status: 404,
message: 'Webhook endpoint not found',
});
return;
}

await recordAdminAuditLog(req, 'webhook.verify', endpoint.verificationStatus === 'verified' ? 200 : 422, {
endpointId: endpoint.id,
verificationStatus: endpoint.verificationStatus,
lastVerificationError: endpoint.lastVerificationError,
});

res.status(endpoint.verificationStatus === 'verified' ? 200 : 422).json({
message:
endpoint.verificationStatus === 'verified'
? 'Webhook endpoint verified'
: 'Webhook endpoint verification failed',
endpoint,
});
} catch (error) {
res.status(500).json({
error: 'Internal Server Error',
status: 500,
message: error instanceof Error ? error.message : 'Failed to verify webhook endpoint',
});
}
});
Comment on lines +346 to +377
router.post('/api/v1/webhooks/verify', validate({ body: WebhookVerifyBodySchema }), (req: Request, res: Response) => {
const { secret, payload, signature } = req.body || {};
if (typeof secret !== 'string' || !secret.trim()) {
res.status(400).json({
error: 'Bad Request',
status: 400,
message: 'secret is required and must be a non-empty string',
});
return;
}

if (typeof payload === 'undefined') {
res.status(400).json({
error: 'Bad Request',
status: 400,
message: 'payload is required',
});
return;
}

const computedSignature = createWebhookSignature(secret, payload);
const verified =
typeof signature === 'string' && signature.length > 0
? verifyWebhookSignature(secret, payload, signature)
: null;

res.status(200).json({
algorithm: 'HMAC-SHA256',
signature: computedSignature,
verified,
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants