sherlock-2025-07-mellow-m04

[M-04] Protocol Fee Exponential Compounding in ShareModule.handleReport

보고서

Summary

ShareModule.handleReport 함수 내 프로토콜 수수료 계산은 선형 누적이 아닌 지수적 복리로 인해 문제가 발생합니다. handleReport가 빈번하게 호출될 경우(예: 매일), 프로토콜 수수료가 이전에 누적된 모든 수수료를 포함한 현재 총 지분에 적용되어 의도보다 높은 수수료 추출이 발생합니다.

Keyword

fee, logic flaw, arithmetic error

Vulnerability

새로운 가격으로 업데이트할 때마다 수수료를 정산한다. 연간 전체 발행된 share의 n%가 수수료로 발행되도록 한다. 이를 계산하기 위해 totalShare의 n%를 계산하고, 이를 초당 발급양으로 변환한다. 그런데 totalShare에는 이전에 수수료로 발행된 share도 포함된다. 이는 즉 수수료가 선형적으로 누적되는 것이 아닌, 기하급수적인 복리로 누적됨을 의미한다.

function handleReport(address asset, uint224 priceD18, uint32 depositTimestamp, uint32 redeemTimestamp)
    external
    nonReentrant
{
    ShareModuleStorage storage $ = _shareModuleStorage();
    if (_msgSender() != $.oracle) {
        revert Forbidden();
    }
    IShareManager shareManager_ = IShareManager($.shareManager);
    IFeeManager feeManager_ = IFeeManager($.feeManager);
@>  uint256 fees = feeManager_.calculateFee(address(this), asset, priceD18, shareManager_.totalShares());
    if (fees != 0) {
        shareManager_.mint(feeManager_.feeRecipient(), fees);
    }
    feeManager_.updateState(asset, priceD18);
    ...
}
 
// FeeManager
function calculateFee(address vault, address asset, uint256 priceD18, uint256 totalShares) external view returns (uint256) {
    // ... other fee calculations ...
@>  uint256 protocolFee = (totalShares * protocolFeeRate * timeElapsed) / (365 days * 1e6);
    return protocolFee;
}

예를 들어 볼트에 연 5%가 프로토콜 수수료로 발행되도록 설정했다고 하자. 하루에 한번씩 가격을 업데이트하고 프로토콜 수수료가 정산된다고 하자. 매일 수수료를 계산할 때, 이전에 발행된 수수료가 totalShares에 포함된다. 따라서 수수료는 매일 증가하며, 이는 복리 효과를 가져온다. 365일 후 받은 수수료를 확인해보면 실제로 받은 수수료는 5%가 아니라 총 5.11%가 된다.

Impact

의도한 것보다 높은 수수료를 징수한다. 수수료가 복리로 누적된다.

Mitigation

수수료를 계산할 때, 이전에 수수료로 발행된 share를 제외한다.

Memo

수정안이 좀 부족한 것 같다. 수정안에서는 수수료 수신자의 share를 totalShare에서 뺀 만큼을 기준으로 수수료를 발행한다. 그런데 수수료 수신자가 share를 출금(소각)하거나 이동하면 틀어지게 된다. 수수료 수신자가 토큰을 이동할 수 없도록 막아야 한다. 또한 수수료 수신자 설정이 변경될 시 기존 수수료 share를 이동해야 한다. 수수료 수신자는 수수료 외의 토큰을 수취할 수도 없어야 한다. 수수료 수신자가 trusted party로 여겨져 단순히 수수료 수신자 소유의 share만큼 제외하는 것으로 수정되었다.


tags: bughunting, mellow, smart contract, solidity, severity medium, fee, logic flaw, arithmetic error