Platform: Code4rena
Start Date: 08/05/2023
Pot Size: $90,500 USDC
Total HM: 17
Participants: 102
Period: 7 days
Judge: 0xean
Total Solo HM: 4
Id: 236
League: ETH
Rank: 44/102
Findings: 3
Award: $219.27
π Selected for report: 0
π Solo Findings: 0
π Selected for report: Team_Rocket
Also found by: 0xkazim, BPZ, Bauchibred, BoltzmannBrain, Brenzee, DeliChainSec, Franfran, Lilyjjo, MohammedRizwan, SaeedAlipoor01988, Yardi256, ast3ros, berlin-101, carlitox477, fs0c, peritoflores, sashik_eth, sces60107, thekmj, volodya, zzykxx
66.5871 USDC - $66.59
The current implementation of the protocol uses blocks rather than seconds to measure time between interest accruals. This makes the implementation highly sensitive to changes in the average time between Ethereum blocks.
On line 23 of BaseJumpRateModelV2
it is implicitly assumed that the time between blocks is 3 seconds(bnb chain need 3 seconds to mine block). However, the average time between blocks can change dramatically. For example, the average time between blocks may increase by significant factors due to the difficulty bomb or decrease by significant factors during the transition to Serenity. The difference between the actual time between blocks and the assumed time between blocks causes proportional differences between the intended interest rates and the actual interest rates. While it is possible for the admin to combat this by adjusting the interest rate model when the average time between blocks changes, such adjustments are manual and happen only after-the-fact. Errors in blocktime assumptions are cumulative, and fixing the model after-the-fact does not make users whole β it only prevents incorrect interest calculations moving forward (until the next change in blocktime).
similar case was submitted by OZ team when they auditing compound protocol : https://blog.openzeppelin.com/compound-audit/
manual review / solodit(reviewing OZ reports)
Consider refactoring the implementation to use seconds rather than blocks to measure the time between accruals. While block.timestamp
can be manipulated by miners within a narrow window, these errors are small and, importantly, are not cumulative. This would decouple the interest rate model from Ethereumβs average blocktime.
Timing
#0 - c4-judge
2023-05-16T09:21:58Z
0xean marked the issue as duplicate of #559
#1 - c4-judge
2023-06-05T14:34:49Z
0xean marked the issue as satisfactory
#2 - c4-judge
2023-06-05T14:38:31Z
0xean changed the severity to 3 (High Risk)
π Selected for report: Team_Rocket
Also found by: 0xkazim, BPZ, Bauchibred, BoltzmannBrain, Brenzee, DeliChainSec, Franfran, Lilyjjo, MohammedRizwan, SaeedAlipoor01988, Yardi256, ast3ros, berlin-101, carlitox477, fs0c, peritoflores, sashik_eth, sces60107, thekmj, volodya, zzykxx
66.5871 USDC - $66.59
the blocksPerYear
is set to 2102400
in the WhitePaperInterestRateModel
this should be equal to number of blocks per year that is assumed by the interest rate model, but the number of block is set incorrectly and it's not equal to block per year.
in the contract WhitePaperInterestRateModel
the block per year is set to 2102400
same as the compound protocol do, but this won't work as it's for the compound protocol, this number will be equal to block number per year only if the contract/protocol is deploying on Ethereum blockchain because it need 15-13 seconds to mine a block which 2102400
will be equal to 31,536,000 second or 365 days(compound v2 is deployed when ethereum mining time for one block was15 seconds) but this will be 73 days on bnb chain because bnb need 3 seconds to mine a block which mean that 2102400
blocks will be equal to 6,307,200 seconds or 73 days( not one year) this will cause a may problem into the protocol because it set block per 73 days not 365 days and the WhitePaperInterestRateModelFactory
will create WhitePaperInterestRateModel
depending on this informations.
manual review
i recommend to set the correct block number which is 10512000
that equal to 31,536,000 seconds or 365 days, this block number is set in the BaseJumpRateModelV2
contract so i recommend to do the same thing in the WhitePaperInterestRateModel
Timing
#0 - c4-judge
2023-05-16T09:21:43Z
0xean marked the issue as duplicate of #559
#1 - c4-judge
2023-06-05T14:02:53Z
0xean marked the issue as satisfactory
#2 - c4-judge
2023-06-05T14:38:31Z
0xean changed the severity to 3 (High Risk)
π Selected for report: 0x73696d616f
Also found by: 0xadrii, 0xbepresent, 0xkazim, J4de, peanuts, pontifex, rvierdiiev, volodya
96.0525 USDC - $96.05
https://github.com/code-423n4/2023-05-venus/blob/8be784ed9752b80e6f1b8b781e2e6251748d0d7e/contracts/RiskFund/RiskFund.sol#L151-L182 https://github.com/code-423n4/2023-05-venus/blob/8be784ed9752b80e6f1b8b781e2e6251748d0d7e/contracts/RiskFund/RiskFund.sol#L235
when a call made to the swapPoolsAssets
function this function will call the _swapAsset
function to doing the swap, if you check this function you will recognized that the balanceOfUnderlyingAsset
is set before the update of the address(vToken)
which may cause using a old price than it expected.
ComptrollerViewInterface(comptroller).oracle().updatePrice(address(vToken));
when the swapPoolsAssets
function is called this function will call the _swapAsset
function
function swapPoolsAssets( address[] calldata markets, uint256[] calldata amountsOutMin, address[][] calldata paths ) external override returns (uint256) { _checkAccessAllowed("swapPoolsAssets(address[],uint256[],address[][])"); require(poolRegistry != address(0), "Risk fund: Invalid pool registry."); require(markets.length == amountsOutMin.length, "Risk fund: markets and amountsOutMin are unequal lengths"); require(markets.length == paths.length, "Risk fund: markets and paths are unequal lengths"); uint256 totalAmount; uint256 marketsCount = markets.length; _ensureMaxLoops(marketsCount); for (uint256 i; i < marketsCount; ++i) { VToken vToken = VToken(markets[i]); address comptroller = address(vToken.comptroller()); PoolRegistry.VenusPool memory pool = PoolRegistry(poolRegistry).getPoolByComptroller(comptroller); require(pool.comptroller == comptroller, "comptroller doesn't exist pool registry"); require(Comptroller(comptroller).isMarketListed(vToken), "market is not listed"); uint256 swappedTokens = _swapAsset(vToken, comptroller, amountsOutMin[i], paths[i]); poolReserves[comptroller] = poolReserves[comptroller] + swappedTokens; totalAmount = totalAmount + swappedTokens; } emit SwappedPoolsAssets(markets, amountsOutMin, totalAmount); return totalAmount; }
2-this function will all the _swapAsset
function to make the swap as it show in the code :
function _swapAsset( VToken vToken, address comptroller, uint256 amountOutMin, address[] calldata path ) internal returns (uint256) { //audit require(amountOutMin != 0, "RiskFund: amountOutMin must be greater than 0 to swap vToken"); require(amountOutMin >= minAmountToConvert, "RiskFund: amountOutMin should be greater than minAmountToConvert"); uint256 totalAmount; //@audit-info updatePrice should be updated before address underlyingAsset = VTokenInterface(address(vToken)).underlying(); uint256 balanceOfUnderlyingAsset = poolsAssetsReserves[comptroller][underlyingAsset]; ComptrollerViewInterface(comptroller).oracle().updatePrice(address(vToken)); uint256 underlyingAssetPrice = ComptrollerViewInterface(comptroller).oracle().getUnderlyingPrice( address(vToken) ); if (balanceOfUnderlyingAsset > 0) { Exp memory oraclePrice = Exp({ mantissa: underlyingAssetPrice }); uint256 amountInUsd = mul_ScalarTruncate(oraclePrice, balanceOfUnderlyingAsset); if (amountInUsd >= minAmountToConvert) { assetsReserves[underlyingAsset] -= balanceOfUnderlyingAsset; poolsAssetsReserves[comptroller][underlyingAsset] -= balanceOfUnderlyingAsset; if (underlyingAsset != convertibleBaseAsset) { require(path[0] == underlyingAsset, "RiskFund: swap path must start with the underlying asset"); require( path[path.length - 1] == convertibleBaseAsset, "RiskFund: finally path must be convertible base asset" ); IERC20Upgradeable(underlyingAsset).safeApprove(pancakeSwapRouter, 0); IERC20Upgradeable(underlyingAsset).safeApprove(pancakeSwapRouter, balanceOfUnderlyingAsset); uint256[] memory amounts = IPancakeswapV2Router(pancakeSwapRouter).swapExactTokensForTokens( balanceOfUnderlyingAsset, amountOutMin, path, address(this), block.timestamp ); totalAmount = amounts[path.length - 1]; } else { totalAmount = balanceOfUnderlyingAsset; } } } return totalAmount; }
3-as you can see the function is using balanceOfUnderlyingAsset
in many places in the function,for example it used for calculating usdAmount. the problem is that the balanceOfUnderlyingAsset
is set before the oracle price update, this will cause to use a old price than the oracle price and the time between them is on of the reason to allow this problem to happeen, the function is using balanceOfUnderlyingAsset
which is set before the update happen:
address underlyingAsset = VTokenInterface(address(vToken)).underlying(); uint256 balanceOfUnderlyingAsset = poolsAssetsReserves[comptroller][underlyingAsset]; ComptrollerViewInterface(comptroller).oracle().updatePrice(address(vToken));
as you can see the balance for the underlying asset will set before updating the address(vToken)
which is used in the underlyingAsset
, this may cause of using old price/balance in this function.
manual review
always recommended to doing the oracle updates and price updates before setting any balance/price.
ComptrollerViewInterface(comptroller).oracle().updatePrice(address(vToken)); address underlyingAsset = VTokenInterface(address(vToken)).underlying(); uint256 balanceOfUnderlyingAsset = poolsAssetsReserves[comptroller][underlyingAsset];
Oracle
#0 - c4-judge
2023-05-18T11:07:46Z
0xean marked the issue as duplicate of #88
#1 - c4-judge
2023-06-05T14:09:51Z
0xean marked the issue as satisfactory
#2 - c4-judge
2023-06-08T14:39:50Z
0xean marked the issue as duplicate of #486
#3 - c4-judge
2023-06-08T14:41:51Z
0xean marked the issue as partial-50
π Selected for report: brgltd
Also found by: 0x73696d616f, 0xAce, 0xSmartContract, 0xWaitress, 0xkazim, 0xnev, Aymen0909, BGSecurity, Bauchibred, Cayo, ChrisTina, Franfran, IceBear, Infect3d, Kose, Lilyjjo, PNS, RaymondFam, Sathish9098, Team_Rocket, Udsen, YakuzaKiawe, YoungWolves, berlin-101, bin2chen, btk, codeslide, fatherOfBlocks, frazerch, kodyvim, koxuan, lfzkoala, lukris02, matrix_0wl, nadin, naman1778, sashik_eth, tnevler, volodya, wonjun, yjrwkk
56.6347 USDC - $56.63
ID | Title | Severity |
---|---|---|
[L-01] | unncessary set of unnessasry exchange rate to memory | low |
[L-02] | adding max Loop limit to the preMintHook function to avoid DOS if | low |
there is no need to set the exchange rate to memory in the preMintHook
in the comptroller contract because we add it then into storage again.
file: contract/shortfall.shorFall.sol function preMintHook( address vToken, address minter, uint256 mintAmount ) external override { _checkActionPauseState(vToken, Action.MINT); if (!markets[vToken].isListed) { revert MarketNotListed(address(vToken)); } uint256 supplyCap = supplyCaps[vToken]; // Skipping the cap check for uncapped coins to save some gas if (supplyCap != type(uint256).max) { uint256 vTokenSupply = VToken(vToken).totalSupply(); //@audit unnessasry exchange rate ! Exp memory exchangeRate = Exp({ mantissa: VToken(vToken).exchangeRateStored() }); uint256 nextTotalSupply = mul_ScalarTruncateAddUInt(exchangeRate, vTokenSupply, mintAmount); if (nextTotalSupply > supplyCap) { revert SupplyCapExceeded(vToken, supplyCap); } } ...
recommend to not set the exchangeRate
into memory
preMintHook
function to avoid DOS if it have possibility to happenadding _ensureMaxLoops
will be a good choice to the preMintHook
function to avoid any DOS that may happen to the function where there is a lot of call into mints functions
function preMintHook( address vToken, address minter, uint256 mintAmount ) external override { _checkActionPauseState(vToken, Action.MINT);
if (!markets[vToken].isListed) { revert MarketNotListed(address(vToken)); } uint256 supplyCap = supplyCaps[vToken]; // Skipping the cap check for uncapped coins to save some gas if (supplyCap != type(uint256).max) { uint256 vTokenSupply = VToken(vToken).totalSupply(); Exp memory exchangeRate = Exp({ mantissa: VToken(vToken).exchangeRateStored() }); uint256 nextTotalSupply = mul_ScalarTruncateAddUInt(exchangeRate, vTokenSupply, mintAmount); if (nextTotalSupply > supplyCap) { revert SupplyCapExceeded(vToken, supplyCap); } } // Keep the flywheel moving uint256 rewardDistributorsCount = rewardsDistributors.length; //audit add max loop here ! for (uint256 i; i < rewardDistributorsCount; ++i) { rewardsDistributors[i].updateRewardTokenSupplyIndex(vToken); rewardsDistributors[i].distributeSupplierRewardToken(vToken, minter); } }
add _ensureMaxLoops
to this function to avoid DOS in some cases.
#0 - c4-judge
2023-05-18T18:19:20Z
0xean marked the issue as grade-c
#1 - 0xean
2023-05-18T19:46:14Z
upgrading to B based on other issues that warden submitted that are now QA
#2 - c4-judge
2023-05-18T19:46:18Z
0xean marked the issue as grade-b