Platform: Code4rena
Start Date: 07/07/2023
Pot Size: $121,650 USDC
Total HM: 36
Participants: 111
Period: 7 days
Judge: Picodes
Total Solo HM: 13
Id: 258
League: ETH
Rank: 5/111
Findings: 5
Award: $2,710.76
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: KIntern_NA
Also found by: KupiaSec
948.4507 USDC - $948.45
The exchange rate of the vaults will be decreasing and this will block core functionalities.
The exchange rate of the Vault is calculated as follows:
min(_withdrawableAssets, _totalSupplyToAssets) * _assetUnit / _totalSupplyAmount = min(_withdrawableAssets * _assetUnit / _totalSupplyAmount, _lastRecordedExchangeRate)
So the resulting exchange rate from _currentExchangeRate
method is less than or equal to _lastRecordedExchangeRate
in normal situation when the total supply of current vault and the withdrawable assets from the yield vault is not zero.
_lastRecordedExchangeRate
is only updated by the result of _currentExchangeRate
method, so the exchange rate of the vault will be decreasing. It starts with 1, so the exchange rate will be less than 1. In this situation, maxDeposit
and maxMint
will return 0, so depositing to the vault will not work. The liquidation pair also can't call liquidate
. Since these functionalities take important roles in the protocol, so the impact will be huge.
Manual Review
I think the implementation of _currentExchangeRate
is not correct at the moment, but I am not sure how to mitigate this method.
Error
#0 - c4-judge
2023-07-16T10:09:53Z
Picodes marked the issue as unsatisfactory: Insufficient proof
#1 - c4-judge
2023-07-16T10:12:25Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-07-18T19:54:45Z
Picodes marked the issue as duplicate of #443
#3 - c4-judge
2023-08-05T21:46:17Z
Picodes marked the issue as partial-50
#4 - Picodes
2023-08-05T21:46:54Z
Partial credit as it seems the warden as an intuition that something is off but no impact or mitigation is proposed
#5 - auditor0517
2023-08-09T09:53:33Z
This report explains why the exchange rate is decreasing. And it indicates the impacts related to deposit and liquidations. Although this report did not show the mitigation, mitigation for this issue is not obvious. I think no mitigation is better than the wrong mitigation. partial-50 is too tight for this issue.
#6 - Picodes
2023-08-12T17:18:28Z
@auditor0517 I respectfully disagree.
As I understand it the High severity (loss of funds) scenario here is the fact that "when the vault is undercollateralized (_currentExchangeRate < _assetUnit), it can't be further collateralized", so users will withdraw less than intended. Here, this report highlights that it'd lead to "maxDeposit and maxMint will return 0, so depositing to the vault will not work. The liquidation pair also can't call liquidate. Since these functionalities take important roles in the protocol, the impact will be huge.", which is a scenario of Medium severity (DoS). In addition to this, the report is vague about the impact and mitigation. So overall partial-50
of a High issue seems justified to me. Note that it is better than Med.
Please let me know if I am missing something here. Also, just out of transparency, is this your report?
#7 - auditor0517
2023-08-13T02:28:04Z
@Picodes Thanks for your reply. It's not mine but I've checked their issues as I am going to collaborate with them later.
🌟 Selected for report: Aymen0909
Also found by: 0xWaitress, KupiaSec, wangxx2026
768.245 USDC - $768.25
The output amount validation is not correct in Vault.liquidate()
, so the method might accept invalid output amount and refuse valid output amount.
In Vault.liquidate()
, there is a validation about the output share amount should be less than or equal to the liquidatable yield.
uint256 _liquidableYield = _liquidatableBalanceOf(_tokenOut); if (_amountOut > _liquidableYield) revert LiquidationAmountOutGTYield(_amountOut, _liquidableYield);
The liquidatable yield amount is in underlying asset token. So the comparison between the share amount and the underlying asset amount is not appropriate.
We could get share tokens from asset tokens via exchange rate. The vault gets _liquidableYield
and mints _amountOut
, so the correct asset amount equivalent to _amountOut
of the share token will be _amountOut
* exchange rate. The correct validation should use the asset amount and the current implementation is not correct when the exchange rate is not 1.
Manual Review
We should use the underlying equivalent with the exchange rate for the validation.
Error
#0 - c4-judge
2023-07-18T17:25:22Z
Picodes marked the issue as duplicate of #427
#1 - c4-judge
2023-08-05T21:47:31Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-08-05T21:50:35Z
Picodes changed the severity to 3 (High Risk)
🌟 Selected for report: Udsen
Also found by: 0xMirce, 0xPsuedoPandit, 0xStalin, 0xbepresent, Aymen0909, Bobface, Co0nan, GREY-HAWK-REACH, Jeiwan, John, KupiaSec, LuchoLeonel1, Nyx, Praise, RedTiger, alexweb3, bin2chen, btk, dacian, dirk_y, josephdara, keccak123, ktg, mahdirostami, markus_ether, minhtrng, ni8mare, peanuts, ptsanev, ravikiranweb3, rvierdiiev, seeques, serial-coder, shaka, teawaterwire, wangxx2026, zzzitron
2.2492 USDC - $2.25
Anyone can mint the yield fee using mintYieldFee()
.
mintYieldFee()
mints the shares for yield fee.
function mintYieldFee(uint256 _shares, address _recipient) external { _requireVaultCollateralized(); if (_shares > _yieldFeeTotalSupply) revert YieldFeeGTAvailable(_shares, _yieldFeeTotalSupply); _yieldFeeTotalSupply -= _shares; _mint(_recipient, _shares); emit MintYieldFee(msg.sender, _recipient, _shares); }
This function mints the shares to the _recipient
and reduces _yieldFeeTotalSupply
accordingly.
But there is no access control here and anyone can steal the yield fee using this function.
Manual Review
mintYieldFee()
should have a whitelist for callers.
Access Control
#0 - c4-judge
2023-07-16T21:54:12Z
Picodes marked the issue as duplicate of #396
#1 - c4-judge
2023-08-05T22:01:28Z
Picodes changed the severity to 3 (High Risk)
#2 - c4-judge
2023-08-05T22:05:11Z
Picodes marked the issue as satisfactory
🌟 Selected for report: dirk_y
Also found by: 0xbepresent, 0xkasper, Jeiwan, KupiaSec, bin2chen, rvierdiiev, xuwinnie
252.0228 USDC - $252.02
Undelegated users will lose their balances if they set delegate to themselves.
A User can set a delegate and the delegated balance of the user will be accounted on the delegate's delegated balance in the TWAB controller.
The internal method _delegate
will handle this functionality as follows:
function _delegate(address _vault, address _from, address _to) internal { address _currentDelegate = _delegateOf(_vault, _from); if (_to == _currentDelegate) { revert SameDelegateAlreadySet(_to); } delegates[_vault][_from] = _to; _transferDelegateBalance( _vault, _currentDelegate, _to, uint96(userObservations[_vault][_from].details.balance) ); emit Delegated(_vault, _from, _to); }
If a user sets his delegate to himself when he is not delegated through the external delegate
method, _to
is address(0), and _currentDelegate
will be same as _from
and this passes the if statement in the next line. But this is the case when the delegates are the same, so we need to revert here.
After that, delegates
storage variable is not changed. In this situation, _transferDelegateBalance
will be called and this will cause the user loses his balance.
Manual Review
We should revert when _currentDelegate
is same as _from
and _to
is address(0).
Error
#0 - c4-judge
2023-07-16T22:02:25Z
Picodes marked the issue as duplicate of #293
#1 - c4-judge
2023-08-06T19:59:50Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-08-06T19:59:57Z
Picodes changed the severity to 3 (High Risk)
739.7915 USDC - $739.79
https://github.com/GenerationSoftware/pt-v5-vault/tree/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L466-L469 https://github.com/GenerationSoftware/pt-v5-vault/tree/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L971-L974 https://github.com/GenerationSoftware/pt-v5-vault/tree/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L882-L887
Vault.mintWithPermit()
uses a signature to approve the underlying asset. But the asset amount can be changed easily, so this method can be reverted and might be DoSed.
Vault.mintWithPermit()
gets the share amount as an input and calculates the asset amount from the share. And then approves the asset amount with permit
method.
uint256 _assets = _beforeMint(_shares, _receiver); _permit(IERC20Permit(asset()), msg.sender, address(this), _assets, _deadline, _v, _r, _s); _deposit(msg.sender, _receiver, _assets, _shares);
The signature is generated using the exact value of the expected asset amount calculated from the share amount, and the resulting asset amount depends on the exchange rate of current vault.
function _beforeMint(uint256 _shares, address _receiver) internal view returns (uint256) { if (_shares > maxMint(_receiver)) revert MintMoreThanMax(_receiver, _shares, maxMint(_receiver)); return _convertToAssets(_shares, Math.Rounding.Up); }
function _convertToAssets( uint256 _shares, Math.Rounding _rounding ) internal view virtual override returns (uint256) { return _convertToAssets(_shares, _currentExchangeRate(), _rounding); }
So the resulting asset amount can be different from the value of transaction start time. And even an adversary can front-run and manipulate the exchange rate. If the resulting asset amount is different from the original one, the signature will not work as expected and mintWithPermit()
will revert in most cases.
Manual Review
We can input upper bound of the asset amount instead of the exact value of the asset amount.
DoS
#0 - c4-judge
2023-07-16T22:01:25Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-07-20T22:31:15Z
asselstine marked the issue as sponsor confirmed
#2 - c4-judge
2023-08-07T16:44:21Z
Picodes marked the issue as selected for report
#3 - c4-judge
2023-08-07T16:44:39Z
Picodes marked the issue as satisfactory
#4 - PierrickGT
2023-08-10T20:17:51Z
Since most users will use the depositWithPermit
function, I've removed the mintWithPermit
one in this PR: https://github.com/GenerationSoftware/pt-v5-vault/pull/15