<!-- canonical: https://0xsimao.com/findings/aegis-missing-max-redeem-implementation -->

# Missing `maxRedeem()` implementation

Low/Info · Sherlock · Bitcoin-backed stablecoin · 22nd April, 2025

Finding L-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

`ERC4626` implements a `maxRedeem()` function that must specify the max amount of shares a user can redeem. Due to the locked shares property, a user can't withdraw all their shares at any time, but the `maxRedeem()` still returns their balance (inherited).

## Vulnerability Detail

When we look at `sYUSD::maxWithdraw()`, it correctly accounts for the locked shares:
```solidity
function maxWithdraw(address owner) public view virtual override returns (uint256) {
    // Calculate current unlocked amount (without state changes)
    uint256 currentUnlocked = unlockedShares[owner];
    LockedShares[] storage userLocks = userLockedShares[owner];
    
    for (uint256 i = 0; i < userLocks.length; i++) {
        if (block.timestamp >= userLocks[i].expiryTimestamp) {
            currentUnlocked += userLocks[i].amount;
        }
    }
    
    // Convert unlocked shares to assets
    return convertToAssets(currentUnlocked);
}
```
However, this is not true for `maxRedeem()`, which returns the inherited unchanged `balanceOf(user)` from `ERC4626`.

## Impact

Specification mismatch.

## Code Snippet

https://github.com/sherlock-audit/2025-04-aegis-staked-yusd/blob/foundry-tests/aegis-contracts/contracts/sYUSD.sol#L17

## Tool Used

Manual Review

## Recommendation

Implement `maxRedeem()`, for example:
```solidity
function maxRedeem(address owner) public view virtual override returns (uint256) {
    // Calculate current unlocked amount (without state changes)
    uint256 currentUnlocked = unlockedShares[owner];
    LockedShares[] storage userLocks = userLockedShares[owner];
    
    for (uint256 i = 0; i < userLocks.length; i++) {
        if (block.timestamp >= userLocks[i].expiryTimestamp) {
            currentUnlocked += userLocks[i].amount;
        }
    }

    return currentUnlocked;
}
```

---

Related findings:

- [Incorrect `ERC4626ExceededMaxRedeem` event on `ManagedLeveragedVault.sol:: cancelWithdrawalIntent()`](https://0xsimao.com/findings/beraborrow-i-erc4626-exceeded-redeem-withdrawal): Beraborrow Managed Dens
- [YieldVault maxRedeem(...) unnecessarily converts shares to assets and back to shares again](https://0xsimao.com/findings/fuji-finance-yield-redeem-converts-shares): Fuji Finance
- [LoopedVault::maxMint() and LoopedVault::maxRedeem() are not overriden](https://0xsimao.com/findings/yieldoor-iii-looped-mint-redeem-overriden): Yieldoor LoopedVault Update
- [`PreDepositVault::maxDeposit/Mint()` are missing `maxDepositLimit` as per the ERC4626 spec](https://0xsimao.com/findings/gaib-deposit-mint-erc4626-spec): GAIB Pre-Vaults
