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: 25/111
Findings: 3
Award: $896.46
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Aymen0909
Also found by: 0xWaitress, KupiaSec, wangxx2026
768.245 USDC - $768.25
liquidate at Vault wrongly uses liquidatable asset to compare with the _amountout which is shareToMint leading to potentially over-mint and under-collateralization
liquidate
is assumed to be called by liquidator, with _amountOut as the expected shares to mint for _account. This is verified by the line 584 _mint(_account, _amountOut)
.
In the context, the _amountOut
is checked against the _liquidableYield
to ensure the vault remains collateralized after the execution.
uint256 _liquidableYield = _liquidatableBalanceOf(_tokenOut); if (_amountOut > _liquidableYield) revert LiquidationAmountOutGTYield(_amountOut, _liquidableYield);
However when we dive into the function _liquidatableBalanceOf(_tokenOut)
, the function actually calculate the excess amount of assets, but not the excess amount of shares as a result of excess asset
_liquidatableBalanceOf, accounting the yieldFee from the result of availableYieldBalance()
function _liquidatableBalanceOf(address _token) internal view returns (uint256) { if (_token != address(this)) revert LiquidationTokenOutNotVaultShare(_token, address(this)); uint256 _availableYield = availableYieldBalance(); unchecked { return _availableYield -= _availableYieldFeeBalance(_availableYield); } }
As u can see, availableYieldBalance
returns the totalAsset, minus the asset equivalent from the current totalShare.
function availableYieldBalance() public view returns (uint256) { uint256 _assets = _totalAssets(); uint256 _sharesToAssets = _convertToAssets(_totalShares(), Math.Rounding.Down); return _sharesToAssets > _assets ? 0 : _assets - _sharesToAssets; }
If the share is worth more than 1 unit of underlying, which is typically the case for yield-bearing vault, The impact is that an over-mint happens. At worst, then the Vault would be rendered under-collateralized immediately after an liquidate
call.
Example:
_amountout
as 20.if (_amountOut > _liquidableYield) revert LiquidationAmountOutGTYield(_amountOut, _liquidableYield);
would pass, thus 20 shares are minted to the _account
.NA
Consider converting the _amountout
into share before minting, if the input is expected to be compared against asset. Alternatively convert the excess asset into excess share before comparing against the input (_amountout).
converting the _amountout
into share
... +++ uint256 _shareOut = _convertToShares(_amountOut, Math.Rounding.Down); _mint(_account, _shareOut);
In the main contest page, vault security is highlighted to be the main concern:
The Vaults hold user funds, so it's critical that there can be no loss of funds. The liquidator is able to liquidate the accrued yield on the vault, so it's a second possible ingress point for an attacker. We want to make sure that the liquidator can't access more than the yield, and that users can't access each other's principal.
With due respect, and to harden the invaraiant, i would also recommend to move the modifier _requireVaultCollateralized()
to the end of the liquidate
function to ensure post-liquidate collaterialization of the vault
Math
#0 - c4-judge
2023-07-14T22:41:56Z
Picodes marked the issue as primary issue
#1 - c4-judge
2023-07-14T22:43:46Z
Picodes marked issue #427 as primary and marked this issue as a duplicate of 427
#2 - c4-judge
2023-08-05T21:49:41Z
Picodes marked the issue as satisfactory
🌟 Selected for report: bin2chen
Also found by: 0x11singh99, 0xWaitress, 0xbepresent, ABAIKUNANBAEV, ArmedGoose, Bauchibred, DadeKuma, GREY-HAWK-REACH, GalloDaSballo, Inspecktor, Jeiwan, Kaysoft, MohammedRizwan, Rolezn, Vagner, alexzoid, alymurtazamemon, ayden, banpaleo5, catellatech, dacian, erebus, eyexploit, fatherOfBlocks, grearlake, joaovwfreire, keccak123, kutugu, lanrebayode77, markus_ether, nadin, naman1778, rvierdiiev, squeaky_cactus, volodya, yixxas
15.9228 USDC - $15.92
[L-1] At PrizePool, _isWinner
passes a uint16 of _drawDuration
to _getVaultUserBalanceAndTotalSupplyTwab()
which takes a uint256 of _drawDuration
.
function _isWinner( ... uint16 _drawDuration ) ... (uint256 _userTwab, uint256 _vaultTwabTotalSupply) = _getVaultUserBalanceAndTotalSupplyTwab( _vault, _user, _drawDuration );
_getVaultUserBalanceAndTotalSupplyTwab
function _getVaultUserBalanceAndTotalSupplyTwab( address _vault, address _user, uint256 _drawDuration )
Since the maximum value of _drawDuration is only 366 so the severity is low.
Recommendation Please align data type for consistency.
[L-2] estimatePrizeFrequencyInDraws returns uin256 but casted to uint16 at getTierAccrualDurationInDraws.
function getTierAccrualDurationInDraws(uint8 _tier) external view returns (uint16) { return uint16(TierCalculationLib.estimatePrizeFrequencyInDraws(_tierOdds(_tier, numberOfTiers))); }
function estimatePrizeFrequencyInDraws(SD59x18 _tierOdds) internal pure returns (uint256) { return uint256(fromSD59x18(sd(1e18).div(_tierOdds).ceil())); }
[QA-1] _tierOdds of tier 0 at every tier_num is the same, thus can be reduced to 1 variable
all tier odds at tier 0, for example TIER_ODDS_0_3 and TIER_ODDS_0_4 and so on so forth, has the same value 2739726027397260
, it's advisable to reduce the number of variable to TIER_ODDS_0 and return this for all _tierOdds(0, x).
function _tierOdds() { if (_tier == 0) return TIER_ODDS_0; ... ...
[QA-2] this line in Vault can be re-written for better clarity
if (_withdrawableAssets > _totalSupplyToAssets) { _withdrawableAssets = _withdrawableAssets - (_withdrawableAssets - _totalSupplyToAssets); }
is no different from making _withdrawableAssets
as _totalSupplyToAssets
if (_withdrawableAssets > _totalSupplyToAssets) { _withdrawableAssets = _totalSupplyToAssets; }
#0 - c4-judge
2023-07-18T19:22:46Z
Picodes marked the issue as grade-b
🌟 Selected for report: 0xStalin
Also found by: 0xSmartContract, 0xWaitress, 3docSec, ABAIKUNANBAEV, DadeKuma, Jeiwan, K42, catellatech, dirk_y, keccak123, peanuts, squeaky_cactus
112.2875 USDC - $112.29
I am submitting a report since i dont think this is an issue but would find it worth a second thought in terms of design:
the smoothing curve combined with calculation of _getVaultPortion may create confusion on the initial lower-than-expected probability of winning (In the initial stage of joining, vault does not stand as much chance winning big prizes as vaults that join earlier with same contribution)
The vaultPortion (and its user collectively), is calculated by its contribution over the entire contribution for a particular draw. This vaultPortion represents the entitled expectedAmount and is translated into the probability of winning.
When a vault makes some contribution, the amount is split into contribution for multiple upcoming draws using the smoothing factor and formula. Generally speaking the contribution is bigger in the beginning and decays gradually with the passage of time.
https://dev.pooltogether.com/protocol/next/design/prize-pool#yield-smoothing-example
During the calculation of a particular tier prize, based on the expected occurrence, or the (expected) duration the prize, it would trace back up to the duration to find the cumulative contribution of a vault relative to the cumulative overall contribution to compute the winning probability.
a vault would basically have negligible change of winning grand prize at the beginning. Since its contribution for the entire duration is 0, except after its contribution which is recent. This is fair, since its TWAB contribution would then gradually move up with the passage of time, just like any other contributors has experienced. However, this delay due to smoothing factor + retrospective calculation of winning probability is not specified (explicitly), and might not be expected by contributor.
Consider evenly distribute the contribution of a newly joined vault over a period of X draws (where X covers some 1 prize cycle, 365 in this set up).
The smoothing factor is nice, as it gets the new comer started winning smaller prizes as soon as it participated, however it seems to create an infinite residuals and potentially leave accounting problems for the future. It is advised to format the vesting/distribution schedule on a rigid percentage similar to the tier probability, such that the contribution runs completely at a finite number of draws.
8 hours
#0 - c4-judge
2023-07-18T18:50:14Z
Picodes marked the issue as grade-b
#1 - c4-sponsor
2023-07-18T22:33:47Z
asselstine marked the issue as sponsor disputed