Platform: Code4rena
Start Date: 31/01/2023
Pot Size: $90,500 USDC
Total HM: 47
Participants: 169
Period: 7 days
Judge: LSDan
Total Solo HM: 9
Id: 211
League: ETH
Rank: 101/169
Findings: 2
Award: $40.09
π Selected for report: 0
π Solo Findings: 0
π Selected for report: 0xdeadbeef0x
Also found by: 0Kage, 0xNazgul, 0xRobocop, Aymen0909, KIntern_NA, Kenshin, KingNFT, Krace, Kumpa, SadBase, aashar, apvlki, btk, cccz, critical-or-high, eccentricexit, fs0c, gjaldon, hansfriese, immeas, mert_eren, mgf15, mrpathfindr, orion, peanuts, rvi0x, rvierdiiev, supernova, ulqiorra, waldenyan20, y1cunhui
4.6115 USDC - $4.61
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L170-L186
For rewardTokens
that implement callback mechanism like calling onERC20Received
function after transfer, the function claimRewards()
in MultiRewardStaking.sol
can be reentered to drain this rewardToken from the pool.
File: src/utils/MultiRewardStaking.sol 170 function claimRewards(address user, IERC20[] memory _rewardTokens) external accrueRewards(msg.sender, user) { for (uint8 i; i < _rewardTokens.length; i++) { uint256 rewardAmount = accruedRewards[user][_rewardTokens[i]]; if (rewardAmount == 0) revert ZeroRewards(_rewardTokens[i]); EscrowInfo memory escrowInfo = escrowInfos[_rewardTokens[i]]; if (escrowInfo.escrowPercentage > 0) { _lockToken(user, _rewardTokens[i], rewardAmount, escrowInfo); emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, true); } else { //@audit reentrancy here _rewardTokens[i].transfer(user, rewardAmount); emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, false); } accruedRewards[user][_rewardTokens[i]] = 0; } }
onERC20Received
rewardAmout
> 0claimReward
with corresponding _rewardToken
. Assuming the escrowPercentage <= 0
and it goes to the line with comment.claimRewards
again. By this time, the accruedRewards
has not been zeroed; and in the accruedRewards(msg.sender, _user)
modifier, there is no way the accruedRewards
will be decreased. So the user get the same rewardAmount
again and again, until gas drained or RewardToken drained.Manual Review, VS Code
The vulnerability is because the developer neither used the ReentrancyGuard
modifier, nor follow the CEI (check-effects-interactions) pattern, since the accuedRewards
zeroed after the transfer call.
As a result, the mitigation steps is just add ReentrancyGuard
modifier to this function, or move the line 186 to line 175. Always check functions with external calls, not just ether send/transfer.
#0 - c4-judge
2023-02-16T07:39:10Z
dmvt marked the issue as duplicate of #54
#1 - c4-sponsor
2023-02-18T12:10:59Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-sponsor
2023-02-18T12:11:59Z
RedVeil marked the issue as disagree with severity
#3 - c4-judge
2023-02-23T00:50:38Z
dmvt marked the issue as partial-50
#4 - c4-judge
2023-03-01T00:32:49Z
dmvt marked the issue as full credit
#5 - c4-judge
2023-03-01T00:44:15Z
dmvt marked the issue as satisfactory
π Selected for report: IllIllI
Also found by: 0x3b, 0xAgro, 0xBeirao, 0xMirce, 0xNineDec, 0xRobocop, 0xSmartContract, 0xTraub, 0xWeiss, 2997ms, 41i3xn, Awesome, Aymen0909, Bauer, Bnke0x0, Breeje, Cryptor, DadeKuma, Deathstore, Deekshith99, DevABDee, DevTimSch, Dewaxindo, Diana, Ermaniwe, Guild_3, H0, IceBear, Inspectah, JDeryl, Kaiziron, Kaysoft, Kenshin, Mukund, Praise, RaymondFam, Rickard, Rolezn, Ruhum, Sathish9098, SkyWalkerMan, SleepingBugs, UdarTeam, Udsen, Walter, aashar, adeolu, apvlki, arialblack14, ast3ros, btk, chaduke, chandkommanaboyina, chrisdior4, climber2002, codetilda, cryptonue, cryptostellar5, csanuragjain, ddimitrov22, descharre, dharma09, doublesharp, eccentricexit, ethernomad, fs0c, georgits, halden, hansfriese, hashminer0725, immeas, lukris02, luxartvinsec, matrix_0wl, merlin, mookimgo, mrpathfindr, nadin, olegthegoat, pavankv, rbserver, rebase, savi0ur, sayan, scokaf, seeu, shark, simon135, tnevler, tsvetanovv, ulqiorra, ustas, waldenyan20, y1cunhui, yongskiws, yosuke
35.4779 USDC - $35.48
When addToken
and changeRewardSpeed
, there is no boundry for rewardPerSecond
. Maybe some basic boundry should be added.
File: src/utils/MultiRewardStaking.sol function addRewardToken( IERC20 rewardToken, uint160 rewardsPerSecond, uint256 amount, bool useEscrow, uint192 escrowPercentage, uint32 escrowDuration, uint32 offset ) external onlyOwner function changeRewardSpeed(IERC20 rewardToken, uint160 rewardsPerSecond) external onlyOwner
addTemplate
As the comment of theser two function says: * @notice Adds a new category for templates. Caller must be owner.
But in below code snippet, there is no control over caller:
File: src/vault/DeploymentController.sol function addTemplate( bytes32 templateCategory, bytes32 templateId, Template calldata template ) external { templateRegistry.addTemplate(templateCategory, templateId, template); }
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/DeploymentController.sol#L66
Since the comment here says it should only be called by owner, and in the TemplateRegistry.sol
, this function has the modifier onlyOwner
where the owner is just the DeploymentController
above. Missing modifier here will make the permisson control in TemplateRegistry
useless.
When 1e18=100%
, 1 BPS
should be 1e14
as other comments of this file says.
File: src/vault/ValutController.sol /** * @notice Sets new fees per vault. Caller must be creator of the vaults. * @param vaults Addresses of the vaults to change * @param fees New fee structures for these vaults * @dev Value is in 1e18, e.g. 100% = 1e18 - 1 BPS = 1e12 */
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L350
owner
Parameter name owner
is used here:
File: src/utils/MultiRewardStaking.sol 121 function _withdraw( address caller, address receiver, address owner, uint256 assets, uint256 shares ) internal override accrueRewards(caller, receiver) { if (caller != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); _burn(owner, shares); IERC20(asset()).safeTransfer(receiver, assets); emit Withdraw(caller, receiver, owner, assets, shares); }
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L121
However, since this contract is inherited from OwnedUpgradeable
, where there is also a state variable named owner
:
contract OwnedUpgradeable is Initializable { address public owner; address public nominatedOwner; ...
Consider change the parameter name of function _withdraw
.
Just list some of them below for example:
File: src/utils/MultiRewardStaking.sol _name = string(abi.encodePacked("Staked ", IERC20Metadata(address(_stakingToken)).name())); _symbol = string(abi.encodePacked("pst-", IERC20Metadata(address(_stakingToken)).symbol())); function name() public view override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { function claimRewards(address user, IERC20[] memory _rewardTokens) external accrueRewards(msg.sender, user) {
#0 - c4-judge
2023-02-28T15:00:51Z
dmvt marked the issue as grade-b