Platform: Code4rena
Start Date: 17/02/2022
Pot Size: $75,000 USDC
Total HM: 20
Participants: 39
Period: 7 days
Judges: moose-code, JasoonS
Total Solo HM: 13
Id: 89
League: ETH
Rank: 15/39
Findings: 4
Award: $1,009.45
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: danb
Also found by: Meta0xNull, Ruhum, cmichel, csanuragjain, hyh, kirk-baird, leastwood, minhquanym, robee, throttle
#0 - CloudEllie
2022-03-30T02:22:53Z
See report #19 to read the original submission.
#0 - CloudEllie
2022-03-30T02:25:43Z
For original submission, see #19
🌟 Selected for report: defsec
Also found by: 0v3rf10w, 0x0x0x, 0x1f8b, 0xwags, CertoraInc, Dravee, IllIllI, Meta0xNull, Nikolay, Omik, WatchPug, bobi, cccz, csanuragjain, danb, gzeon, hubble, hyh, itsmeSTYJ, jayjonah8, kenta, kirk-baird, leastwood, pauliax, peritoflores, rfa, robee, sorrynotsorry, ye0lde
431.226 USDC - $431.23
Recommendation: If contract does not have balance for particular withdrawal instance, keep that in pending object and try to complete the remaining ones
Recommendation: There should be a timelock after which withdraw can be called again otherwise this can be called repeatedly for small amounts If user has requested withdraw then he should only be able to call this function again after x timestamp
Consider withdraw function at InsuranceFund.sol#L62
if some big bad debt comes (seizeBadDebt at MarginAccount.sol#L368) then settlePendingObligation function which is called at withdraw function will consume most contract balance. This will reduce amount in balance()
Since withdraw amount is directly proportional to balance (uint amount = balance() * _shares / totalSupply();) so same shares will give less amount
Recommendation: Modify the function as below:
(uint80 round, int256 latestPrice, , uint256 latestTimestamp, uint80 answeredInRound) = _aggregator.latestRoundData(); require(feedPrice > 0, "Chainlink price <= 0"); require(answeredInRound >= round, "Stale price"); require(latestTimestamp != 0, "Round not complete");
Recommendation: Add below check
function setStablePrice(address underlying, int256 price) external onlyGovernance { require(price!=0,"Invalid price") requireNonEmptyAddress(underlying); stablePrice[underlying] = price; }
The getUnderlyingTwapPrice function at Oracle.sol#L67 is returning latestPrice when latestTimestamp < baseTimestamp.
Else it would goto previous rounds
This is incorrect. This function should return latestPrice when latestTimestamp = baseTimestamp
Recommendation: Modify the check like below
if (latestTimestamp <= baseTimestamp || round == 0) { return formatPrice(latestPrice); }
require(_oracle!=address(0), "Invalid address"); require(_clearingHouse!=address(0), "Invalid address"); require(_insuranceFund!=address(0), "Invalid address"); require(_marginAccount!=address(0), "Invalid address"); require(_vusd!=address(0), "Invalid address");
require(_registry!=address(0), "Invalid address"); require(_underlyingAsset!=address(0), "Invalid address"); require(_vamm!=address(0), "Invalid address");
require(_insuranceFund!=address(0), "Invalid address"); require(_marginAccount!=address(0), "Invalid address"); require(_vusd!=address(0), "Invalid address");
function getUnderlyingPrice(address underlying) virtual external view returns(int256 answer) { AggregatorV3Interface aggregator = AggregatorV3Interface(chainLinkAggregatorMap[underlying]); requireNonEmptyAddress(address(aggregator)); ... }
#0 - atvanguard
2022-02-26T07:02:22Z
Good report that includes duplicates from other severity 2 and 3 issues.
#1 - moose-code
2022-03-05T16:15:49Z
"There should be a timelock after which withdraw can be called again otherwise this can be called repeatedly for small amounts" - you could just use a sybil type attack which makes a timelock not effective to defend against what you want to defend against.
#2 - moose-code
2022-03-05T16:18:28Z
Looks like some things in here should be upgraded to beyond low. Will circle back after medium and high severity issue reviews
227.4932 USDC - $227.49
In processWithdrawals function at VUSD.sol#L64, modify i+=1 to ++i
In processWithdrawals function at VUSD.sol#L63, use unchecked at reserve -= withdrawal.amount; since we already know that reserve > withdrawal.amount
In withdraw function at VUSD.sol#L48, add a check require(amount!=0, "Invalid amount");
In mintWithReserve function at VUSD.sol#L43, add below require
require(amount!=0, "Invalid amount"); require(to!=address(0), "Invalid address");
function getLatestRoundData(AggregatorV3Interface _aggregator) internal view returns ( uint80, uint256 finalPrice, uint256 ) { (uint80 round, int256 latestPrice, , uint256 latestTimestamp, ) = _aggregator.latestRoundData(); finalPrice = uint256(latestPrice); while (latestPrice < 0) { requireEnoughHistory(round); (round, finalPrice, latestTimestamp) = getRoundData(_aggregator, round - 1); } return (round, finalPrice, latestTimestamp); }
In deposit function at InsuranceFund.sol#L39, add a condition require(amount!=0, "Invalid amount")
In withdraw function at InsuranceFund.sol#L62, add a check require( _shares!=0,"Invalid shares");
In liquidate function at ClearingHouse.sol#L140, updatePositions function is not required since this is already called in _liquidateMaker and _liquidateTaker function
In addMarginFor function at MarginAccount.sol#L149, add new check
require(idx >= supportedCollateral.length,"Invalid index");
In removeMargin function at MarginAccount.sol#L177, margin[idx][trader] -= amount.toInt256(); can be unchecked since contract has already checked margin[idx][trader] >= amount.toInt256()
In removeMargin function at MarginAccount.sol#L168, add a check require(amount!=0,"Invalid amount");
In settleBadDebt function at MarginAccount.sol#L362, modify require(vusdBal < 0, "Nothing to repay"); to require(vusdBal <= 0, "Nothing to repay"); since if vusdBal is 0 then also there is nothing to repay
In weightedAndSpotCollateral function at MarginAccount.sol#L524 add a check to see margin[i][trader]==0 as shown below
function weightedAndSpotCollateral(address trader) public view returns (int256 weighted, int256 spot) { ... for (uint i = 0; i < assets.length; i++) { _collateral = assets[i]; if(margin[i][trader]==0){continue} int numerator = margin[i][trader] * oracle.getUnderlyingPrice(address(assets[i].token)); ... } }
function liquidateFlexible(address trader, uint maxRepay, uint[] calldata idxs) external whenNotPaused { clearingHouse.updatePositions(trader); // credits/debits funding (buffer.status, buffer.repayAble, buffer.incentivePerDollar) = isLiquidatable(trader, false); if (buffer.status != IMarginAccount.LiquidationStatus.IS_LIQUIDATABLE) {return} for (uint i = 0; i < idxs.length; i++){ ... }
#0 - atvanguard
2022-02-26T08:24:27Z
Good suggestion.
#1 - moose-code
2022-03-05T15:31:17Z
Some nice less generic suggestions :+1: