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: 21/169
Findings: 4
Award: $1,142.87
🌟 Selected for report: 1
🚀 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
In MultiRewardStaking
the function claimRewards
doesn’t have nonReentrant
which makes it possible to re-enter the function. If one of the reward tokens in ERC-777
token, it is possible to re-enter and claim the reward again and again until the contract is drained out of those tokens.
When the function would be re-entered, it would call accrueRewards
again which would call _accrueUser
for _receiver and _caller, it would update the value of accruedRewards again as accruedRewards[_user][_rewardToken] += supplierDelta;
The second time supplierDelta
would be zero, but the accruedRewards would remain the same. This value is used later in the claimRewards function as rewardAmount
and sent to the user.
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 { _rewardTokens[i].transfer(user, rewardAmount); emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, false); } accruedRewards[user][_rewardTokens[i]] = 0; } }
In the above function the accruedRewards[user][_rewardTokens[i]]
is updated after the tokens are transfered to the user. This makes it possible to steal the rewards multiple times in case of ERC-777 (extension of ERC-20) tokens, which have a callback.
Add nonReentrant
modifier.
#0 - c4-judge
2023-02-16T07:40:21Z
dmvt marked the issue as duplicate of #54
#1 - c4-sponsor
2023-02-18T12:11:19Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:47:20Z
dmvt marked the issue as partial-50
#3 - c4-judge
2023-03-01T00:41:34Z
dmvt marked the issue as satisfactory
1044.3421 USDC - $1,044.34
As the strategy implementation is currently unknown and a user can define it on their own, it is possible for strategy to have some storage variables, and for the harvest
function to work upon those storage variables.
The function harvest()
in AdapterBase.sol calls harvest in strategy using a delegate call. If in some case, the delegate call modifies storage variables. Those variables would be modified in adapter and might change some important variables in the adapter contract.
Currently we can’t tell the complete effect this has on adapter contract as the strategy contract can have different implementations, but this design can surely lead to some storage collision.
function harvest() public takeFees { if ( address(strategy) != address(0) && ((lastHarvest + harvestCooldown) < block.timestamp) ) { // solhint-disable address(strategy).delegatecall( abi.encodeWithSignature("harvest()") ); } emit Harvested(); }
If the delegatecall is just used to forward the msg.sender
(as written in the comments) , use a different approach rather than a delegatecall.
#0 - c4-judge
2023-02-16T06:53:34Z
dmvt marked the issue as duplicate of #388
#1 - c4-sponsor
2023-02-18T12:07:02Z
RedVeil marked the issue as sponsor acknowledged
#2 - c4-judge
2023-02-25T16:21:40Z
dmvt changed the severity to 3 (High Risk)
#3 - c4-judge
2023-02-28T17:29:56Z
dmvt marked the issue as satisfactory
#4 - c4-judge
2023-03-01T23:28:21Z
dmvt marked the issue as selected for report
#5 - c4-judge
2023-03-01T23:29:22Z
dmvt marked issue #388 as primary and marked this issue as a duplicate of 388
58.4422 USDC - $58.44
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L294
Malicious users can drain the assets of the vault.
The withdraw
function users convertToShares
to convert the assets to the amount of shares. These shares are burned from the users account and the assets are returned to the user.
The function withdraw
is shown below:
function withdraw( uint256 assets, address receiver, address owner ) public nonReentrant syncFeeCheckpoint returns (uint256 shares) { if (receiver == address(0)) revert InvalidReceiver(); shares = convertToShares(assets); /// .... [skipped the code]
The function convertToShares
is shown below:
function convertToShares(uint256 assets) public view returns (uint256) { uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), Math.Rounding.Down); }
It uses Math.Rounding.Down
, but it should use Math.Rounding.Up
Assume that the vault with the following state:
Assume that Alice wants to withdraw 99 WETH from the vault. Thus, she calls the Vault.withdraw(99 WETH)
 function.
The calculation would go like this:
assets = 99 return value = assets * supply / totalAssets() return value = 99 * 10 / 1000 return value = 0
The value would be rounded round to zero. This will be the amount of shares burned from users account, which is zero.
Hence user can drain the assets from the vault without burning their shares.
Note : A similar issue also exists in
mint
functionality whereMath.Rounding.Down
is used andMath.Rounding.Up
should be used.
Use Math.Rounding.Up
instead of Math.Rounding.Down
.
As per OZ implementation here is the rounding method that should be used:
deposit
: convertToShares
→ Math.Rounding.Down
mint
: converttoAssets
→ Math.Rounding.Up
withdraw
: convertToShares
→ Math.Rounding.Up
redeem
: convertToAssets
→ Math.Rounding.Down
#0 - c4-judge
2023-02-16T07:57:50Z
dmvt marked the issue as duplicate of #71
#1 - c4-sponsor
2023-02-18T12:13:29Z
RedVeil marked the issue as disagree with severity
#2 - c4-sponsor
2023-02-18T12:13:32Z
RedVeil marked the issue as sponsor confirmed
#3 - c4-judge
2023-02-23T01:13:35Z
dmvt changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-02-23T01:23:13Z
dmvt marked the issue as selected for report
🌟 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
changeAdapter
should only be called by owner.Altough there is no immediate threat due to this, but this function should only be called by owner.
function changeAdapter() external takeFees { if (block.timestamp < proposedAdapterTime + quitPeriod) revert NotPassedQuitPeriod(quitPeriod); adapter.redeem( adapter.balanceOf(address(this)), address(this), address(this) ); asset.approve(address(adapter), 0); emit ChangedAdapter(adapter, proposedAdapter); adapter = proposedAdapter; asset.approve(address(adapter), type(uint256).max); adapter.deposit(asset.balanceOf(address(this)), address(this)); }
Add onlyOwner
modfier to the function.
assetsCheckpoint
and change the natspec comments regarding that.The unnecessary variable assetsCheckpoint
can be removed and the natspec comment of changeAdapter
has to be modified to remove the variable.
* @notice Set a new Adapter for this Vault after the quit period has passed. * @dev This migration function will remove all assets from the old Vault and move them into the new vault * @dev Additionally it will zero old allowances and set new ones * @dev Last we update HWM and assetsCheckpoint for fees to make sure they adjust to the new adapter */ function changeAdapter() external takeFees {
src/utils/MultiRewardStaking.sol:351:3:
function _calcRewardsEnd( uint32 rewardsEndTimestamp, uint160 rewardsPerSecond, uint256 amount ) internal returns (uint32) {
src/vault/VaultController.sol:242:3:
function _encodeAdapterData(DeploymentArgs memory adapterData, bytes memory baseAdapterData) internal returns (bytes memory)
src/vault/VaultController.sol:667:3:
function _verifyCreatorOrOwner(address vault) internal returns (VaultMetadata memory metadata) {
#0 - c4-judge
2023-02-28T15:02:50Z
dmvt marked the issue as grade-b