Omniscia Teahouse Finance Audit
TeaVaultV3Portfolio Manual Review Findings
TeaVaultV3Portfolio Manual Review Findings
TVV-01M: Non-Standard Gap Size Specification
Type | Severity | Location |
---|---|---|
Standard Conformity | TeaVaultV3Portfolio.sol:L813 |
Description:
The __gap
variable is meant to adhere to the OpenZeppelin upgradeability standard, however, its size has been incorrectly defined. Specifically, the size of the __gap
variable should equate the value of 50
minus all storage slots presently reserved by the contract.
Example:
813uint256[35] private __gap;
Recommendation:
We advise the __gap
variable's length
to be corrected to 50 - 18 = 32
instead of 35
as the contract presently reserves 18
storage slots. Should the immutable
and constant
optimizations advised be applied, the length
should instead be corrected to 34
.
Alleviation (302b96f324a88038a0872015466cd43783c14543):
The __gap
variable's length has been corrected to 34
as the immutable
and constant
optimizations have been applied to the codebase.
TVV-02M: Discrepant Management of Fee Times
Type | Severity | Location |
---|---|---|
Logical Fault | TeaVaultV3Portfolio.sol:L379, L412 |
Description:
The TeaVaultV3Portfolio
is discrepant in the way it handles time updates for its management and performance fees. Specifically, it will update the management fee regardless of whether any shares were actually collected (i.e. due to a short interval) whilst the performance fee time will solely be updated when non-zero fees have been acquired.
Impact:
The system will currently forfeit management fees if they are frequently captured whilst performance fees will always be captured regardless of invocation frequency.
Example:
356function _collectManagementFee(357 uint256 _totalShares,358 FeeConfig memory _feeConfig359) internal returns (360 uint256 collectedShares361) {362 uint256 timeDiff = block.timestamp - lastCollectManagementFee;363 if (timeDiff > 0) {364 unchecked {365 uint256 feeTimesTimediff = _feeConfig.managementFee * timeDiff;366 uint256 denominator = (367 PERCENTAGE_MULTIPLIER * SECONDS_IN_A_YEAR > feeTimesTimediff ?368 PERCENTAGE_MULTIPLIER * SECONDS_IN_A_YEAR - feeTimesTimediff :369 1370 );371 collectedShares = _totalShares.mulDiv(feeTimesTimediff, denominator, MathUpgradeable.Rounding.Up);372 }373
374 if (collectedShares > 0) {375 _mint(_feeConfig.vault, collectedShares);376 emit ManagementFeeCollected(_feeConfig.vault, collectedShares, block.timestamp);377 }378
379 lastCollectManagementFee = block.timestamp;380 }381}382
383/// @inheritdoc ITeaVaultV3Portfolio384function collectPerformanceFee() external override nonReentrant returns (uint256 collectedShares) {385 ERC20Upgradeable[] memory _assets = assets;386
387 return _collectPerformanceFee(388 totalSupply(),389 _calculateAssetsValue(_assets, _calculateTotalAmounts(_assets), _getAssetsTwap(_assets)),390 feeConfig391 );392}393
394function _collectPerformanceFee(395 uint256 _totalShares,396 uint256 _totalValue,397 FeeConfig memory _feeConfig398) internal returns (399 uint256 collectedShares400) {401 // calculate and transfer the unlocked performance fee from the reserve402 uint256 timeDiff = block.timestamp - lastCollectPerformanceFee;403 if (performanceFeeReserve > 0 && timeDiff > 0) {404 collectedShares = performanceFeeReserve.mulDiv(405 (1 << 128) - power128(feeConfig.decayFactor, timeDiff),406 1 << 128,407 MathUpgradeable.Rounding.Up408 );409 if (collectedShares > 0) {410 performanceFeeReserve -= collectedShares;411 _transfer(address(this), feeConfig.vault, collectedShares);412 lastCollectPerformanceFee = block.timestamp;413 emit PerformanceFeeCollected(feeConfig.vault, collectedShares, block.timestamp);414 }415 }
Recommendation:
We advise this discrepancy to be corrected, either ensuring that both fees have their time updated when fees are actually acquired or that the time is updated regardless of whether fees have been collected, the former of which we expect to be desirable behaviour.
Alleviation (302b96f324a88038a0872015466cd43783c14543):
The TeaVaultV3Portfolio::_collectPerformanceFee
function was updated to assign its lastCollectPerformanceFee
value at all times regardless of whether a fee was actually captured.
As such, we consider this exhibit alleviated as the performance and management fees have become consistent in their application.
TVV-03M: Improper Fee Acquisition Methodology
Type | Severity | Location |
---|---|---|
Logical Fault | TeaVaultV3Portfolio.sol:L290 |
Description:
The TeaVaultV3Portfolio
system will acquire deposit fees by extracting an additional value on top of the expected deposit from the caller. This is non-standard behaviour and will lead to significant UX problems given that users will have to "over-approve" their assets to the system in order to interact with it.
Impact:
The current deposit fee acquisition mechanism will lead to failed transactions, difficulty in on-chain integration, and a generally bad user experience for users and integrators alike.
Example:
274for (uint256 i; i < assetsLength; i = i + 1) {275 if (shareAmounts[i] > 0) {276 _assets[i].safeTransferFrom(msg.sender, address(this), shareAmounts[i]);277 totalAmount = totalAmount + shareAmounts[i];278 279 // collect entry fee for users280 // do not collect entry fee for fee recipient281 if (senderNotVault) {282 entryFeeAmount = shareAmounts[i].mulDiv(283 _feeConfig.entryFee,284 PERCENTAGE_MULTIPLIER,285 MathUpgradeable.Rounding.Up286 );287
288 if (entryFeeAmount > 0) {289 totalAmount = totalAmount + entryFeeAmount;290 _assets[i].safeTransferFrom(msg.sender, _feeConfig.vault, entryFeeAmount);291 entryFeeAmounts[i] = entryFeeAmount;292 }293 }294 depositedAmounts[i] = shareAmounts[i] + entryFeeAmount;295 }296}
Recommendation:
We advise the fee to either be imposed on the input amounts, or to be imposed on the output _shares
, either of which we consider an adequate resolution to this exhibit.
Alleviation (302b96f324):
The Teahouse Finance team specified that the user would not over-approve in this case as the calculations are performed before the deposit is actually done.
We would like to note that on-chain contracts would not be able to reliably integrate with the system, and the over-approval in our exhibit refers to an approval beyond the actual deposit rather than a lingering approval from the user to the system.
Based on the aforementioned facts, we advise the Teahouse Finance team to revisit this exhibit.
Alleviation (1dc136da87):
The Teahouse Finance team revisited this exhibit and opted to retain the current behaviour in place, rendering it acknowledged.
TVV-04M: Improper Order of Performance Fee Evaluation
Type | Severity | Location |
---|---|---|
Logical Fault | TeaVaultV3Portfolio.sol:L401-L415, L417-L446 |
Description:
The TeaVaultV3Portfolio::_collectPerformanceFee
function is currently incorrect as it will attempt to capture the performance fee from a pool of shares that are calculated after the fee is captured.
Coupled with the fact that the performance fee's time variable is updated solely when fees are captured, the system becomes inefficient as it will have to be re-invoked to actually capture the fees reserved in the latter part of the function's body.
Impact:
The system is presently inefficient and would lead to untapped performance fees at any given time.
Example:
394function _collectPerformanceFee(395 uint256 _totalShares,396 uint256 _totalValue,397 FeeConfig memory _feeConfig398) internal returns (399 uint256 collectedShares400) {401 // calculate and transfer the unlocked performance fee from the reserve402 uint256 timeDiff = block.timestamp - lastCollectPerformanceFee;403 if (performanceFeeReserve > 0 && timeDiff > 0) {404 collectedShares = performanceFeeReserve.mulDiv(405 (1 << 128) - power128(feeConfig.decayFactor, timeDiff),406 1 << 128,407 MathUpgradeable.Rounding.Up408 );409 if (collectedShares > 0) {410 performanceFeeReserve -= collectedShares;411 _transfer(address(this), feeConfig.vault, collectedShares);412 lastCollectPerformanceFee = block.timestamp;413 emit PerformanceFeeCollected(feeConfig.vault, collectedShares, block.timestamp);414 }415 }416
417 if (_totalShares != 0) {418 if (_totalValue > highWaterMark) {419 uint256 increasedReserveValue = (_totalValue - highWaterMark).mulDiv(420 _feeConfig.performanceFee,421 PERCENTAGE_MULTIPLIER,422 MathUpgradeable.Rounding.Up423 );424 uint256 increasedReserve = _totalShares.mulDiv(425 increasedReserveValue,426 _totalValue - increasedReserveValue,427 MathUpgradeable.Rounding.Up428 );429 performanceFeeReserve = performanceFeeReserve + increasedReserve;430 _mint(address(this), increasedReserve);431 highWaterMark = _totalValue;432 }433 else {434 uint256 decreasedReserveValue = (highWaterMark - _totalValue).mulDiv(435 _feeConfig.performanceFee,436 PERCENTAGE_MULTIPLIER437 );438 uint256 decreasedReserve = _totalShares.mulDiv(439 decreasedReserveValue,440 _totalValue + decreasedReserveValue441 );442 decreasedReserve = decreasedReserve > performanceFeeReserve ? performanceFeeReserve : decreasedReserve;443 performanceFeeReserve = performanceFeeReserve - decreasedReserve;444 _burn(address(this), decreasedReserve);445 }446 }447}
Recommendation:
We advise the statements to be re-ordered, calculating the performanceFeeReserve
prior to actually capturing the fees.
Alleviation (302b96f324a88038a0872015466cd43783c14543):
The Teahouse Finance team specified that the described behaviour by the exhibit is by design as the performance fee is meant to be unlocked over time rather than immediately.
As such, we consider the code correct and adhering to the contract's business requirements rendering this exhibit invalid.
TVV-05M: Insufficient Validation of Oracle Compatibility
Type | Severity | Location |
---|---|---|
Input Sanitization | TeaVaultV3Portfolio.sol:L109-L112 |
Description:
The TeaVaultV3Portfolio::initialize
function will validate that the base asset of the _assetOracle
and _teaVaultV3PairOracle
contracts match the one in TeaVaultV3Portfolio
, however, the base asset of the aaveATokenOracle
is never validated.
Impact:
The TeaVaultV3Portfolio
can presently be initialized with an aaveATokenOracle
that utilizes a different base asset which we consider incorrect.
Example:
99assetOracle = _assetOracle;100aaveATokenOracle = _aaveATokenOracle;101teaVaultV3PairOracle = _teaVaultV3PairOracle;102swapper = _swapper;103
104_addAsset(_baseAsset, AssetType.Base);105for (uint256 i; i < _assets.length; i = i + 1) {106 _addAsset(_assets[i], _assetTypes[i]);107}108
109if (110 address(_baseAsset) != _assetOracle.getBaseAsset() ||111 address(_baseAsset) != _teaVaultV3PairOracle.getBaseAsset()112) revert IAssetOracle.BaseAssetMismatch();
Recommendation:
We advise it to be validated as well, ensuring all oracles are synchronized regarding the _baseAsset
they rely on.
Alleviation (302b96f324a88038a0872015466cd43783c14543):
The base asset of the _aaveATokenOracle
is now validated as well in the referenced if
conditional, alleviating this exhibit in full.
TVV-06M: Improper Assumptions of Asset Maintenance
Type | Severity | Location |
---|---|---|
Logical Fault | TeaVaultV3Portfolio.sol:L130-L132, L150-L160 |
Description:
The TeaVaultV3Portfolio::addAsset
and TeaVaultV3Portfolio::removeAsset
functions are flawed and will not behave as expected in a production environment of the TeaVaultV3Portfolio
. Specifically, a newly added asset via TeaVaultV3Portfolio::addAsset
will never have a non-zero balance as the TeaVaultV3Portfolio::_calculateTotalAmounts
function would yield zero for it.
On the other hand, a removal of an asset via TeaVaultV3Portfolio::removeAsset
will evaluate whether a non-zero balance of the asset-to-be-removed exists in the contract and prevent execution in such a case. This permits hijackers to transmit a negligible amount (i.e. 1 wei
) to the contract of an asset being removed to keep it within the assets
list forever.
Impact:
The current mechanisms in place for maintaining the assets
supported by the TeaVaultV3Portfolio
are insufficient and can be hijacked, forcing unwanted assets to remain part of the portfolio.
Example:
129/// @inheritdoc ITeaVaultV3Portfolio130function addAsset(ERC20Upgradeable _asset, AssetType _assetType) external override onlyOwner {131 _addAsset(_asset, _assetType);132}133
134function _addAsset(ERC20Upgradeable _asset, AssetType _assetType) internal {135 // base asset must be the first to be added136 if (_assetType == AssetType.Null || _assetType >= AssetType.End) revert InvalidAssetType();137 if (_assetType == AssetType.Base && assets.length != 0) revert BaseAssetCannotBeAdded();138 if (assetType[_asset] != AssetType.Null) revert AssetAlreadyAdded();139 if (_assetType == AssetType.Atomic && !assetOracle.isOracleEnabled(address(_asset))) revert OracleNotEnabled();140 if (_assetType == AssetType.AToken && !aaveATokenOracle.isOracleEnabled(address(_asset))) revert OracleNotEnabled();141 if (_assetType == AssetType.TeaVaultV3Pair && !teaVaultV3PairOracle.isOracleEnabled(address(_asset))) revert OracleNotEnabled();142 143 assets.push(_asset);144 assetType[_asset] = _assetType;145
146 emit AssetAdded(address(_asset), block.timestamp);147}148
149/// @inheritdoc ITeaVaultV3Portfolio150function removeAsset(uint256 _index) external override onlyOwner {151 if (assets[_index].balanceOf(address(this)) != 0) revert AssetBalanceNotZero();152 if (assetType[assets[_index]] == AssetType.Base) revert BaseAssetCannotBeRemoved();153
154 ERC20Upgradeable asset = assets[_index];155 assets[_index] = assets[assets.length - 1];156 assets.pop();157 assetType[asset] = AssetType.Null;158
159 emit AssetRemoved(address(asset), block.timestamp);160}
Recommendation:
We advise the code to be revised as the assets
list in its current state cannot be dynamically updated. Example remediations include converting the asset to be removed to the supported assets of the system, and supporting the inclusion of a new asset solely if existing balance is converted to it.
Alleviation (302b96f324):
The Teahouse Finance team stated that they wish to retain the dynamic adjustment of the assets
list in place due to their business requirements.
We would like to reiterate that this exhibit does not state that such functionality should be removed, it instead states that the functionality is presently impossible and easy to sabotage.
As such, we advise the Teahouse Finance team to revisit this exhibit.
Alleviation (a337648498):
The Teahouse Finance team re-evaluated this exhibit and opted to introduce an on-chain mechanism whereby a swap is performed before the removal of the asset. This mechanism while valid is presently insufficient as it will rely on the TeaVaultV3Portfolio::swapAndRemoveAsset
input amount (_inputAmount
).
To ensure the function can be reliably utilized, we advise the code to use the full balance of the contract in the respective asset.
Alleviation (1dc136da87):
The code was updated to properly calculate the _inputAmount
of the swap dynamically in the TeaVaultV3Portfolio::swapAndRemoveAsset
flow, however, the TeaVaultV3Portfolio::removeAsset
flow was updated in this commit as well and will perform a Uniswap V3 withdrawal with no slippage control thereby rendering the full withdrawn amount susceptible to sandwich attacks.
We advise proper slippage values to be supplied to the ITeaVaultV3Pair::withdraw
call, ensuring that the withdrawn amounts are properly associated with the LP units burned.
Alleviation (3a0cc9c15a):
The code was updated to properly accept minimum outputs for all assets in the vault that are validated after the swap of the asset-to-be-removed but prior to its removal, thereby ensuring that any swap that occurred has abode by the expected slippage thresholds configured by the caller.
As such, we consider this exhibit fully alleviated.
TVV-07M: Potential Hijack of High Water Mark Initialization
Type | Severity | Location |
---|---|---|
Logical Fault | TeaVaultV3Portfolio.sol:L262 |
Description:
The TeaVaultV3Portfolio::deposit
function relies on an empty balance of the contract to initialize the highWaterMark
which can be trivially hijacked, causing the highWaterMark
to remain zero until the first time a performance fee is captured at an unfair evaluation.
Impact:
By donating supported assets to the portfolio directly, the highWaterMark
will not be initialized with the first deposit and as such the first TeaVaultV3Portfolio::_collectPerformanceFee
invocation after the first deposit will capture a performance fee as if the contract went from 0
to the depositedValue
.
Example:
262if (totalValue == 0) {263 highWaterMark = depositedValue;264}265else {266 highWaterMark = highWaterMark.mulDiv(totalValue + depositedValue, totalValue);267}
Recommendation:
We advise the code to instead check if the highWaterMark
is 0
, ensuring that its value is initialized regardless of the system's state.
Alleviation (302b96f324a88038a0872015466cd43783c14543):
The code was updated to correctly initialize the value of the highWaterMark
, evaluating whether its 0
and either initializing it to the depositedValue
or updating it per the business requirements of the system.
As such, we consider this exhibit fully alleviated.