Polystream Vault
Cantina Security Report
Organization
- @polystream
Engagement Type
Cantina Reviews
Period
-
Repositories
Researchers
Findings
High Risk
5 findings
5 fixed
0 acknowledged
Medium Risk
8 findings
8 fixed
0 acknowledged
Low Risk
10 findings
10 fixed
0 acknowledged
Informational
7 findings
6 fixed
1 acknowledged
High Risk5 findings
Users can still withdraw/deposit in case of Killswitch of accountant being triggered
State
Severity
- Severity: High
≈
Likelihood: High×
Impact: High Submitted by
J4X
Description
The codebase implements two safeguards against quick changes in the total managed funds.The first one is done in the
Accountanton theupdateExchangeRate()if (highWaterMark > 0) { uint256 deviation = newExchangeRate > highWaterMark ? newExchangeRate - highWaterMark : highWaterMark - newExchangeRate; uint256 deviationPercentage = deviation.mulDiv(1e18, highWaterMark, Math.Rounding.Ceil); if (deviationPercentage > maxDeviation) { _pause(); emit EmergencyPaused(deviationPercentage, newExchangeRate); return; // Exit early without updating state }}If the exchange rate (totalAssets/totalShares) changes too significantly, the
Accountantwill pause and break.The second safeguard is in the
HubVaultupdateUnderlyingBalancefunction itself. It will trigger if the asset balance changes too significantly:uint256 percentageChange = _calculatePercentageChange(lastPricePerShare, newExchangeRate); if (percentageChange > maxPercentageChange) { _pause();return;}This will never be reached, as the accountant's pause will return before
updateUnderlyingBalance()is ever called.Recommendation
We recommend also pausing the vault in this case
Withdrawals will get stuck due to OOG DOS
State
Severity
- Severity: High
Submitted by
J4X
Description
Every time a user creates a withdrawal their
nextRequestIdgets incremented.// Get next request ID for userrequestId = nextRequestId[msg.sender];nextRequestId[msg.sender]++;So a user (or a bot interacting with the protocol frequently) with many interactions will have a significantly high
nextRequestId. This will lead to a problem as the program will call the following at the end of finalizing a withdrawal:// Remove user from pending queue only if they have no other active requestsif (!_hasActiveRequests(user)) { pendingUsers.remove(user);}The
_hasActiveRequests()function then works as follows:function _hasActiveRequests(address user) internal view returns (bool hasActive) { // Check if user has any requests from ID 0 to their nextRequestId for (uint256 i = 0; i < nextRequestId[user]; i++) { if (requests[user][i].shares > 0) { return true; } } return false; }, As this will loop
0 -> nextRequestId[user]andnextRequestId[user]will increase with every withdrawal, this will revert at some point and lead to none of the users' withdrawals being finalizable anymore, and thus getting stuck.Recommendation
We recommend instead counting the number of active withdrawal requests per user. They can be incremented on every new creation and decremented on every cancel/finalization.
Precision loss inside HubVault leads to inability to check deviation of exchange rate
State
Severity
- Severity: High
≈
Likelihood: High×
Impact: Medium Submitted by
rvierdiiev
Description
HubVault.updateUnderlyingBalance()calculatesnewExchangeRateusing the following formula:uint256 newExchangeRate = _totalAssets(newAggregatedBalance).mulDiv(1, totalVaultSupply, Math.Rounding.Floor);Since the exchange rate is computed per 1 unit, this causes significant precision loss. For example, with 5000 assets and 4000 shares, the true exchange rate is 1.25, but the calculation truncates it to 1.
Because of this truncation, the percentage deviation check becomes inaccurate, and the contract may fail to pause when the exchange rate deviates beyond the allowed threshold.
Recommendation
Compute
newExchangeRateusing a scaled base such as10 ** decimals()to preserve precision:uint256 newExchangeRate = _totalAssets(newAggregatedBalance).mulDiv(10 ** decimals(), totalVaultSupply, Math.Rounding.Floor);SpectraExecutionSchema doesn't validate last command allowing curator to transfer funds to any address
State
Severity
- Severity: High
≈
Likelihood: High×
Impact: High Submitted by
rvierdiiev
Description
In order to validate the input,
SpectraExecutionSchemaiterates through thecommandsarray:for (uint256 i = 0; i < commands.length; i++) { uint8 command = uint8(commands[i]); // Validate command sequence if (i < commands.length - 1) { _validateCommandSequence(command, uint8(commands[i + 1]), inputs[i]); }}However, the logic only validates the current command against the next command, and never validates the last command. This allows a curator to place an unsafe or malicious final command (e.g., an arbitrary token transfer to their own address), bypassing validation entirely.
Recommendation
Ensure that every command in the list is validated.
Multiple Spectra commands are unrestricted allowing for full drainage of vault
State
Severity
- Severity: High
≈
Likelihood: High×
Impact: High Submitted by
J4X
Description
The
_validateCommandSequencefunction of theSpectraExecutionSchemais used to verify that the commands being executed through spectra are valid and in the correct order. The commands that should be executable are the following:uint256 constant TRANSFER_FROM = 0x00;uint256 constant TRANSFER_FROM_WITH_PERMIT = 0x01;uint256 constant TRANSFER = 0x02;uint256 constant DEPOSIT_ASSET_IN_IBT = 0x04;uint256 constant DEPOSIT_ASSET_IN_PT = 0x05;uint256 constant DEPOSIT_IBT_IN_PT = 0x06;uint256 constant REDEEM_IBT_FOR_ASSET = 0x07;uint256 constant REDEEM_PT_FOR_ASSET = 0x08;uint256 constant REDEEM_PT_FOR_IBT = 0x09;uint256 constant CURVE_ADD_LIQUIDITY = 0x0c;uint256 constant WRAP_VAULT_IN_4626_ADAPTER = 0x10;uint256 constant UNWRAP_VAULT_FROM_4626_ADAPTER = 0x11;uint256 constant CURVE_NG_ADD_LIQUIDITY = 0x17;uint256 constant CURVE_ADD_LIQUIDITY_SNG = 0x1B;uint256 constant CURVE_REMOVE_LIQUIDITY_ONE_COIN = 0x0e;uint256 constant CURVE_REMOVE_LIQUIDITY_ONE_COIN_SNG = 0x1D;uint256 constant CURVE_NG_REMOVE_LIQUIDITY_ONE_COIN = 0x19;However, when the provided commands are validated in
_validateCommandSequence, it is actually never verified that they are contained in this list, thus an array of potentially malicious commands can be executed without any restrictions.Recommendation
We recommend reverting for every command not on the list of trusted commands.
Medium Risk8 findings
updateMinUpdateDelay maximum is not enforced
Severity
- Severity: Medium
≈
Likelihood: Medium×
Impact: Medium Submitted by
J4X
Description
The documentation for
updateMinUpdateDelaystates, "Must not exceed 7 days." So the function should revert if the providednewMinUpdateDelay > 7days./// @notice Update the minimum delay between updates/// @dev Callable by authorized accounts only. Must not exceed 7 days./// @param newMinUpdateDelay The new minimum update delay (in seconds)function updateMinUpdateDelay(uint256 newMinUpdateDelay) external requiresAuth whenNotPaused { uint256 oldDelay = minUpdateDelay; minUpdateDelay = newMinUpdateDelay; emit MinUpdateDelayUpdated(oldDelay, newMinUpdateDelay);}However, this is actually never checked and any value can be set.
Recommendation
We recommend adding a check for
newMinUpdateDelay <= 7 daysWithdrawals can be queued while vault is paused
State
Severity
- Severity: Medium
≈
Likelihood: High×
Impact: Medium Submitted by
J4X
Description
The
requestWithdraw()function is used to queue withdrawals:/// @notice Request a withdrawal with escrow and rate lock./// @param shares The amount of shares to withdraw./// @param userMaxLossBps The maximum loss basis points the user accepts (0 = use default)./// @param allowThirdPartyComplete Whether to allow solver buyouts (protocol nodes can always complete)./// @return requestId The ID of the created withdrawal request.function requestWithdraw( uint256 shares, uint16 userMaxLossBps, bool allowThirdPartyCompleteIt correctly ensures that the
WithdrawEscrowis currently not paused; however, it ignores whether the vault is paused.Recommendation
We recommend checking if the vault is currently paused at the start of the
requestWithdraw()function.Users can't require no losses on withdrawal
State
Severity
- Severity: Medium
≈
Likelihood: High×
Impact: Medium Submitted by
J4X
Description
The
userMaxLossBpscan be set by a user on the creation of a withdrawal request. It states how many BPS of loss the user is willing to incur on their withdrawal (which might get executed by a node).uint16 userMaxLossBps; // 0 => use defaultHowever the user can never enforce that he does not want his withdrawal to be executed at all if any losses occur. This is due to the following reason. If the user sets the value to 0, the program will use the default which will be avove zero. So the user is forced to allow each withdrawal to incur at least 1 BPS (0.01%) of loss.
Recommendation
We recommend adding a bool that lets the user indicate whether to use the default for
userMaxLossBps.Accountant.updateFeeRates should not charge previous epoch with new rates
State
Severity
- Severity: Medium
≈
Likelihood: Medium×
Impact: Medium Submitted by
rvierdiiev
Description
To avoid charging past time periods with newly updated fee rates, the fee changes provided to
Accountant.updateFeeRates()should only take effect afterupdateExchangeRate()has been executed. Otherwise, the new rates apply retroactively, causing inaccurate fee accounting.Recommendation
Use one of the following approaches:
- Call
updateExchangeRate()at the beginning ofupdateFeeRates()to ensure all fees are settled up toblock.timestampbefore applying new rates. - Alternatively, queue the new fee rates and apply them only after the next
updateExchangeRate()call, updating the rates at the end of that function.
Users can get bigger loss than maximum they expected
Severity
- Severity: Medium
≈
Likelihood: Medium×
Impact: Medium Submitted by
rvierdiiev
Description
When a user requests a withdrawal through
WithdrawEscrow, they specifyuserMaxLossBps— the maximum loss they are willing to accept. If, at withdrawal maturity, the exchange rate implies a loss greater than this value, the user cannot complete the withdrawal and can only cancel it and create a new request.However, after canceling and re-requesting, the user is exposed to the risk of an even worse exchange rate, potentially resulting in losses far greater than those at the time of the original matured request.
Recommendation
Allow the user, at the moment the withdrawal is matured, to explicitly accept the current loss even if it exceeds their originally specified
userMaxLossBps, enabling them to complete the withdrawal without canceling and retrying.Solver overpays for the buyout
State
Severity
- Severity: Medium
≈
Likelihood: High×
Impact: Low Submitted by
rvierdiiev
Description
As the price of a withdrawal request increases over time, solvers will likely submit a slightly higher
payAmountbecause they cannot predict the exact block in which their transaction will be executed. However, thesolverBuyout()function always charges the user with the fullpayAmount, even when the fair priceminPayis lower.Recommendation
Charge the user only the fair price
minPay, instead of thepayAmountprovided by the solver.Spectra schema implements wrong check after IBT withdrawal
State
Severity
- Severity: Medium
≈
Likelihood: High×
Impact: Medium Submitted by
J4X
Description
The spectra schema is intended to ensure that the curator can not call malicious series of instructions. The following snippet is intended to ensure this:
} else if (i == 0) { // Output is IBT, next command must be IBT-related if (nextCommand != REDEEM_PT_FOR_ASSET && nextCommand != REDEEM_PT_FOR_IBT && nextCommand != CURVE_ADD_LIQUIDITY && nextCommand != CURVE_NG_ADD_LIQUIDITY && nextCommand != CURVE_ADD_LIQUIDITY_SNG && nextCommand != UNWRAP_VAULT_FROM_4626_ADAPTER && nextCommand != TRANSFER) { revert SpectraExecutionSchema__InvalidCommandSequence(); }}However this actually are PT not IBT related instructions. As a result this will allow for a bypass of the intended behavior.
Recommendation
We recommend adapting the accepted commands to the following:
// Output is IBT, next command must consume or handle IBT safelyif (nextCommand != DEPOSIT_IBT_IN_PT && nextCommand != REDEEM_IBT_FOR_ASSET && nextCommand != CURVE_ADD_LIQUIDITY && nextCommand != CURVE_NG_ADD_LIQUIDITY && nextCommand != CURVE_ADD_LIQUIDITY_SNG && nextCommand != UNWRAP_VAULT_FROM_4626_ADAPTER && nextCommand != TRANSFER) { revert SpectraExecutionSchema__InvalidCommandSequence();}Accountant contract is being stuck in case price increases more than 10%
State
Severity
- Severity: Medium
≈
Likelihood: Low×
Impact: High Submitted by
rvierdiiev
Description
During
Accountant.updateExchangeRate()call, thedeviationbetweennewExchangeRateandhighWaterMarkis calculated. If it exceedsmaxDeviation, the contract is paused.if (highWaterMark > 0) { uint256 deviation = newExchangeRate > highWaterMark ? newExchangeRate - highWaterMark : highWaterMark - newExchangeRate; uint256 deviationPercentage = deviation.mulDiv(1e18, highWaterMark, Math.Rounding.Ceil); if (deviationPercentage > maxDeviation) { _pause(); emit EmergencyPaused(deviationPercentage, newExchangeRate); return; // Exit early without updating state }}If deviation is legitimate, admins will likely attempt to increase
maxDeviationand then unpause the contract. However, the system enforces a strict upper bound of 10%:function updateMaxDeviation(uint256 newMaxDeviation) external requiresAuth whenNotPaused { require(newMaxDeviation <= 1e17, Errors.Accountant__MaxDeviationTooHigh()); // 10% max uint256 oldDeviation = maxDeviation; maxDeviation = newMaxDeviation; emit MaxDeviationUpdated(oldDeviation, newMaxDeviation);}If the actual deviation exceeds 10%, admins cannot set
maxDeviationabove that value, meaning it becomes impossible to unpause the contract. The system becomes permanently stuck until an upgrade is deployed.Recommendation
Remove the requirement that
newMaxDeviationmust be ≤ 10%, allowing admins to temporarily raise the threshold in exceptional cases and safely unpause the protocol.
Low Risk10 findings
manage() arrays size check is missing
State
Severity
- Severity: Low
≈
Likelihood: Medium×
Impact: Low Submitted by
J4X
Description
The
manage()function allows for a multicall through theHubVaultto arbitrary addresses./** * @notice Allows manager to make arbitrary function calls from this contract. * @dev Callable by MANAGER_ROLE. */ function manage(address[] calldata targets, bytes[] calldata data, uint256[] calldata values) external requiresAuth whenNotPaused returns (bytes[] memory results) { uint256 targetsLength = targets.length; results = new bytes[](targetsLength); for (uint256 i; i < targetsLength; ++i) { results[i] = targets[i].functionCallWithValue(data[i], values[i]); } }To do this, the caller provides 3 arrays, one for the targets, one for the data, and one for the values. However, it is not ensured that all of those are of the same length, which could lead to unexpected reverts.
Recommendation
We recommend ensuring that
targets.length() == data.length == values. length().Fee recipient could be initialized to zero
State
Severity
- Severity: Low
≈
Likelihood: High×
Impact: Low Submitted by
J4X
Description
In the initializer of
HubVaultthe provided_feeRecipientwill be set without any checks.function initialize(IERC20 _asset, address _owner, string memory _name, string memory _symbol, address _feeRecipient) public initializer { __Context_init(); __ERC20_init(_name, _symbol); __ERC4626_init(_asset); __Auth_init(_owner, Authority(address(0))); __Pausable_init(); __ReentrancyGuard_init(); maxPercentageChange = 1e16; // 1% feeRecipient = _feeRecipient;}However, when looking at the update function, it is intended that
feeRecipientcan't be set to zero:/// @notice Update the fee recipient address./// @dev Callable by authorized accounts only./// @param newFeeRecipient The new fee recipient address.function updateFeeRecipient(address newFeeRecipient) external requiresAuth { require(newFeeRecipient != address(0), Errors.InvalidFeeRecipient()); emit FeeRecipientUpdated(feeRecipient, newFeeRecipient); feeRecipient = newFeeRecipient;}Recommendation
We recommend adding a zero address check to the initializer.
Incorrect rounding on Platform fees
State
Severity
- Severity: Low
Submitted by
J4X
Description
The platform fees are calculated as follows:
uint256 platformFeeShares = totalSupply .mulDiv(platformFee, feeDenominator, Math.Rounding.Floor) .mulDiv(timeDelta, 365 days, Math.Rounding.Floor);This actually leads to rounding, which results in lower fees than intended.
Recommendation
We recommend rounding up when calculating the platform fees.
Incorrect rounding on Performance fees
State
Severity
- Severity: Low
≈
Likelihood: High×
Impact: Low Submitted by
J4X
Description
The performance fees are calculated as follows:
// feeShares = totalSupply * (gain / highWaterMark) * (performanceFee / feeDenominator)feeShares = gain.mulDiv(totalSupply, highWaterMark, Math.Rounding.Floor) .mulDiv(performanceFee, feeDenominator, Math.Rounding.Floor);However, this leads to less fees being charged than intended.
Recommendation
We recommend rounding up when calculating the performance fees.
getUserRequests() will run OOG
State
Severity
- Severity: Low
≈
Likelihood: High×
Impact: Low Submitted by
J4X
Description
The users
nextRequestId[user]will increase over time. This will lead to an OOG issue in thegetUserRequests()function.for (uint256 i = 0; i < nextId; i++) { if (requests[user][i].shares > 0) { activeCount++; }} // Second pass: populate arraysrequestIds = new uint256[](activeCount);requestDetails = new WithdrawRequest[](activeCount);uint256 index = 0; for (uint256 i = 0; i < nextId; i++) { if (requests[user][i].shares > 0) { requestIds[index] = i; requestDetails[index] = requests[user][i]; index++; }}This function will loop over
0 -> nextRequestId[user], which will lead to an OOG revert oncenextRequestId[user]has reached a certain value.Recommendation
We recommend tracking a
lastActiveid and only iterating over the rangelastActive -> nextRequestId[user]. This way, the looping is limited unless a user willingly leaves an old request open (for which there is no benefit).Deprecated function in Across Schema
State
Severity
- Severity: Low
≈
Likelihood: High×
Impact: Low Submitted by
J4X
Description
The schema for the Across protocol is used to verify and check transactions. In the current version, it includes the
depositfunction:// Across deposit function - extract only necessary addressesfunction deposit( address recipient, address originToken, uint256, /*amount*/ uint32, /*destinationChainId*/ uint64, /*relayerFeePct*/ uint32 /*quoteTimestamp*/) external view virtual returns (bytes memory restrictedData) { // Return only the addresses needed for Merkle verification restrictedData = abi.encodePacked( recipient, originToken );}However, this function is actually deprecated in the across bridge:
function depositDeprecated_5947912356( address recipient, address originToken, uint256 amount, uint256 destinationChainId, int64 relayerFeePct, uint32 quoteTimestamp, bytes memory message, uint256 maxCount) external payable;Recommendation
We recommend removing the deprecated function.
Non-matching functions in Across Schema
State
Severity
- Severity: Low
≈
Likelihood: High×
Impact: Low Submitted by
J4X
Description
Both the
speedUp()andestimateAmountToReceive()functions implemented in the across schema are currently not implemented in the actual across protocol in the same way.// Across speedUp function - extract only necessary addresses function speedUp( address recipient, address originToken, uint256, /*amount*/ uint32, /*destinationChainId*/ uint64, /*relayerFeePct*/ uint32, /*quoteTimestamp*/ bytes32 /*depositId*/ ) external view virtual returns (bytes memory restrictedData) { // Return only the addresses needed for Merkle verification restrictedData = abi.encodePacked( recipient, originToken ); } // Across estimateAmountToReceive function - extract only necessary addresses function estimateAmountToReceive( address originToken, uint256, /*amount*/ uint32, /*destinationChainId*/ uint64 /*relayerFeePct*/ ) external view virtual returns (bytes memory restrictedData) { // Return only the addresses needed for Merkle verification restrictedData = abi.encodePacked( originToken ); }As a result, these will never be callable through the schema.
Recommendation
We recommend removing the functions.
Unsafe cast on shares amount
State
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
rvierdiiev
Description
Inside
WithdrawEscrow.requestWithdraw()thesharesamount is cast touint96without ensuring that the provided value fits into theuint96type. If the value exceeds the max ofuint96, it will be silently truncated, leading to incorrect withdrawal amounts.Recommendation
Before casting, validate that
sharesis less than or equal totype(uint96).maxto prevent truncation.completeWithdraw() can be removed
State
Severity
- Severity: Low
≈
Likelihood: High×
Impact: Low Submitted by
J4X
Description
The
WithdrawEscrowimplements two functions that finalize a withdrawal request./// @notice Complete withdrawal as the current owner (self-service). /// @param user The original user who created the request. /// @param requestId The ID of the withdrawal request to complete. /// @return assetsOut The amount of assets received. function completeWithdrawSelf(address user, uint256 requestId) external nonReentrant whenNotPaused returns (uint256) { return _completeWithdraw(user, requestId, msg.sender); } /// @notice Complete withdrawal as authorized node. /// @param user The user whose withdrawal to complete. /// @param requestId The ID of the withdrawal request to complete. /// @return assetsOut The amount of assets received. function completeWithdraw(address user, uint256 requestId) external requiresAuth nonReentrant whenNotPaused returns (uint256) { return _completeWithdraw(user, requestId, msg.sender); }The separation is intended so that one can be called by the user and the other by the authorized node. However, both result in the same function with the same arguments, and the authorization is checked there. As a result, keeping
completeWithdrawSelfand renaming it tocompleteWithdrawwill suffice.Recommendation
We recommend removing the function.
completionWindow changes will affect already pending withdrawals
State
Severity
- Severity: Low
≈
Likelihood: High×
Impact: Low Submitted by
J4X
Description
A withdrawal request has two important timeframes for processing. The
withdrawDelaydetermines how long it takes for a withdrawal to be finalised. ThecompletionWindowdetermines how long the withdrawal can take.struct AssetConfig { uint32 withdrawDelay; // seconds to maturity uint32 completionWindow; // after maturityBoth these values can be updated using an admin function.
/// @notice Update withdrawal parameters. function setParams( uint32 _withdrawDelay, uint32 _completionWindow, uint16 _defaultMaxLossBps ) external requiresAuth { if (_withdrawDelay == 0) revert Errors.Escrow__InvalidWithdrawDelay(); if (_completionWindow == 0) revert Errors.Escrow__InvalidCompletionWindow(); require(_defaultMaxLossBps <= MAX_LOSS_BPS_CAP, Errors.Escrow__TooMuchMaxLoss()); config.withdrawDelay = _withdrawDelay; config.completionWindow = _completionWindow; config.defaultMaxLossBps = _defaultMaxLossBps; emit WithdrawParamsUpdated(_withdrawDelay, _completionWindow, _defaultMaxLossBps); }However, while the
_withdrawDelayis tracked in the user's withdrawal request in thematurityvalue, thecompletionWindowat creation is not saved. As a result, the users' withdrawal requests made pre-change will still be affected by the changed completion window.// Check timing - allow cancel before maturity or after completion windowif (block.timestamp >= req.maturity && block.timestamp <= req.maturity + config.completionWindow) { revert Errors.Escrow__NotCancellable();}Recommendation
We recommend tracking the completion timestamp in the struct.
Informational7 findings
maxPercentageChange could be set to 0
State
- Acknowledged
Severity
- Severity: Informational
≈
Likelihood: Low×
Impact: Low Submitted by
J4X
Description
The
maxPercentageChangevariable ensures the vault pauses if the underlying balance changes significantly. It is ensured that this pause occurs within a 0-10% range./// @notice Update the maximum percentage change allowed before the vault is paused./// @param newMaxPercentageChange The new maximum percentage change. Max value is 1e17 (10%).function updateMaxPercentageChange(uint256 newMaxPercentageChange) external requiresAuth { require(newMaxPercentageChange < MAX_PERCENTAGE_THRESHOLD, Errors.InvalidMaxPercentage()); emit MaxPercentageUpdated(maxPercentageChange, newMaxPercentageChange); maxPercentageChange = newMaxPercentageChange;}However, it is never checked whether the percent change is set to 0, which would cause an instant pause on any update (unless the balance doesn't change, which is highly unlikely).
Recommendation
We recommend ensuring that
maxPercentageChange > 0.Invalid documentation of InvalidMaxPercentage
State
Severity
- Severity: Informational
≈
Likelihood: High×
Impact: Low Submitted by
J4X
Description
The
InvalidMaxPercentageerror is documented as:/// @notice Thrown when the new max percentage is greater than the current max percentage.error InvalidMaxPercentage();However, this is not true, as the error actually occurs when the new max percentage is greater than the threshold, not the old one.
Recommendation
We recommend adapting it to:
/// @notice Thrown when the new max percentage is greater than the max percentage threshold.error InvalidMaxPercentage();Accountant.initialize() uses wrong error in case of zero vault
State
Severity
- Severity: Informational
≈
Likelihood: High×
Impact: Low Submitted by
J4X
Description
The initializer of
Accountantemits the following error if the vault is set to 0.require(_vault != address(0), Errors.InvalidFeeRecipient());However, this is incorrect as the vault is not the fee receiver.
Recommendation
We recommend emitting the correct error code.
Incorrect documentation of getAvailableBalance()
State
Severity
- Severity: Informational
Submitted by
J4X
Description
The documentation of
getAvailableBalance()states the following:/// @notice Get the available balance./// @dev Returns the vault's asset balance minus pending redemption assets./// @return The available balance for instant redemptions.function getAvailableBalance() public view returns (uint256) { return _getAvailableBalance();}However this is untrue as the returned value does not account for the pending assets.
Recommendation
We recommend adapting the comment to "@dev Returns the vault's asset balance".
feeDenominator should be hardcoded as constant
State
Severity
- Severity: Informational
≈
Likelihood: High×
Impact: Low Submitted by
J4X
Description
The
feeDenominatorvariable is once set to10,000in the initializer ofAccountantfeeDenominator = 10_000;The admin can't change it and will stay at this value forever.
Recommendation
We recommend using a constant for the
feeDenominator.Remove the redundant highWaterMark > 0 check.
State
Severity
- Severity: Informational
Submitted by
rvierdiiev
Description
if (highWaterMark > 0) { uint256 deviation = newExchangeRate > highWaterMark ? newExchangeRate - highWaterMark : highWaterMark - newExchangeRate; uint256 deviationPercentage = deviation.mulDiv(1e18, highWaterMark, Math.Rounding.Ceil); ...}highWaterMarkis initialized to 1e18 during contract deployment and is never allowed to decrease below this value. Since the variable can never be zero, the conditional statement adds unnecessary branching and can be removed.Recommendation
Remove the redundant
highWaterMark > 0check.Recipient of Balancer flashloan is always Manager
Severity
- Severity: Informational
Submitted by
rvierdiiev
Description
As the recipient of a Balancer flash loan should always be the Manager contract, the
recipientparameter inManager.flashLoan()is unnecessary.Recommendation
Remove the
recipientparameter and always passaddress(this)as the flash-loan recipient:BalancerFlashLoan.flashLoan(address(this), tokens, amounts, userData);