https://github.com/OpenEdenHQ/openeden.usdoexpress.audit/tree/f3f31d2ac15e3253cba342229f9d05495f95d6fd
Contract: LiquidityController.sol
Location: https://github.com/OpenEdenHQ/openeden.usdoexpress.audit/blob/main/contracts/extensions/redemption/LiquidityController.sol#L109-L111
Severity: CRITICAL
Type: Phantom Liquidity Creation / Over-consumption
Any removal of a user's quota (setting userQuota to 0) creates phantom liquidity by incorrectly decrementing the totalUsed counter. This breaks the fundamental accounting invariant and allows the system to allocate more liquidity than physically available.
When removing a user's quota via setUserQuota(user, 0), the contract incorrectly decrements totalUsed by the user's usedQuota amount:
// LiquidityController.sol, lines 106-113
} else {
// Removing quota - also clear used quota
authorizedUsers[user] = false;
if (usedQuota > 0) {
totalUsed = totalUsed - usedQuota; // ❌ BUG HERE
usedQuotas[user] = 0;
}
}
The Problem: The code assumes that used liquidity "returns" to the pool when a quota is removed. However, this liquidity was already physically spent through UsycRedemption contract and does NOT return to the pool.
totalLiquidity limitDay 1: 1 user removed → 50 USYC phantom liquidity
Day 2: 2 users removed → 100 USYC phantom liquidity
Day 3: 3 users removed → 150 USYC phantom liquidity
...
Month: 30 users removed → 1,500 USYC phantom liquidity 💸
Current problematic code:
} else {
// Removing quota - also clear used quota
authorizedUsers[user] = false;
if (usedQuota > 0) {
totalUsed = totalUsed - usedQuota; // ❌ REMOVE THIS LINE
usedQuotas[user] = 0;
}
}
Fixed code:
} else {
// Removing quota - DON'T automatically restore used quota
authorizedUsers[user] = false;
// Keep usedQuota in totalUsed - it was really consumed!
// Owner must manually call restoreLiquidity() if appropriate
usedQuotas[user] = 0; // Clear user's record only
}
Key principle: totalUsed should always reflect really spent liquidity, not just sum of current usedQuotas of active users.
Description: User consumes only part of their quota before removal. This is the most realistic scenario as users rarely use 100% of allocated quotas.
Test: test_CRITICAL_PartialUsage_StillVulnerable()
File: LiquidityControllerAccountingBug.t.sol
Setup:
totalLiquidity = 1,000 USYC
Alice quota = 100, Bob quota = 60
totalAllocated = 160, totalUsed = 0
Step 1: Alice uses 50 USYC (partial)
reserveLiquidity(Alice, 50)
totalUsed = 50
Physical: 50 USYC GONE (converted to USDC)
Step 2: Admin removes Alice (BUG)
setUserQuota(Alice, 0)
Code: totalUsed = 50 - 50 = 0 ❌
Contract thinks: 50 USYC "returned"
Reality: 50 USYC still GONE
Step 3: Admin adds Carol
setUserQuota(Carol, 940)
Check: 60 + 940 ≤ 1,000 ✅ PASSES
totalAllocated = 1,000
Step 4: PROBLEM
Carol needs: 940 USYC
Available: 890 USYC (1K - 50 Alice - 60 Bob)
SHORTAGE: 50 USYC ❌
Result:
Admin can add to Carol: 940 ❌ WRONG
Admin only can add: 890 ✅ CORRECT
But checks passed ❌
BUG: totalUsed incorrectly decremented on deletion
RESULT: 50 USYC phantom liquidity created
This scenario involves zero admin mistakes. Every action is standard operational procedure:
Admin is doing exactly what the system appears to allow and what makes business sense, but the system silently creates phantom liquidity during this normal flow.
File: test/LiquidityControllerAccountingBug.t.sol
Test Function: test_CRITICAL_PartialUsage_StillVulnerable()
Run Test:
forge test --match-test test_CRITICAL_PartialUsage_StillVulnerable -vv
Expected Output:
[PASS] test_CRITICAL_PartialUsage_StillVulnerable()
Contract thinks: totalUsed = 1,000 USYC ✅
Reality: 1,050 USYC consumed ❌
EXCESS: 50 USYC (5%)
This vulnerability demonstrates how any user quota deletion creates phantom liquidity by incorrectly decrementing the totalUsed counter. The simple example above shows:
The bug is inevitable in any system with user quota management and rotation, making it a critical design flaw rather than an edge case.