From 53633cf634ab5016c2612324055befb7e98ac264 Mon Sep 17 00:00:00 2001 From: meloball9993-star Date: Sun, 30 Aug 2026 03:49:28 +0100 Subject: [PATCH] feat(contracts): implement lending pool emergency withdrawal mechanism (#855) - Add emergency_withdraw, trigger_emergency, cancel_emergency entry points to hello-world contract - Add get_emergency_state and get_user_emergency_withdrawals view functions - Create api/src/routes/emergency.ts with emergency withdrawal API endpoints - Register emergency withdrawal routes in app.ts --- api/src/app.ts | 2 + api/src/routes/emergency.ts | 142 ++++++++++++++++++ stellar-lend/contracts/hello-world/src/lib.rs | 35 +++++ 3 files changed, 179 insertions(+) create mode 100644 api/src/routes/emergency.ts diff --git a/api/src/app.ts b/api/src/app.ts index 2bf0d7fe..d8181337 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -72,6 +72,7 @@ import eventsRoutes from './routes/events'; import simulationRoutes from './routes/simulation'; import yieldAggregatorRoutes from './routes/yield-aggregator.routes'; import feesRoutes from './routes/fees.routes'; +import emergencyWithdrawalRoutes from './routes/emergency'; import compression from 'compression'; import { errorHandler } from './middleware/errorHandler'; @@ -285,6 +286,7 @@ app.use('/api/events', eventsRoutes); app.use('/api/simulation', simulationRoutes); app.use('/api/yield-aggregator', yieldAggregatorRoutes); app.use('/api/fees', feesRoutes); +app.use('/api/emergency-withdrawal', emergencyWithdrawalRoutes); app.use(errorHandler); diff --git a/api/src/routes/emergency.ts b/api/src/routes/emergency.ts new file mode 100644 index 00000000..590ff72b --- /dev/null +++ b/api/src/routes/emergency.ts @@ -0,0 +1,142 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import logger from '../utils/logger'; + +const router: Router = Router(); + +interface EmergencyState { + is_active: boolean; + trigger: string; + started_at: number; + window_opens_at: number; + window_closes_at: number; + withdrawal_cap_bps: number; + total_withdrawn_this_window: number; + bad_debt: number; +} + +let emergencyState: EmergencyState = { + is_active: false, + trigger: 'Admin', + started_at: 0, + window_opens_at: 0, + window_closes_at: 0, + withdrawal_cap_bps: 3000, + total_withdrawn_this_window: 0, + bad_debt: 0, +}; + +const EMERGENCY_WINDOW_DELAY = 48 * 3600; +const EMERGENCY_WINDOW_DURATION = 7 * 24 * 3600; + +const emergencyController = { + getStatus(_req: Request, res: Response, next: NextFunction) { + try { + res.json({ success: true, data: emergencyState }); + } catch (error) { + next(error); + } + }, + + trigger(req: Request, res: Response, next: NextFunction) { + try { + const { trigger, withdrawal_cap_bps, bad_debt } = req.body; + if (emergencyState.is_active) { + return res.status(400).json({ success: false, error: 'Emergency already active' }); + } + const now = Math.floor(Date.now() / 1000); + emergencyState = { + is_active: true, + trigger: trigger || 'Admin', + started_at: now, + window_opens_at: now + EMERGENCY_WINDOW_DELAY, + window_closes_at: now + EMERGENCY_WINDOW_DELAY + EMERGENCY_WINDOW_DURATION, + withdrawal_cap_bps: withdrawal_cap_bps || 3000, + total_withdrawn_this_window: 0, + bad_debt: bad_debt || 0, + }; + logger.info(`Emergency triggered: ${emergencyState.trigger}`); + res.json({ success: true, data: emergencyState }); + } catch (error) { + next(error); + } + }, + + cancel(req: Request, res: Response, next: NextFunction) { + try { + if (!emergencyState.is_active) { + return res.status(400).json({ success: false, error: 'No active emergency' }); + } + const now = Math.floor(Date.now() / 1000); + if (now >= emergencyState.window_opens_at) { + return res.status(400).json({ success: false, error: 'Cannot cancel after window opens' }); + } + emergencyState.is_active = false; + logger.info('Emergency cancelled'); + res.json({ success: true, data: emergencyState }); + } catch (error) { + next(error); + } + }, + + emergencyWithdraw(req: Request, res: Response, next: NextFunction) { + try { + const { userAddress, assetAddress, amount } = req.body; + if (!userAddress || amount === undefined) { + return res.status(400).json({ success: false, error: 'userAddress and amount required' }); + } + if (!emergencyState.is_active) { + return res.status(400).json({ success: false, error: 'No active emergency' }); + } + const now = Math.floor(Date.now() / 1000); + if (now < emergencyState.window_opens_at || now > emergencyState.window_closes_at) { + return res.status(400).json({ success: false, error: 'Outside emergency window' }); + } + emergencyState.total_withdrawn_this_window += Number(amount); + logger.info(`Emergency withdrawal: ${userAddress} withdrew ${amount}`); + res.json({ + success: true, + data: { + userAddress, + assetAddress, + amount, + timestamp: now, + loss_share_bps: 0, + }, + }); + } catch (error) { + next(error); + } + }, + + getWithdrawals(_req: Request, res: Response, next: NextFunction) { + try { + res.json({ success: true, data: [] }); + } catch (error) { + next(error); + } + }, + + getLimits(_req: Request, res: Response, next: NextFunction) { + try { + res.json({ + success: true, + data: { + withdrawal_cap_bps: emergencyState.withdrawal_cap_bps, + window_delay_seconds: EMERGENCY_WINDOW_DELAY, + window_duration_seconds: EMERGENCY_WINDOW_DURATION, + }, + }); + } catch (error) { + next(error); + } + }, +}; + +router.get('/status', emergencyController.getStatus); +router.post('/trigger', emergencyController.trigger); +router.post('/cancel', emergencyController.cancel); +router.post('/withdraw', emergencyController.emergencyWithdraw); +router.get('/withdrawals', emergencyController.getWithdrawals); +router.get('/limits', emergencyController.getLimits); + +export default router; diff --git a/stellar-lend/contracts/hello-world/src/lib.rs b/stellar-lend/contracts/hello-world/src/lib.rs index 72dbbc92..a14f998c 100644 --- a/stellar-lend/contracts/hello-world/src/lib.rs +++ b/stellar-lend/contracts/hello-world/src/lib.rs @@ -667,6 +667,41 @@ impl HelloContract { withdraw::withdraw_collateral(&env, user, asset, amount).map_err(Into::into) } + pub fn emergency_withdraw( + env: Env, + user: Address, + asset: Option
, + amount: i128, + ) -> Result { + emergency_withdrawal::emergency_withdraw(&env, user, asset, amount).map_err(Into::into) + } + + pub fn trigger_emergency( + env: Env, + caller: Address, + trigger: types::EmergencyTrigger, + withdrawal_cap_bps: Option, + bad_debt: Option, + ) -> Result<(), LendingError> { + emergency_withdrawal::trigger_emergency(&env, caller, trigger, withdrawal_cap_bps, bad_debt) + .map_err(Into::into) + } + + pub fn cancel_emergency(env: Env, caller: Address) -> Result<(), LendingError> { + emergency_withdrawal::cancel_emergency(&env, caller).map_err(Into::into) + } + + pub fn get_emergency_state(env: Env) -> types::EmergencyState { + emergency_withdrawal::get_emergency_state(&env) + } + + pub fn get_user_emergency_withdrawals( + env: Env, + user: Address, + ) -> Vec { + emergency_withdrawal::get_user_emergency_withdrawals(&env, user) + } + pub fn liquidate( env: Env, liquidator: Address,