Platform: Code4rena
Start Date: 09/12/2022
Pot Size: $36,500 USDC
Total HM: 9
Participants: 69
Period: 3 days
Judge: Picodes
Total Solo HM: 2
Id: 190
League: ETH
Rank: 5/69
Findings: 2
Award: $1,803.53
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: zaskoh
Also found by: Tricko, deliriusz, rvierdiiev, unforgiven
1273.0771 USDC - $1,273.08
according to the comments in code: "Every time userPeriodLength
seconds passes, the amount withdrawn for all users will be reset to 0" (https://github.com/prepo-io/prepo-monorepo/blob/3541bc704ab185a969f300e96e2f744a572a3640/apps/smart-contracts/core/contracts/interfaces/IWithdrawHook.sol#L64-L76). but in current implementation only one of the users userToAmountWithdrawnThisPeriod[]
value gets reset and this will cause users to not be able to withdraw their tokens even if they follow the limitation. users which has high withdrawal amount only can withdraw when they reset the value of userPeriodLength
and in each period only one user can do that because in each userPeriodLength
only one user withdraw amounts gets reset. so over time in each period only one user could withdraw. for example if userPeriodLength
was 1 day, in each day 1 user can withdraw and if protocol had 100K user some users would never withdraw in their life time.
This is hook()
code in WithdrawHook
:
function hook( address _sender, uint256 _amountBeforeFee, uint256 _amountAfterFee ) external override onlyCollateral { require(withdrawalsAllowed, "withdrawals not allowed"); if (lastGlobalPeriodReset + globalPeriodLength < block.timestamp) { lastGlobalPeriodReset = block.timestamp; globalAmountWithdrawnThisPeriod = _amountBeforeFee; } else { require(globalAmountWithdrawnThisPeriod + _amountBeforeFee <= globalWithdrawLimitPerPeriod, "global withdraw limit exceeded"); globalAmountWithdrawnThisPeriod += _amountBeforeFee; } if (lastUserPeriodReset + userPeriodLength < block.timestamp) { lastUserPeriodReset = block.timestamp; userToAmountWithdrawnThisPeriod[_sender] = _amountBeforeFee; } else { require(userToAmountWithdrawnThisPeriod[_sender] + _amountBeforeFee <= userWithdrawLimitPerPeriod, "user withdraw limit exceeded"); userToAmountWithdrawnThisPeriod[_sender] += _amountBeforeFee; } depositRecord.recordWithdrawal(_amountBeforeFee); uint256 _fee = _amountBeforeFee - _amountAfterFee; if (_fee > 0) { collateral.getBaseToken().transferFrom(address(collateral), _treasury, _fee); _tokenSender.send(_sender, _fee); } }
As you can see when the lastUserPeriodReset + userPeriodLength < block.timestamp
happens(which means last period finished) only the callers userToAmountWithdrawnThisPeriod[]
value changes and other don't get changed. and in the else case (when period is not finished) if user's userToAmountWithdrawnThisPeriod[user] + amount
succeed userWithdrawLimitPerPeriod
code would revert. so during periods users with high withdraw amount can't withdraw and in each period only one user withdraw amount gets new value so overtime all users get high withdraw amounts and in each period 1 user cant get his withdraws and users tokens would stuck in the contract literally forever. POC:
userToAmountWithdrawnThisPeriod[user1]
would be 90. max allowed withdraw in each period for users is 100 token.userToAmountWithdrawnThisPeriod[user1]
is still 90 and 90+20 is bigger than 100 and user1 can't be able to withdraw.VIM
reset all users withdrawal amounts in each period start time.
#0 - c4-judge
2022-12-13T21:06:46Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2022-12-19T21:33:35Z
ramenforbreakfast marked the issue as sponsor confirmed
#2 - c4-judge
2023-01-01T16:58:18Z
Picodes marked the issue as satisfactory
#3 - C4-Staff
2023-01-17T19:11:55Z
captainmangoC4 marked issue 116 as selected for report. Updating associated duplicate and primary issues.
🌟 Selected for report: hansfriese
Also found by: Trust, cccz, unforgiven
530.4488 USDC - $530.45
https://github.com/prepo-io/prepo-monorepo/blob/3541bc704ab185a969f300e96e2f744a572a3640/apps/smart-contracts/core/contracts/PrePOMarket.sol#L119-L124 https://github.com/prepo-io/prepo-monorepo/blob/3541bc704ab185a969f300e96e2f744a572a3640/apps/smart-contracts/core/contracts/PrePOMarket.sol#L76-L107
Function setFinalLongPayout()
Sets the payout a Long token(finalLongPayout
) can be redeemed for after the market has ended. function redeem()
calculates the redeem amount based on the finalLongPayout
and if the value of finalLongPayout
changes then users would receive other redeem amounts(profits). owner most be able to set the value of the finalLongPayout
one time because if owner set multiple values for finalLongPayout
users would get different profits for their positions and some users would lose their profits. as the final value of the asset is one number in the real world so owner should only be able to set one value for finalLongPayout
one time.
This is setFinalLongPayout()
code:
function setFinalLongPayout(uint256 _finalLongPayout) external override onlyOwner { require(_finalLongPayout >= floorLongPayout, "Payout cannot be below floor"); require(_finalLongPayout <= ceilingLongPayout, "Payout cannot exceed ceiling"); finalLongPayout = _finalLongPayout; emit FinalLongPayoutSet(_finalLongPayout); }
As you can see there is no restriction on how many times owner can set this value and the profit and loss calculation in the redeem() function for long or short positions has been calculated based on finalLongPayout
value. so if this scenario happens:
finalLongPayout
.finalLongPayout
and set low value.so users get different profit and loss if owner makes this mistake and contract shouldn't let owner to perform this action because when market is finished and ended(when owner calls this function it means market has ended) owner shouldn't be able to set new finalLongPayout
.
VIM
prevent owner from changing the value of finalLongPayout
after market is closed.
#0 - c4-judge
2022-12-17T21:50:02Z
Picodes marked the issue as duplicate of #231
#1 - c4-judge
2023-01-07T15:30:43Z
Picodes marked the issue as satisfactory