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: 30/111
Findings: 5
Award: $739.38
🌟 Selected for report: 3
🚀 Solo Findings: 0
🌟 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.9239 USDC - $2.92
The Vault.mintYieldFee
external function is used to mint Vault shares
to the yield fee _recipient
. The function is an external function and can be called by anyone since there is no access control. The function will revert only under following two conditions:
_shares
are greater than the accrued _yieldFeeTotalSupply
.The issue with this function is it allows the caller to set the _recipient
(Address of the yield fee recipient). It does not use the _yieldFeeRecipient
state variable which was set in the Vault.constructor
as the yield fee recipient
.
Which means any one can steal the available yield fee
from this vault (As long as above two revert conditions are not satisfied) by minting shares
to his own address or to any address of his choice.
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); }
Manual Review and VSCode
Hence it is recommended to use the _yieldFeeRecipient
state variable value as the yield fee recipient
inside the Vault.mintYieldFee
external function and to remove the input parameter address _recipient
from the Vault.mintYieldFee
function. So that the caller will not be able to mint shares to any arbitory address of his choice and steal the yield fee of the protocol.
The updated function should be as follows:
function mintYieldFee(uint256 _shares) external { _requireVaultCollateralized(); if (_shares > _yieldFeeTotalSupply) revert YieldFeeGTAvailable(_shares, _yieldFeeTotalSupply); _yieldFeeTotalSupply -= _shares; _mint(_yieldFeeRecipient, _shares); emit MintYieldFee(msg.sender, _recipient, _shares); }
Other
#0 - c4-judge
2023-07-14T22:16:57Z
Picodes marked the issue as duplicate of #406
#1 - c4-judge
2023-07-14T22:17:05Z
Picodes marked the issue as selected for report
#2 - c4-sponsor
2023-07-20T22:33:33Z
asselstine marked the issue as sponsor confirmed
#3 - PierrickGT
2023-08-02T22:37:39Z
Fixed in this PR: https://github.com/GenerationSoftware/pt-v5-vault/pull/7
#4 - c4-judge
2023-08-05T22:01:39Z
Picodes marked the issue as satisfactory
443.8749 USDC - $443.87
https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L951-L956 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L959 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1026-L1027
The Vault._deposit
function is used by the users to deposit _assets
to the vault and mint vault shares to the recipient
address. The amount of _assets
are transferred to the Vault
as follows:
SafeERC20.safeTransferFrom( _asset, _caller, address(this), _assetsDeposit != 0 ? _assetsDeposit : _assets );
The Vault.deposit
function uses this _assets
amount to calculate the number of shares
to be minted to the _recipient
address.
The issue here is if the underlying _asset
is a fee on transfer token then the actual received amount to the vault will be less than what is referred in the Vault.deposit
function _assets
input parameter. But the shares to mint is calculated using the entire _assets
amount.
This issue could be further aggravated since the _asset
is again deposited
to the _yieldVault
and when needing to be redeemed will be withdrawn
from the _yieldVault
as well. These operations will again charge a fee if the _asset
is a fee on transfer token. Hence the actual left _asset
amount for particular user will be less than the amount he initially transferred in.
Hence when the user redeems
the minted shares back to the _assets
, the contract will not have enough assets to transfer to the redeemer
thus reverting the transaction.
SafeERC20.safeTransferFrom( _asset, _caller, address(this), _assetsDeposit != 0 ? _assetsDeposit : _assets );
_yieldVault.deposit(_assets, address(this));
_yieldVault.withdraw(_assets, address(this), address(this)); SafeERC20.safeTransfer(IERC20(asset()), _receiver, _assets);
Manual Review and VSCode
Hence it is recommended to compute the _assets
amount balance of the contract before and after the safeTransferFrom
call and get the difference between the two as the actually transferred amount to the Vault
. Then this actually transferred amount can be converted to shares and mint the correct amount of shares to the recipient
.
uint256 balanceBefore = _asset.balanceOf(address(this)); SafeERC20.safeTransferFrom( _asset, _caller, address(this), _assetsDeposit != 0 ? _assetsDeposit : _assets ); uint256 balanceAfter = _asset.balanceOf(address(this)); uint256 transferredAmount = balanceAfter - balanceBefore;
Other
#0 - c4-judge
2023-07-16T21:48:08Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-07-20T23:39:35Z
asselstine marked the issue as sponsor confirmed
#2 - c4-judge
2023-08-07T15:12:20Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-08-07T15:12:24Z
Picodes marked the issue as selected for report
#4 - Picodes
2023-08-08T10:09:09Z
Out of scope per the automated report: https://gist.github.com/itsmetechjay/e7fd03943bbacff1984a33b9f89c4149 (MEDIUM-4)
#5 - c4-judge
2023-08-08T10:10:46Z
Picodes marked the issue as unsatisfactory: Out of scope
#6 - udsene
2023-08-10T14:59:26Z
@Picodes, The fee on transfer token vulnerability described in this issue (#470) is related to the token transfers happening in the Vault.sol
contract. How the fee charged on token transfers could effect the Vault._deposit
, _yieldVault.deposit
and _yieldVault.withdraw
functions.
The MEDIUM-4
given in the automated report
has found one instance of this vulnerability (fee on token transfer) and it is related to the PrizePool.sol
contract and not the Vault.sol
contract. It is not the same vulnerability present in the #470 issue.
#7 - Picodes
2023-08-12T16:00:00Z
Indeed. The automated report didn't properly flag that Vault.sol
won't work with FoT.
#8 - c4-judge
2023-08-12T16:00:16Z
Picodes marked the issue as selected for report
#9 - c4-judge
2023-08-12T16:00:25Z
Picodes marked the issue as satisfactory
🌟 Selected for report: wvleak
Also found by: 0xbepresent, 3docSec, DadeKuma, Jeiwan, Udsen, dirk_y, keccak123, rvierdiiev, serial-coder, shaka, wvleak
20.0427 USDC - $20.04
https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L618-L629 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1053 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1068
The Vault.claimPrizes
function is used to claim prizes
for the winners
of the prizes. This is done by iterating through two nested for
loops. One for loop iterates through the number of winners and other for loop iterates through the _prizeIndices
.
Inside these nested loops the Vault._claimPrize()
internal function is called. In the Vault._claimPrize()
function, the hooks.useBeforeClaimPrize
and hooks.implementation.afterClaimPrize
hooks of a particular winner can be called. These hooks are set by the winner
himself calling the Vault.setHooks
function.
Hence a malicious winner
can revert
the transaction inside the hooks.useBeforeClaimPrize
or hooks.implementation.afterClaimPrize
hook function, thus making the entire prize claim for other winners to be Denied as well(DoS).
This will make it impossible for the _claimer
to distribute the prizes among the winners.
for (uint w = 0; w < _winners.length; w++) { uint prizeIndicesLength = _prizeIndices[w].length; for (uint p = 0; p < prizeIndicesLength; p++) { totalPrizes += _claimPrize( _winners[w], _tier, _prizeIndices[w][p], _feePerClaim, _feeRecipient ); } }
recipient = hooks.implementation.beforeClaimPrize(_winner, _tier, _prizeIndex);
hooks.implementation.afterClaimPrize(
Manual Review and VSCode
Hence it is recommended to either omit the hook
functionality from the winners
or if hooks
are an essential part of the protocol then it is recommended to allow the winner
or a claimer
to claim the prizes individually rather than iterating through an array of all winners.
DoS
#0 - Picodes
2023-07-15T08:31:28Z
What prevents the claimer
from just skipping the malicious hooks using the _winners
array?
#1 - c4-judge
2023-07-15T08:31:32Z
Picodes changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-07-18T17:14:47Z
Picodes marked the issue as duplicate of #465
#3 - c4-judge
2023-08-07T15:13:25Z
Picodes marked the issue as satisfactory
#4 - c4-judge
2023-08-07T15:13:44Z
Picodes marked the issue as partial-50
🌟 Selected for report: Udsen
Also found by: 0x11singh99, 0xPsuedoPandit, Daniel526, Darwin, Inspecktor, Jorgect, Nyx, Praise, Tripathi, YY, catellatech, namx05, squeaky_cactus, xuwinnie
25.0727 USDC - $25.07
https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L278-L281 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L299-L306 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L348 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L335-L342
In the PrizePool.constructor
the drawManager
state variable is set as follows:
drawManager = params.drawManager;
But it accepts even if the params.drawManager == address(0)
. There is no input validation for the address(0) as well.
The PrizePool.setDrawManager
function is used to set the drawManager
if not already set. But the problem here is that there is no access control for this function and any one can call this. A malicious user can front run the valid
drawManager
assignment transaction and set a malicious address as the drawManager
. The other issue is once the drawManager
is set it can not be updated due to the following conditional check. Hence the contract will have to redeployed.
if (drawManager != address(0)) { //@audit-issue - can be front run by a malicious user revert DrawManagerAlreadySet(); }
Hence the following two functions controlled by the onlyDrawManager
modifier will be vulnerable to attacks, since the drawManager
is a malicious address now.
PrizePool.withdrawReserve can withdraw tokens from the reserve
to any address given in the _to
parameter. Hence this function will be vulnerable.
PrizePool.closeDraw()
function can close the current open draw. This function is only callable by the drawManager
. Hence a malicious user can get control of this function thus making this function vulnerable.
drawManager = params.drawManager; if (params.drawManager != address(0)) { emit DrawManagerSet(params.drawManager); }
function setDrawManager(address _drawManager) external { if (drawManager != address(0)) { revert DrawManagerAlreadySet(); } drawManager = _drawManager; emit DrawManagerSet(_drawManager); }
function closeDraw(uint256 winningRandomNumber_) external onlyDrawManager returns (uint16) {
function withdrawReserve(address _to, uint104 _amount) external onlyDrawManager { if (_amount > _reserve) { revert InsufficientReserve(_amount, _reserve); } _reserve -= _amount; _transfer(_to, _amount); emit WithdrawReserve(_to, _amount); }
Manaul Review and VSCode
Hence it is recommended to check for address(0)
in the constructor when drawManager
is set or to implement access control
in the PrizePool.setDrawManager
function so that only the admin of the contract can call this function to set the drawManager
if it is not set already.
Invalid Validation
#0 - c4-judge
2023-07-16T16:16:23Z
Picodes marked the issue as duplicate of #356
#1 - c4-judge
2023-08-06T10:32:06Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-08-06T10:33:03Z
Picodes marked the issue as selected for report
#3 - asselstine
2023-08-17T21:22:02Z
🌟 Selected for report: c3phas
Also found by: 0x11singh99, 0xAnah, 0xn006e7, LeoS, Rageur, Raihan, ReyAdmirado, Rolezn, SAAJ, SAQ, SM3_SS, Udsen, alymurtazamemon, hunter_w3b, koxuan, naman1778, petrichor, ybansal2403
247.4795 USDC - $247.48
_reserve -= _amount;
can be unchecked to save gas, since previous conditional check makes sure that _amount <= _reserve
.
https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L336-L339 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/abstract/TieredLiquidityDistributor.sol#L671 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/abstract/TieredLiquidityDistributor.sol#L538 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L459 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L490
PrizePool._computeNextNumberOfTiers()
function.In the PrizePool._computeNextNumberOfTiers()
the following code snippet is used to update the _nextNumberOfTiers
value as shown below:
_nextNumberOfTiers = _nextNumberOfTiers > MINIMUM_NUMBER_OF_TIERS ? _nextNumberOfTiers : MINIMUM_NUMBER_OF_TIERS;
There is a redundant assignment of the _nextNumberOfTiers
variable to itself when _nextNumberOfTiers > MINIMUM_NUMBER_OF_TIERS
.
This can be optimized as follows:
if(!(_nextNumberOfTiers > MINIMUM_NUMBER_OF_TIERS)){ _nextNumberOfTiers = MINIMUM_NUMBER_OF_TIERS; }
This will save an extra MLOAD
.
The PrizePool._computeVaultTierDetails
function is used to compute the data to determine the winner of a prize. In the function there is an internal call made to teh _tierOdds
function as shown below:
tierOdds = _tierOdds(_tier, numberOfTiers);
The numberOfTiers
used in this function call is a state variable. Inplace of this state variable the _numberOfTiers
memory variable can be used instead. This will save an extra SLOAD
.
The Vault._currentExchangeRate
function, is used to calculate the exchange rate between the amount of assets withdrawable from the YieldVault and the amount of shares minted by this Vault. In the function there is a calculation as follows to compute the _withdrawableAssets
when _withdrawableAssets > _totalSupplyToAssets
, as shown below:
_withdrawableAssets = _withdrawableAssets - (_withdrawableAssets - _totalSupplyToAssets);
Here the yield
is removed from the _withdrawableAssets
amount.
The above calculation can be simplified as follows to save gas: and the above calculation can be commented for ease of understanding later.
_withdrawableAssets = _totalSupplyToAssets;
In the Vault.liquidate
function the following check is performed to verify that _tokenOut
is the vault share.
if (_tokenOut != address(this)) revert LiquidationTokenOutNotVaultShare(_tokenOut, address(this));
But the same check check is performed inside the _liquidatableBalanceOf(_tokenOut)
function call as well. Hence the above mentioned check inside the Vault.liquidate
is a redundant check that can be omitted to save gas.
https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L562-L566 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L830
Vault.claimPrizes
COULD LEAD TO WASTE OF GASVault.claimPrizes
is used to calim the prizes for the winners. There are two arrays used in the function as shown below:
address[] calldata _winners, uint32[][] calldata _prizeIndices,
These two arrays are used iterate through inside a nested for
loop. But before the for
loop the length of the above two arrays are not checked for equality. Hence if there is an array length mismath the transaction will revert after entering the for
loop thus leading to a considerable gas loss to the _claimer
.
Hence it is recommended to check _winners.length == _prizeIndices.length
at the beginning of the function before entering the for
loop.
#0 - c4-judge
2023-07-18T18:55:12Z
Picodes marked the issue as grade-a
#1 - c4-sponsor
2023-07-20T23:11:49Z
asselstine marked the issue as sponsor confirmed
#2 - PierrickGT
2023-09-08T23:50:18Z
1 - Fixed in the following PR: https://github.com/GenerationSoftware/pt-v5-prize-pool/pull/63 2, 3, 4, 5 - Has been fixed 6 - Arrays are not of the same length