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: 42/102
Findings: 2
Award: $248.74
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: berlin-101
Also found by: 0xadrii, Audit_Avengers_2, Emmanuel, Team_Rocket, YungChaza, bin2chen, fs0c, sashik_eth
192.105 USDC - $192.11
https://github.com/code-423n4/2023-05-venus/blob/main/contracts/Shortfall/Shortfall.sol#L183 https://github.com/code-423n4/2023-05-venus/blob/main/contracts/Shortfall/Shortfall.sol#L190
Adversary will gain RiskFund's assets in an unfair way.
In LINE 183 and LINE 190 of Shortfall contract, the protocol tries to send the previous highest bidder's tokens back to him when someone places a higher bid.
erc20.safeTransfer(auction.highestBidder, previousBidAmount);
The problem is, a user can easily make this call to revert and DoS the protocol from sending him his funds, effectively preventing subsequent bidders from bidding. He can do this in two ways:
The user will start an auction, place a malicious bid of auction.startBidBPS, then call closeAuction when he can.
This would make him gain RiskFund's convertibleBaseAmount
at just auction.startBidBPS instead of allowing those who are ready to pay more to win the auction.
Manual Review
Consider providing a mechanism through which previous highest bidders will manually withdraw their funds. For example:
amountToWithdraw
balance in a mappingDoS
#0 - c4-judge
2023-05-18T11:40:25Z
0xean marked the issue as duplicate of #376
#1 - c4-judge
2023-06-05T14:05:07Z
0xean marked the issue as satisfactory
🌟 Selected for report: berlin-101
Also found by: 0xadrii, Audit_Avengers_2, Emmanuel, Team_Rocket, YungChaza, bin2chen, fs0c, sashik_eth
192.105 USDC - $192.11
https://github.com/code-423n4/2023-05-venus/blob/main/contracts/Rewards/RewardsDistributor.sol#L277-L292 https://github.com/code-423n4/2023-05-venus/blob/main/contracts/Rewards/RewardsDistributor.sol#L416-L423
Attacker can drain all rewardToken
s from RewardsDistributor
contract
The claimRewardToken
function calls an internal function _grantRewardToken
Here is the claimRewardToken
function:
function claimRewardToken(address holder, VToken[] memory vTokens) public { uint256 vTokensCount = vTokens.length; _ensureMaxLoops(vTokensCount); for (uint256 i; i < vTokensCount; ++i) { VToken vToken = vTokens[i]; require(comptroller.isMarketListed(vToken), "market must be listed"); Exp memory borrowIndex = Exp({ mantissa: vToken.borrowIndex() }); _updateRewardTokenBorrowIndex(address(vToken), borrowIndex); _distributeBorrowerRewardToken(address(vToken), holder, borrowIndex); _updateRewardTokenSupplyIndex(address(vToken)); _distributeSupplierRewardToken(address(vToken), holder); } rewardTokenAccrued[holder] = _grantRewardToken(holder, rewardTokenAccrued[holder]); }
And the _grantRewardToken
function:
function _grantRewardToken(address user, uint256 amount) internal returns (uint256) { uint256 rewardTokenRemaining = rewardToken.balanceOf(address(this)); if (amount > 0 && amount <= rewardTokenRemaining) { rewardToken.safeTransfer(user, amount); return 0; } return amount; }
_grantRewardToken
sends amount==rewardTokenAccrued[user]
to the recipient address, then returns a value(0 or amount) which will be used to update rewardTokenAccrued[user]
in claimRewardToken
function.
When the rewardToken
is an ERC777 token and recipient is a contract, rewardToken.safeTransfer(user, amount);
in _grantRewardToken
function, will call a tokensReceived
hook in the recipient contract.
The malicious contract creator would have programmed tokensReceived
to call claimRewardToken
again, and continue the loop until all the rewards has been drained.
This reentrancy is possible because the protocol only updates rewardTokenAccrued
, that an address can claim, to 0 after the token has been sent to the address, which allows the malicious contract to recall claimRewardToken
before its rewardTokenAccrued
is set to 0.
Manual Review
Consider adding a nonReentrant
modifier to the claimRewardToken
function:
bool public nonEntered=true; modifier nonReentrant() { require(_notEntered, "re-entered"); _notEntered = false; _; _notEntered = true; // get a gas-refund post-Istanbul } ... function claimRewardToken(address holder, VToken[] memory vTokens) public nonReentrant { ... }
Reentrancy
#0 - c4-judge
2023-05-16T09:30:34Z
0xean marked the issue as unsatisfactory: Overinflated severity
#1 - Emedudu
2023-06-06T09:01:13Z
This issue explains how all rewards(ERC777 tokens) in the RewardsDistributor contract can be drained through reentrancy in claimRewardToken
function.
I think this is a valid high risk because rewards can be stolen.
This issue is same as issue 63
#2 - 0xean
2023-06-06T19:47:27Z
This is was closed due to it being submitted with a H severity, which is clearly overinflated. I will go ahead and mark it as a dupe of the other ERC777 issues
#3 - c4-judge
2023-06-06T19:48:03Z
0xean marked the issue as satisfactory
#4 - c4-judge
2023-06-06T19:48:09Z
0xean changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-06-06T19:48:19Z
0xean marked the issue as duplicate of #305