sherlock-2025-07-mellow-h06

[H-06] Redeems through RedeemQueue avoid paying management and performance fee

보고서

Summary

출금 요청 시 share는 바로 소각되지만 출금액은 대기 후 나중에 받을 수 있다. 출금액이 볼트에 남아있는 동안에는 프로토콜/관리 수수료 징수 대상이지만 share가 이미 소각되어 제외되었다.

Keyword

fee, logic flaw

Vulnerability

유저가 출금을 요청하면 이에 해당하는 share가 즉시 소각되고 activeShares에서 제거된다. 출금될 토큰은 즉시 전송되지 않고, 오라클이 업데이트되고 출금 작업이 완료될 때까지 볼트에 남아있다. 따라서(볼트에 남아있으므로) 출금 대상 토큰은 프로토콜/관리 수수료 징수 대상이어야 한다.

function redeem(uint256 shares) external nonReentrant {
    ...
    IShareManager shareManager_ = IShareManager(IShareModule(vault_).shareManager());
@>  shareManager_.burn(caller, shares);
    {
        IFeeManager feeManager = IShareModule(vault_).feeManager();
        uint256 fees = feeManager.calculateRedeemFee(shares);
        if (fees > 0) {
            shareManager_.mint(feeManager.feeRecipient(), fees);
            shares -= fees;
        }
    }
    ...
}

하지만 수수료를 계산할 때는 현재 발행된, 그리고 입금하여 발행 대기중인 share의 합을 기준으로 계산한다. 출금 대기중인 share는 포함되지 않는다. 발행되지 않았지만 대기중인 share가 포함된다는 점을 고려할 때, 소각되었으나 아직 완전히 출금 처리되지 않은 share도 반영되는 것이 맞다.

// ShareModule.handleReport
function handleReport(address asset, uint224 priceD18, uint32 depositTimestamp, uint32 redeemTimestamp)
    external
    nonReentrant
{
    ...
@>  uint256 fees = feeManager_.calculateFee(address(this), asset, priceD18, shareManager_.totalShares());
    if (fees != 0) {
        shareManager_.mint(feeManager_.feeRecipient(), fees);
    }
    ...
}
 
// # ShareManager.totalShares
function totalShares() public view returns (uint256) {
@>  return _shareManagerStorage().allocatedShares + activeShares();
}
 
// FeeManager.calculateFee
function calculateFee(address vault, address asset, uint256 priceD18, uint256 totalShares)
    public
    view
    returns (uint256 shares)
{
    FeeManagerStorage storage $ = _feeManagerStorage();
    if (asset == $.baseAsset[vault]) {
        uint256 minPriceD18_ = $.minPriceD18[vault];
        if (priceD18 < minPriceD18_ && minPriceD18_ != 0) {
            shares = Math.mulDiv(minPriceD18_ - priceD18, $.performanceFeeD6 * totalShares, 1e24);
        }
    }
    uint256 timestamp = $.timestamps[vault];
    if (timestamp != 0 && block.timestamp > timestamp) {
        shares += Math.mulDiv(totalShares, $.protocolFeeD6 * (block.timestamp - timestamp), 365e6 days);
    }
}

Impact

수수료를 덜 받는다.

Mitigation

실제로 출금이 완료될 때까지 share를 소각하지 않는다. share 토큰을 컨트랙트에 넣어둔 후 handleBatches로 출금 토큰을 볼트에서 꺼내올 때까지는 출금 대기 share에게서도 수수료를 떼도록 한다.


tags: bughunting, mellow, smart contract, solidity, severity high, fee, logic flaw