The feesUpdatedAt
state variable is used before initialization
Description
The takeFees
modifier in the ConcreteMultiStrategyVault contract calculates protocol fees using the feesUpdatedAt
state variable and updates it after accruing fees:
modifier takeFees() {
if (!paused()) {
uint256 totalFee = accruedProtocolFee() + accruedPerformanceFee();
uint256 shareValue = convertToAssets(1e18);
uint256 _totalAssets = totalAssets();
if (shareValue > highWaterMark) highWaterMark = shareValue;
if (totalFee > 0 && _totalAssets > 0) {
uint256 supply = totalSupply();
uint256 feeInShare =
supply == 0 ? totalFee : totalFee.mulDiv(supply, _totalAssets - totalFee, Math.Rounding.Floor);
_mint(feeRecipient, feeInShare);
feesUpdatedAt = block.timestamp;
}
}
_;
}
The deposit
and mint
functions call _validateAndUpdateDepositTimestamps
to initialize feesUpdatedAt
to block.timestamp
during the first deposit or mint:
function _validateAndUpdateDepositTimestamps(address receiver_) private {
// [...]
if (totalSupply() == 0) feesUpdatedAt = block.timestamp;
// [...]
}
However, the takeFees
modifier executes before _validateAndUpdateDepositTimestamps
within both the deposit
and mint
functions:
function deposit(uint256 assets_, address receiver_)
public
override
nonReentrant
whenNotPaused
takeFees
returns (uint256 shares)
{
_validateAndUpdateDepositTimestamps(receiver_);
// [...]
}
function mint(uint256 shares_, address receiver_)
public
override
nonReentrant
whenNotPaused
takeFees
returns (uint256 assets)
{
_validateAndUpdateDepositTimestamps(receiver_);
// [...]
}
As a result, when takeFees
runs for the first time, feesUpdatedAt
remains uninitialized (i.e., zero). This causes accruedProtocolFee
to treat the entire block.timestamp
as elapsed time, resulting in protocol fees being calculated even though no time has actually elapsed for fee accrual. If the contract holds asset tokens at this point, the _mint(feeRecipient, feeInShare)
call issues these unearned shares to the feeRecipient
.
Impact
The feeRecipient
can receive unearned shares during the first deposit or mint if the vault already holds asset tokens. This may result in an incorrect allocation of protocol fees and a dilution of other users' shares.
Recommendations
Consider refactoring _validateAndUpdateDepositTimestamps
into a modifier, and ensure it executes before the takeFees
modifier in the deposit
and mint
functions. This guarantees that feesUpdatedAt
is properly initialized before any fee calculation or accrual logic runs.
Remediation
This issue has been acknowledged by Blueprint Finance, and a fix was implemented in commit 4f5b9785↗.