Extra time elapsed in the function _previewAccrueInterest
may cause inaccurate calculations in other functions
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.