Assessment reports>Avon>Low findings>Extra time elapsed in the function ,_previewAccrueInterest, may cause inaccurate calculations in other functions
Category: Business Logic

Extra time elapsed in the function _previewAccrueInterest may cause inaccurate calculations in other functions

Low Impact
Low Severity
N/A Likelihood

Description

The calculation of accrued interest depends on the time elapsed. The function _previewAccrueInterest simulates the execution of the function _accrueInterest, but for frontend convenience and to allow off-chain users to obtain a predictable state, it adds PoolConstants.QUOTE_VALID_PERIOD seconds on top of the original time elapsed.

function _previewAccrueInterest(PoolStorage.PoolState storage s) internal view returns (PoolData memory previewPool) {
    uint256 elapsed = block.timestamp + PoolConstants.QUOTE_VALID_PERIOD - s.lastUpdate;

    // [...]

    int128 scaledRate = ABDKMath64x64.divu(rate, PoolConstants.WAD);
    int128 scaledTime = ABDKMath64x64.fromUInt(elapsed);
    int128 expInput = ABDKMath64x64.mul(scaledRate, scaledTime);
    int128 expResult = ABDKMath64x64.exp(expInput); // e^(r*t)

    // Convert back to uint256 with WAD precision
    uint256 expFactor = ABDKMath64x64.mulu(expResult, PoolConstants.WAD);

    // Accrued interest = totalBorrowAssets * (e^(r*t) - 1)
    uint256 accruedInterest = s.totalBorrowAssets.mulDiv(expFactor - PoolConstants.WAD, PoolConstants.WAD);

    // [...]
}

Impact

Some functions perform their calculations based on the state returned by _previewAccrueInterest. Because the function _previewAccrueInterest increases the time elapsed by PoolConstants.QUOTE_VALID_PERIOD, these functions — which should be using AvonPool’s current state — end up using a future state, leading to inaccurate results. For example, the function withdraw of the contract AvonPool uses the function previewWithdraw to compute the corresponding number of shares, but the function previewWithdraw overridden by the contract AvonPool performs its calculation against the future state returned by _previewAccrueInterest, whereas the function withdraw should be using AvonPool’s current state.

// AvonPool
function withdraw(
    uint256 assets,
    address receiver,
    address owner
) public override whenNotPaused returns (uint256 shares) {
    // [...]
    shares = super.withdraw(assets, receiver, owner);
    // [...]
}

function previewWithdraw(uint256 assets) public view override returns (uint256) {
    PoolStorage.PoolState storage s = PoolStorage._state();
    (PoolGetter.PoolData memory previewPool) = s._previewAccrueInterest();
    return assets.mulDiv(previewPool.totalSupplyShares + 10 ** _decimalsOffset(), previewPool.totalSupplyAssets + 1, Math.Rounding.Ceil);
}

// ERC4626
function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) {
    uint256 maxAssets = maxWithdraw(owner);
    if (assets > maxAssets) {
        revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
    }

    uint256 shares = previewWithdraw(assets);
    _withdraw(_msgSender(), receiver, owner, assets, shares);

    return shares;
}

Recommendations

Consider providing wrapper functions, which could add PoolConstants.QUOTE_VALID_PERIOD to the time elapsed, for off-chain use.

Remediation

Zellic © 2025Back to top ↑