The yield reserve is critical to protocol solvency. This document identifies attack vectors targeting reserve depletion and mitigation strategies.
Description: Attacker manipulates yield rate to exceed reserve capacity.
Preconditions:
- Attacker controls admin multisig (compromised key)
- Yield rate set to unsustainable level (e.g., 50% instead of 2%)
Attack Flow:
- Attacker calls
update_config()withyield_bps = 50000(500%) - Borrowers request loans
- On repayment, yield payout exceeds reserve balance
- Contract panics with
InsufficientFunds - Protocol halts
Impact:
- Denial of service (contract paused)
- Vouchers cannot receive yield
- Borrowers cannot repay
- Reputation damage
Likelihood: Low (requires admin compromise)
Description: Contract disburses loans without verifying yield reserve sufficiency.
Preconditions:
- Reserve balance < (loan_amount * (1 + yield_bps/10000))
- No pre-disbursement reserve check
Attack Flow:
- Attacker requests large loan
- Contract disburses without checking reserve
- Reserve depleted
- Future repayments fail
- Protocol becomes insolvent
Impact:
- Protocol insolvency
- Vouchers lose yield
- Borrowers cannot repay
Likelihood: Medium (if reserve checks not implemented)
Description: Attacker coordinates defaults to drain slash treasury, then exploits reserve.
Preconditions:
- Attacker controls multiple borrower accounts
- Attacker controls voucher accounts
- Slash treasury used to replenish yield reserve
Attack Flow:
- Attacker vouches for own borrower accounts
- Requests large loans
- Defaults intentionally
- Slash treasury accumulates slashed funds
- Attacker withdraws slash treasury
- Yield reserve depleted for future loans
Impact:
- Yield reserve depletion
- Protocol insolvency
- Loss of funds for legitimate vouchers
Likelihood: Low (requires multiple account control)
Description: Attacker creates many small vouches to accumulate yield through rounding errors.
Preconditions:
- Yield calculation uses integer division
- Minimum stake < 50 stroops (current minimum)
Attack Flow:
- Attacker creates 1000 vouches of 1 stroop each
- Loan repaid with 2% yield
- Each vouch:
1 * 200 / 10000 = 0(rounds down) - Attacker receives 0 yield but protocol owes 20 stroops
- Repeated across many loans drains reserve
Impact:
- Yield reserve depletion through accumulated rounding errors
- Legitimate vouchers receive no yield
Likelihood: Low (minimum stake enforced at 50 stroops)
Implementation:
fn request_loan(...) {
// Calculate required reserve
let required_reserve = amount + (amount * yield_bps / 10_000);
// Check reserve before disbursement
let current_reserve = get_yield_reserve();
assert!(current_reserve >= required_reserve, "InsufficientFunds");
// Disburse loan
transfer_to_borrower(amount);
}Effectiveness: Prevents loans when reserve insufficient
Operational Impact: May reject valid loans if reserve low
Implementation:
const MAX_YIELD_BPS: i128 = 1000; // 10% max
fn update_config(yield_bps: i128) {
assert!(yield_bps <= MAX_YIELD_BPS, "InvalidYield");
}Effectiveness: Prevents unsustainable yield rates
Operational Impact: Limits protocol flexibility
Implementation:
- Prometheus metric:
qc_yield_reserve_balance - Alert when reserve < 110% of max loan amount
- Alert when reserve < 10% of total loan volume
Effectiveness: Early warning of reserve depletion
Operational Impact: Requires active monitoring
Implementation:
const MIN_STAKE_FOR_YIELD: i128 = 50; // stroops
fn vouch(stake: i128) {
assert!(stake >= MIN_STAKE_FOR_YIELD, "MinStakeNotMet");
}Effectiveness: Prevents rounding errors from small stakes
Operational Impact: Minimum stake requirement
Implementation:
- All config changes require
admin_thresholdsignatures - Prevents single key compromise from changing yield rate
- Requires 2-of-3 or 3-of-5 multisig
Effectiveness: Prevents unilateral yield manipulation
Operational Impact: Slower config changes
Implementation:
- Admin-only function to transfer XLM to contract
- Requires multisig approval
- Logged and auditable
fn replenish_reserve(admin_signers: Vec<Address>, amount: i128) {
require_admin_approval(admin_signers);
transfer_from_admin(amount);
}Effectiveness: Allows reserve recovery
Operational Impact: Requires admin action
# Check reserve balance
stellar contract invoke \
--id $CONTRACT_ID \
--fn get_fee_treasury \
--network mainnet
# Calculate reserve health
# reserve_health = reserve / (max_loan_amount * 1.02)
# Alert if < 1.1 (110%)- Review loan volume trends
- Check yield distribution
- Verify no unusual defaults
- Audit admin actions
- Replenish reserve if needed
- Review yield rate sustainability
- Update monitoring thresholds
- Audit slash treasury
Required Reserve = (Max Concurrent Loans) × (Max Loan Amount) × (1 + Yield Rate)
Example:
- Max concurrent loans: 100
- Max loan amount: 1000 XLM
- Yield rate: 2%
- Required reserve: 100 × 1000 × 1.02 = 102,000 XLM
- Recommended buffer: 110% = 112,200 XLM
Monitor these metrics for anomalies:
| Metric | Normal Range | Alert Threshold |
|---|---|---|
| Reserve balance | > 110% required | < 110% required |
| Yield payout rate | 2% of repayments | > 5% of repayments |
| Default rate | < 5% | > 10% |
| Loan volume | Steady growth | > 50% spike |
def detect_reserve_drain():
"""Detect unusual reserve depletion patterns"""
# Get reserve history
reserve_history = get_reserve_history(days=7)
# Calculate daily change
daily_changes = [
reserve_history[i] - reserve_history[i-1]
for i in range(1, len(reserve_history))
]
# Alert if > 20% daily decrease
for change in daily_changes:
if change < -0.2 * reserve_history[0]:
alert("Unusual reserve depletion detected")-
Immediate (< 5 min):
- Pause contract
- Alert ops team
- Notify stakeholders
-
Short-term (< 1 hour):
- Investigate cause
- Review recent transactions
- Check admin logs
-
Medium-term (< 24 hours):
- Replenish reserve
- Audit all loans
- Verify yield calculations
-
Long-term (< 1 week):
- Root cause analysis
- Update monitoring
- Implement additional safeguards
INCIDENT: Yield Reserve Depletion
SEVERITY: Critical
TIME: [timestamp]
DURATION: [duration]
IMPACT:
- Repayment transactions failing
- Vouchers cannot receive yield
- Protocol paused
CAUSE: [root cause]
RESOLUTION:
- Reserve replenished with [amount] XLM
- Contract unpaused at [time]
PREVENTION:
- [mitigation implemented]
#[test]
fn test_reserve_depletion_protection() {
// Setup: Create contract with 100 XLM reserve
let reserve = 100_000_000_000i128; // 100 XLM
// Attempt to request loan > reserve
let loan_amount = 150_000_000_000i128; // 150 XLM
// Should fail with InsufficientFunds
assert_eq!(
request_loan(borrower, loan_amount, threshold, token),
Err(ContractError::InsufficientFunds)
);
}#[test]
fn fuzz_yield_calculation() {
for stake in 1..1_000_000_000 {
let yield_amount = (stake * 200) / 10_000;
// Verify yield never exceeds 2%
assert!(yield_amount <= (stake * 2) / 100);
// Verify no negative yields
assert!(yield_amount >= 0);
}
}