<!-- canonical: https://0xsimao.com/findings/aegis-attacker-user-withdrawals-cost -->

# Attacker can DoS user withdrawals at no cost

Crit/High · Sherlock · Bitcoin-backed stablecoin · 22nd April, 2025

Finding H-1 of the Aegis Staked YUSD security review.

- Protocol: https://www.aegisweb3.com/
- Report: /reports/aegis
- Source: https://github.com/sherlock-protocol/sherlock-reports/blob/main/audits/2025.04.26%20-%20Final%20-%20Aegis%20Collaborative%20Audit%20Report.pdf

---

## Summary

Everytime a user deposits or mints, a lockup element is added to an array. An attacker can deposit on behalf of other user and fill the array with excessive elements such that it is DoSed due to OOG. 

## Vulnerability Detail

`sYUSD::deposit()` is as follows:
```solidity
function _deposit(
    address caller,
    address receiver,
    uint256 assets,
    uint256 shares
) internal virtual override {
    super._deposit(caller, receiver, assets, shares);
    
    // Add new locked shares entry
    userLockedShares[receiver].push(LockedShares({
        amount: shares,
        expiryTimestamp: block.timestamp + lockupPeriod
    }));
}
```

As can be seen, it pushes a new element each time it is called, via `sYUSD::deposit()` or `sYUSD::mint()`.
An attacker can deposit to another user and fill their array. Then, when the user withdraws via `sYUSD::withdraw()` or `sYUSD::redeem()`, it loops through the array all at once and DoSes withdrawals due to OOG.

```solidity
function _withdraw(
    address caller,
    address receiver,
    address owner,
    uint256 assets,
    uint256 shares
) internal virtual override {
    // First update the unlocked shares
    updateUnlockedShares(owner);
    
    // Ensure user has enough unlocked shares
    if (unlockedShares[owner] < shares) {
        revert InsufficientUnlockedShares(shares, unlockedShares[owner]);
    }
    
    // Reduce unlocked shares
    unlockedShares[owner] -= shares;
    
    super._withdraw(caller, receiver, owner, assets, shares);
}

...

function updateUnlockedShares(address user) public {
    LockedShares[] storage userLocks = userLockedShares[user];
    
    for (uint256 i = 0; i < userLocks.length; i++) {
        if (block.timestamp >= userLocks[i].expiryTimestamp && userLocks[i].amount > 0) {
            unlockedShares[user] += userLocks[i].amount;
            userLocks[i].amount = 0;
        }
    }
    
    // Clean up empty entries
    _cleanupLockedShares(user);
}
```

Please find a POC [here](https://github.com/sherlock-audit/2025-04-aegis-staked-yusd/blob/foundry-tests/aegis-contracts/test/SYUSD.t.sol#L403).

## Impact

Stuck funds at no cost for an attacker.

## Code Snippet

https://github.com/sherlock-audit/2025-04-aegis-staked-yusd/blob/main/aegis-contracts/contracts/sYUSD.sol#L80-L92

## Tool Used

Manual Review

## Recommendation

Set a max iterations argument.

---

Related findings:

- [An attacker may DoS user Fluid balance increases by frontrunning `FluidLocker::claim()` calls and calling `EP_PROGRAM_MANAGER::batchUpdateUserUnits()` directly](https://0xsimao.com/findings/superfluid-locker-system-increases-frontrunning-program-directly): Superfluid Locker System
- [`VaultPoolLib::reserve()` will store the `Pa` not attributed to user withdrawals incorrectly and leave in untracked once it expires again](https://0xsimao.com/findings/cork-protocol-attributed-withdrawals-untracked-expires): Cork Protocol
- [User may be requesting/fulfilling withdrawals at better rates than the real share/asset ratio](https://0xsimao.com/findings/yieldoor-iii-requesting-fulfilling-withdrawals-share): Yieldoor LoopedVault Update
