OpenEden Disclosed Report

Service Fee Claim Causes TBILL-USDC Depeg → DoS in `_deposit`

Company
Created date
Jul 22 2025

Target

https://github.com/OpenEdenHQ/openeden.vault.audit/tree/d18288e944df21729b18d430b2afec2da99b6287

Vulnerability Details

Summary

The claimServiceFee() function allows the protocol to claim unClaimedFee denominated in the underlying token (e.g., USDC). However, these underlying tokens are the exact assets backing TBILLs, and are used in tbillUsdcRate() to calculate the TBILL/USDC exchange rate. As such, claiming any portion of the underlying assets — even a tiny fee — causes the protocol to become undercollateralized.

This directly breaks the tbillUsdcRate() function, which then reverts due to this undercollateralization. Since _deposit(), updateEpoch(), processWithdrawalQueue(), redeem() etc (basically every major function) relies on tbillUsdcRate() to function, this leads to a denial of service in the entire protocol.


Root Cause

In claimServiceFee(), unClaimedFee is transferred out of the contract:

function claimServiceFee() external onlyRole(SERVICE_FEE_CLAIMER_ROLE) {
    uint256 feeToClaim = unClaimedFee;
    if (feeToClaim == 0) revert NoFeeToClaim();

    unClaimedFee = 0;
    IERC20MetadataUpgradeable(underlying).safeTransfer(msg.sender, feeToClaim);
    emit ServiceFeeClaimed(msg.sender, feeToClaim);
}

This function transfers funds from the contract’s underlying token balance, which is supposed to fully back the TBILLs in existence. Removing any of it causes:

  • a slight depeg, meaning the value of all TBILLs is no longer backed by 1:1 USDC.
  • tbillUsdcRate() to revert due to failing price sanity checks.
function tbillUsdcRate() public view returns (uint256) {
    // ...
    int256 answer = getTBillOracleAnswer();
    if (answer < 0 || tbillUsdPrice < ONE) revert TBillInvalidPrice(answer);
    // ...
}

The answer (representing TBILL's USD price) becomes < 1e8 when undercollateralized due to fee withdrawal, causing an immediate revert.


Impact

  • Total loss of usability for _deposit(): it calls tbillUsdcRate() and will revert forever once unClaimedFee is claimed.
  • Prevents new users from joining the protocol.
  • All minting of TBILL is disabled.
  • Affects user trust and economic integrity of the protocol.

Code Flow Evidence

1. updateEpoch() computes unClaimedFee from TVL:

uint256 serviceFee = (tvl * serviceFeeRate * epochLength) / (365 days * FEE_DENOMINATOR);
unClaimedFee += serviceFee;

It is directly proportional to the tvlbut it is not deducted at this point — so totalAssets remains unchanged until claimServiceFee() is called.

2. TBILL rate becomes invalid post-claim:

function tbillUsdcRate() public view returns (uint256) {
    int256 answer = getTBillOracleAnswer(); // uses backing (i.e., underlying tokens)
    if (answer < 0 || tbillUsdPrice < ONE) revert TBillInvalidPrice(answer); 
}

Once unClaimedFee is removed from underlying, the price drops below 1e8.

3. _deposit() becomes permanently broken:

function _deposit(...) internal override returns (uint256) {
    uint256 rate = tbillUsdcRate(); //  will revert
    ...
}

No user can deposit anymore.


🛠 Suggested Fix

Do not claim fees from the underlying token balance.

Option 1: Mint TBILLs to fee claimer instead

Instead of pulling funds from underlying, mint TBILLs equivalent to the fee amount:

function claimServiceFee() external onlyRole(SERVICE_FEE_CLAIMER_ROLE) {
    uint256 feeToClaim = unClaimedFee;
    if (feeToClaim == 0) revert NoFeeToClaim();

    unClaimedFee = 0;
    _mint(msg.sender, feeToClaim); // mint TBILLs instead
    emit ServiceFeeClaimed(msg.sender, feeToClaim);
}

This preserves full backing of TBILLs by underlying assets, while still compensating the service provider.

Option 2: Accrue fees off-chain or from yield only

Only allow unClaimedFee to be claimable from yield or interest earned, not principal. That way, collateral remains intact.


Conclusion

This bug fundamentally breaks core functionality of the protocol (_deposit) and puts the entire collateralization model at risk. It stems from misunderstanding the dual role of underlying as both collateral and claimable fee source.

Severity: Critical

Impact: Denial of Service, Protocol Integrity Loss


🔍 References

  • claimServiceFee()
  • updateEpoch()
  • tbillUsdcRate()
  • _deposit()

Validation steps

soon...

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
High
Bounty$500
Visibilitypartially
VulnerabilityBlockchain
Participants (4)
author
manager
triage team