From 4b813097aa9a62a756d7dbb51631f419d394e9fc Mon Sep 17 00:00:00 2001 From: Kingeligma Date: Sat, 26 Sep 2026 15:19:32 +0100 Subject: [PATCH] ci: enforce backend types and contract formatting --- .github/workflows/backend-governance.yml | 6 + .github/workflows/rust-wasm.yml | 2 +- Cargo.lock | 7 + backend/openapi.json | 121 +++++++++++++++++- backend/package-lock.json | 25 ++-- .../src/__tests__/cacheInvalidation.test.ts | 17 +++ backend/src/__tests__/openapi.test.ts | 19 +++ backend/src/middleware/cache.ts | 24 +++- backend/src/swagger.ts | 27 +++- backend/src/vaultEndpoints.ts | 1 - contracts/bridge-compat/src/lib.rs | 81 ++++-------- contracts/vault/src/lib.rs | 82 +++++------- 12 files changed, 295 insertions(+), 117 deletions(-) create mode 100644 backend/src/__tests__/cacheInvalidation.test.ts diff --git a/.github/workflows/backend-governance.yml b/.github/workflows/backend-governance.yml index 7f0157011..3713c23a3 100644 --- a/.github/workflows/backend-governance.yml +++ b/.github/workflows/backend-governance.yml @@ -56,6 +56,12 @@ jobs: - name: Install dependencies run: npm ci + - name: Generate Prisma client + run: npm run prisma:generate + + - name: Type-check backend + run: npm run build + - name: Validate Prisma schema and migration consistency run: npm run prisma:schema-check diff --git a/.github/workflows/rust-wasm.yml b/.github/workflows/rust-wasm.yml index 5fcbe7add..6d1829f6f 100644 --- a/.github/workflows/rust-wasm.yml +++ b/.github/workflows/rust-wasm.yml @@ -28,7 +28,7 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable - components: clippy + components: clippy, rustfmt profile: minimal override: true diff --git a/Cargo.lock b/Cargo.lock index 5dfd75187..bdf36c8c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,6 +195,13 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bridge-compat" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "bumpalo" version = "3.20.2" diff --git a/backend/openapi.json b/backend/openapi.json index 164916fb7..b94a07d4c 100644 --- a/backend/openapi.json +++ b/backend/openapi.json @@ -186,6 +186,121 @@ "example": "99.5" } } + }, + "HealthResponse": { + "type": "object", + "required": [ + "status", + "timestamp", + "uptime", + "environment", + "checks" + ], + "properties": { + "status": { + "type": "string", + "example": "healthy" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "uptime": { + "type": "number", + "example": 123.4 + }, + "environment": { + "type": "string", + "example": "production" + }, + "checks": { + "type": "object", + "required": [ + "api", + "cache", + "stellarRpc", + "databasePrimary", + "databaseReplica", + "prisma", + "jobs", + "indexer" + ], + "properties": { + "api": { + "type": "string", + "enum": [ + "up", + "down", + "degraded", + "unknown" + ] + }, + "cache": { + "type": "string", + "enum": [ + "up", + "down", + "degraded", + "unknown" + ] + }, + "stellarRpc": { + "type": "string", + "enum": [ + "up", + "down", + "degraded", + "unknown" + ] + }, + "databasePrimary": { + "type": "string", + "enum": [ + "up", + "down", + "degraded", + "unknown" + ] + }, + "databaseReplica": { + "type": "string", + "enum": [ + "up", + "down", + "degraded", + "unknown" + ] + }, + "prisma": { + "type": "string", + "enum": [ + "up", + "down", + "degraded", + "unknown" + ] + }, + "jobs": { + "type": "string", + "enum": [ + "up", + "down", + "degraded", + "unknown" + ] + }, + "indexer": { + "type": "string", + "enum": [ + "up", + "down", + "degraded", + "unknown" + ] + } + } + } + } } } }, @@ -233,6 +348,9 @@ "description": "Service healthy", "content": { "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + }, "example": { "status": "healthy", "timestamp": "2024-01-01T00:00:00.000Z", @@ -241,7 +359,8 @@ "checks": { "api": "up", "cache": "up", - "stellarRpc": "up" + "stellarRpc": "up", + "indexer": "up" } } } diff --git a/backend/package-lock.json b/backend/package-lock.json index b0061a963..3e4ec15ad 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2474,9 +2474,9 @@ } }, "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", "license": "Apache-2.0", "engines": { "node": ">=8.0.0" @@ -3602,13 +3602,13 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", - "dev": true + "devOptional": true }, "node_modules/@prisma/engines": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "dependencies": { "@prisma/debug": "5.22.0", @@ -3621,13 +3621,13 @@ "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", - "dev": true + "devOptional": true }, "node_modules/@prisma/fetch-engine": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", - "dev": true, + "devOptional": true, "dependencies": { "@prisma/debug": "5.22.0", "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", @@ -3638,7 +3638,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", - "dev": true, + "devOptional": true, "dependencies": { "@prisma/debug": "5.22.0" } @@ -8469,6 +8469,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -8962,7 +8969,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "5.22.0" diff --git a/backend/src/__tests__/cacheInvalidation.test.ts b/backend/src/__tests__/cacheInvalidation.test.ts new file mode 100644 index 000000000..f50f23bc4 --- /dev/null +++ b/backend/src/__tests__/cacheInvalidation.test.ts @@ -0,0 +1,17 @@ +import { registerInvalidationHook, triggerCacheInvalidation } from '../middleware/cache'; + +describe('triggerCacheInvalidation', () => { + it('skips non-array hook results without throwing', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined); + registerInvalidationHook( + (() => undefined) as unknown as (eventType: string, metadata?: Record) => string[], + ); + registerInvalidationHook( + (() => ['GET:/health']) as (eventType: string, metadata?: Record) => string[], + ); + + expect(() => triggerCacheInvalidation('test.event')).not.toThrow(); + expect(triggerCacheInvalidation('test.event').patternsInvalidated).toContain('GET:/health'); + errorSpy.mockRestore(); + }); +}); \ No newline at end of file diff --git a/backend/src/__tests__/openapi.test.ts b/backend/src/__tests__/openapi.test.ts index 24309e4e0..2b02dc4e3 100644 --- a/backend/src/__tests__/openapi.test.ts +++ b/backend/src/__tests__/openapi.test.ts @@ -32,6 +32,25 @@ describe('OpenAPI documentation', () => { expect(spec.info.description).toMatch(/Rate limit/i); }); + it('documents the indexer health dependency', () => { + const health = spec.paths['/health'] as { + get: { + responses: { + '200': { + content: { + 'application/json': { + schema: { $ref: string }; + }; + }; + }; + }; + }; + }; + expect(health.get.responses['200'].content['application/json'].schema.$ref).toBe( + '#/components/schemas/HealthResponse', + ); + }); + it('serves the spec and Swagger UI over HTTP', async () => { const app = express(); setupSwagger(app); diff --git a/backend/src/middleware/cache.ts b/backend/src/middleware/cache.ts index 9182b1dac..b69a32e47 100644 --- a/backend/src/middleware/cache.ts +++ b/backend/src/middleware/cache.ts @@ -307,7 +307,10 @@ export function cacheMiddleware(options: CacheOptions) { // ── Invalidation ───────────────────────────────────────────────────────────── -type InvalidationHook = (eventType: string, metadata?: Record) => string[]; +type InvalidationHook = ( + eventType: string, + metadata?: Record, +) => string[] | Promise | Promise; const invalidationHooks: InvalidationHook[] = []; @@ -332,7 +335,24 @@ export function triggerCacheInvalidation( for (const hook of invalidationHooks) { try { const hookPatterns = hook(eventType, metadata); - patterns.push(...hookPatterns); + if (hookPatterns instanceof Promise) { + void hookPatterns.catch((err) => { + console.error( + JSON.stringify({ + level: 'error', + event: 'invalidation_hook_error', + error: err instanceof Error ? err.message : String(err), + }), + ); + }); + continue; + } + + if (!Array.isArray(hookPatterns)) { + throw new TypeError('invalidation hook must return an array of patterns'); + } + + patterns.push(...hookPatterns.filter((pattern): pattern is string => typeof pattern === 'string')); } catch (err) { console.error( JSON.stringify({ diff --git a/backend/src/swagger.ts b/backend/src/swagger.ts index 754327bda..418b67dc0 100644 --- a/backend/src/swagger.ts +++ b/backend/src/swagger.ts @@ -138,6 +138,30 @@ const options: swaggerJsdoc.Options = { shares: { type: 'string', example: '99.5' }, }, }, + HealthResponse: { + type: 'object', + required: ['status', 'timestamp', 'uptime', 'environment', 'checks'], + properties: { + status: { type: 'string', example: 'healthy' }, + timestamp: { type: 'string', format: 'date-time' }, + uptime: { type: 'number', example: 123.4 }, + environment: { type: 'string', example: 'production' }, + checks: { + type: 'object', + required: ['api', 'cache', 'stellarRpc', 'databasePrimary', 'databaseReplica', 'prisma', 'jobs', 'indexer'], + properties: { + api: { type: 'string', enum: ['up', 'down', 'degraded', 'unknown'] }, + cache: { type: 'string', enum: ['up', 'down', 'degraded', 'unknown'] }, + stellarRpc: { type: 'string', enum: ['up', 'down', 'degraded', 'unknown'] }, + databasePrimary: { type: 'string', enum: ['up', 'down', 'degraded', 'unknown'] }, + databaseReplica: { type: 'string', enum: ['up', 'down', 'degraded', 'unknown'] }, + prisma: { type: 'string', enum: ['up', 'down', 'degraded', 'unknown'] }, + jobs: { type: 'string', enum: ['up', 'down', 'degraded', 'unknown'] }, + indexer: { type: 'string', enum: ['up', 'down', 'degraded', 'unknown'] }, + }, + }, + }, + }, }, }, tags: [ @@ -160,12 +184,13 @@ const options: swaggerJsdoc.Options = { description: 'Service healthy', content: { 'application/json': { + schema: { $ref: '#/components/schemas/HealthResponse' }, example: { status: 'healthy', timestamp: '2024-01-01T00:00:00.000Z', uptime: 123.4, environment: 'production', - checks: { api: 'up', cache: 'up', stellarRpc: 'up' }, + checks: { api: 'up', cache: 'up', stellarRpc: 'up', indexer: 'up' }, }, }, }, diff --git a/backend/src/vaultEndpoints.ts b/backend/src/vaultEndpoints.ts index 83c7db895..abbbdf8cb 100644 --- a/backend/src/vaultEndpoints.ts +++ b/backend/src/vaultEndpoints.ts @@ -717,7 +717,6 @@ router.get('/strategy/cooldown', cacheMiddleware({ ttl: 5000 }), (_req: Request, }); }); -router.post('/strategy', depositsLimiter, requireFlag('strategy-selection'), (_req: Request, res: Response) => { router.post('/strategy', depositsLimiter, requireFlag('strategy-selection'), validate({ body: VaultStrategyBodySchema }), (req: Request, res: Response) => { const cooldownSec = parseInt(process.env.STRATEGY_SWITCH_COOLDOWN_SEC || '0', 10); const lastSwitchIso = process.env.LAST_STRATEGY_SWITCH_TIME || null; diff --git a/contracts/bridge-compat/src/lib.rs b/contracts/bridge-compat/src/lib.rs index 965d4a60f..777be82e0 100644 --- a/contracts/bridge-compat/src/lib.rs +++ b/contracts/bridge-compat/src/lib.rs @@ -189,7 +189,7 @@ impl BridgeCompat { env.storage().instance().set( &DataKey::Limits, &TransferLimits { - per_transfer_limit: 1_000_000_000_000, // 1M USDC (6 decimals) + per_transfer_limit: 1_000_000_000_000, // 1M USDC (6 decimals) epoch_volume_limit: 10_000_000_000_000, // 10M USDC per epoch epoch_duration: 86_400, // 24 hours }, @@ -253,10 +253,7 @@ impl BridgeCompat { .set(&DataKey::ProviderNonce, &provider_id); // Set as default if first provider - let has_default = env - .storage() - .instance() - .has(&DataKey::DefaultProvider); + let has_default = env.storage().instance().has(&DataKey::DefaultProvider); if !has_default { env.storage() .instance() @@ -283,19 +280,13 @@ impl BridgeCompat { env.storage() .instance() .set(&DataKey::Provider(provider_id), &provider); - env.events().publish( - (symbol_short!("brgtog"),), - (provider_id, enabled), - ); + env.events() + .publish((symbol_short!("brgtog"),), (provider_id, enabled)); Ok(()) } /// Update a provider's fee. Admin-only. - pub fn set_provider_fee( - env: Env, - provider_id: u32, - fee_bps: i128, - ) -> Result<(), BridgeError> { + pub fn set_provider_fee(env: Env, provider_id: u32, fee_bps: i128) -> Result<(), BridgeError> { Self::require_admin(&env)?; if !(0..=BPS_DENOMINATOR).contains(&fee_bps) { return Err(BridgeError::InvalidAmount); @@ -309,10 +300,7 @@ impl BridgeCompat { } /// Set the default provider. Admin-only. - pub fn set_default_provider( - env: Env, - provider_id: u32, - ) -> Result<(), BridgeError> { + pub fn set_default_provider(env: Env, provider_id: u32) -> Result<(), BridgeError> { Self::require_admin(&env)?; let _ = Self::get_provider(&env, provider_id)?; // validate exists env.storage() @@ -344,10 +332,7 @@ impl BridgeCompat { // ── Transfer limits ──────────────────────────────────────────────────── /// Update transfer limits. Admin-only. - pub fn set_transfer_limits( - env: Env, - limits: TransferLimits, - ) -> Result<(), BridgeError> { + pub fn set_transfer_limits(env: Env, limits: TransferLimits) -> Result<(), BridgeError> { Self::require_admin(&env)?; env.storage().instance().set(&DataKey::Limits, &limits); Ok(()) @@ -432,15 +417,15 @@ impl BridgeCompat { } // Check per-transfer limit - let limits: TransferLimits = env - .storage() - .instance() - .get(&DataKey::Limits) - .unwrap_or(TransferLimits { - per_transfer_limit: 1_000_000_000_000, - epoch_volume_limit: 10_000_000_000_000, - epoch_duration: 86_400, - }); + let limits: TransferLimits = + env.storage() + .instance() + .get(&DataKey::Limits) + .unwrap_or(TransferLimits { + per_transfer_limit: 1_000_000_000_000, + epoch_volume_limit: 10_000_000_000_000, + epoch_duration: 86_400, + }); if amount > limits.per_transfer_limit { return Err(BridgeError::TransferLimitExceeded); } @@ -449,11 +434,7 @@ impl BridgeCompat { Self::check_epoch_volume(&env, amount, &limits)?; // Check token balance - let token_addr: Address = env - .storage() - .instance() - .get(&DataKey::TokenAsset) - .unwrap(); + let token_addr: Address = env.storage().instance().get(&DataKey::TokenAsset).unwrap(); let token_client = soroban_sdk::token::Client::new(&env, &token_addr); let balance = token_client.balance(&env.current_contract_address()); if balance < amount { @@ -520,10 +501,7 @@ impl BridgeCompat { /// /// In production, this would be triggered by a bridge event listener. /// For testnet, admin can manually confirm transfers. - pub fn confirm_transfer( - env: Env, - transfer_id: u64, - ) -> Result<(), BridgeError> { + pub fn confirm_transfer(env: Env, transfer_id: u64) -> Result<(), BridgeError> { Self::require_admin(&env)?; let mut transfer: BridgeTransfer = env @@ -543,10 +521,8 @@ impl BridgeCompat { .instance() .set(&DataKey::Transfer(transfer_id), &transfer); - env.events().publish( - (symbol_short!("brgdone"),), - (transfer_id, transfer.amount), - ); + env.events() + .publish((symbol_short!("brgdone"),), (transfer_id, transfer.amount)); Ok(()) } @@ -555,10 +531,7 @@ impl BridgeCompat { /// /// If a transfer fails (e.g., bridge timeout), the tokens are returned /// to the sender. Admin-only in testnet; production would use oracle. - pub fn fail_transfer( - env: Env, - transfer_id: u64, - ) -> Result<(), BridgeError> { + pub fn fail_transfer(env: Env, transfer_id: u64) -> Result<(), BridgeError> { Self::require_admin(&env)?; let mut transfer: BridgeTransfer = env @@ -572,11 +545,7 @@ impl BridgeCompat { } // Refund the sender - let token_addr: Address = env - .storage() - .instance() - .get(&DataKey::TokenAsset) - .unwrap(); + let token_addr: Address = env.storage().instance().get(&DataKey::TokenAsset).unwrap(); let token_client = soroban_sdk::token::Client::new(&env, &token_addr); token_client.transfer( &env.current_contract_address(), @@ -656,7 +625,9 @@ impl BridgeCompat { env.storage().instance().set(&DataKey::EpochStart, &now); } - let new_volume = epoch_volume.checked_add(amount).ok_or(BridgeError::TransferLimitExceeded)?; + let new_volume = epoch_volume + .checked_add(amount) + .ok_or(BridgeError::TransferLimitExceeded)?; if new_volume > limits.epoch_volume_limit { return Err(BridgeError::TransferLimitExceeded); } @@ -716,7 +687,7 @@ mod tests { String::from_str(&env, "Wormhole"), BridgeProviderKind::Wormhole, endpoint, - 50, // 0.5% fee + 50, // 0.5% fee 1_000_000_000_000, chains, ) diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index 296180606..51c90258e 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -1381,7 +1381,6 @@ impl YieldVault { .storage() .instance() .get(&DataKeyExt::Risk(RiskExtKey::LastPx)); - let last_price = Self::last_oracle_price(&env); oracle::OracleValidator::validate_price_data( &env, &price_data, @@ -1389,12 +1388,6 @@ impl YieldVault { Some(oracle::MAX_PRICE_DEVIATION_BPS), last.as_ref(), ) - .expect("OracleValidationFailed"); - env.storage() - .instance() - .set(&DataKeyExt::Risk(RiskExtKey::LastPx), &price_data); - last_price.as_ref(), - ) .map_err(|_| VaultError::OracleValidationFailed)?; Self::set_last_oracle_price(&env, &price_data); } @@ -1573,9 +1566,7 @@ impl YieldVault { let mut perf_fee_amount: i128 = 0; if perf_enabled && harvested > 0 { let current_watermark = Self::strategy_watermark(env.clone(), strategy.clone()); - let yield_above_hwm = harvested - .checked_sub(current_watermark) - .unwrap_or(0); + let yield_above_hwm = harvested.checked_sub(current_watermark).unwrap_or(0); if yield_above_hwm > 0 { let perf_fee_bps: i128 = env .storage() @@ -1604,18 +1595,20 @@ impl YieldVault { } } - let net_harvested = harvested - .checked_sub(perf_fee_amount) - .unwrap_or(0); + let net_harvested = harvested.checked_sub(perf_fee_amount).unwrap_or(0); let mut state = Self::get_state(&env); let pre_total_assets = state.total_assets; - let new_total_assets = pre_total_assets.checked_add(net_harvested).expect("overflow"); + let new_total_assets = pre_total_assets + .checked_add(net_harvested) + .expect("overflow"); state.total_assets = new_total_assets; env.storage().instance().set(&DataKey::State, &state); - env.events() - .publish((symbol_short!("k_yield"),), (net_harvested, new_total_assets)); + env.events().publish( + (symbol_short!("k_yield"),), + (net_harvested, new_total_assets), + ); Ok(harvested) } @@ -1864,7 +1857,10 @@ impl YieldVault { .instance() .get(&DataKey::DaoThreshold) .unwrap_or(1); - let total_votes = proposal.yes_votes.checked_add(proposal.no_votes).expect("overflow"); + let total_votes = proposal + .yes_votes + .checked_add(proposal.no_votes) + .expect("overflow"); if total_votes < threshold { return Err(VaultError::QuorumNotReached); } @@ -3574,8 +3570,7 @@ impl YieldVault { env.storage() .instance() .set(&DataKeyExt::PerformanceFeeBps, &bps); - env.events() - .publish((symbol_short!("pperfchg"),), (bps,)); + env.events().publish((symbol_short!("pperfchg"),), (bps,)); Ok(()) } @@ -3593,17 +3588,13 @@ impl YieldVault { /// fees are transferred to this address on each yield report. /// /// Only the Admin can call this. - pub fn set_performance_incentive_pool( - env: Env, - pool: Address, - ) -> Result<(), VaultError> { + pub fn set_performance_incentive_pool(env: Env, pool: Address) -> Result<(), VaultError> { let admin: Address = get_admin(&env).expect("Admin not set"); admin.require_auth(); env.storage() .instance() .set(&DataKeyExt::PerformanceIncentivePool, &pool); - env.events() - .publish((symbol_short!("pperfpool"),), (pool,)); + env.events().publish((symbol_short!("pperfpool"),), (pool,)); Ok(()) } @@ -3621,10 +3612,7 @@ impl YieldVault { /// must be configured before enabling. /// /// Only the Admin can call this. - pub fn set_performance_fee_enabled( - env: Env, - enabled: bool, - ) -> Result<(), VaultError> { + pub fn set_performance_fee_enabled(env: Env, enabled: bool) -> Result<(), VaultError> { let admin: Address = get_admin(&env).expect("Admin not set"); admin.require_auth(); if enabled { @@ -3650,6 +3638,8 @@ impl YieldVault { .instance() .get(&DataKeyExt::PerformanceFeeEnabled) .unwrap_or(false) + } + // ── Utilization-based dynamic fee curve (Issue #1243) ──────────────────── /// Returns the configured dynamic fee curve. @@ -4575,9 +4565,7 @@ impl YieldVault { let mut perf_fee_amount: i128 = 0; if perf_enabled && net_yield > 0 { let current_watermark = Self::strategy_watermark(env.clone(), strategy.clone()); - let yield_above_hwm = net_yield - .checked_sub(current_watermark) - .unwrap_or(0); + let yield_above_hwm = net_yield.checked_sub(current_watermark).unwrap_or(0); if yield_above_hwm > 0 { let perf_fee_bps: i128 = env .storage() @@ -4837,18 +4825,18 @@ pub struct ContractMetadata { pub contract_paused: bool, pub has_strategy: bool, } - #[cfg(test)] - #[doc(hidden)] - pub fn test_seed_withdrawal_queue_entry(env: Env, user: Address, shares: i128, assets: i128) { - let tail = YieldVault::withdrawal_queue_tail(&env); - let entry = WithdrawalQueueEntry { - user, - shares, - assets, - enqueued_at: env.ledger().timestamp(), - }; - env.storage() - .instance() - .set(&DataKey::WithdrawalQueueEntry(tail), &entry); - YieldVault::set_withdrawal_queue_tail(&env, tail.checked_add(1).expect("queue overflow")); - } +#[cfg(test)] +#[doc(hidden)] +pub fn test_seed_withdrawal_queue_entry(env: Env, user: Address, shares: i128, assets: i128) { + let tail = YieldVault::withdrawal_queue_tail(&env); + let entry = WithdrawalQueueEntry { + user, + shares, + assets, + enqueued_at: env.ledger().timestamp(), + }; + env.storage() + .instance() + .set(&DataKey::WithdrawalQueueEntry(tail), &entry); + YieldVault::set_withdrawal_queue_tail(&env, tail.checked_add(1).expect("queue overflow")); +}