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: 9/69
Findings: 3
Award: $953.20
🌟 Selected for report: 1
🚀 Solo Findings: 0
210.7761 USDC - $210.78
In WithdrawHook.hook()
, withdraw limits can be bypassed.
As a result, users might withdraw more amount of the base token at a time than they should.
WithdrawHook.hook()
checks the withdraw limits like below.
if (lastGlobalPeriodReset + globalPeriodLength < block.timestamp) { //@audit bypass limit lastGlobalPeriodReset = block.timestamp; globalAmountWithdrawnThisPeriod = _amountBeforeFee; } else { require(globalAmountWithdrawnThisPeriod + _amountBeforeFee <= globalWithdrawLimitPerPeriod, "global withdraw limit exceeded"); globalAmountWithdrawnThisPeriod += _amountBeforeFee; } if (lastUserPeriodReset + userPeriodLength < block.timestamp) { //@audit bypass limit lastUserPeriodReset = block.timestamp; userToAmountWithdrawnThisPeriod[_sender] = _amountBeforeFee; } else { require(userToAmountWithdrawnThisPeriod[_sender] + _amountBeforeFee <= userWithdrawLimitPerPeriod, "user withdraw limit exceeded"); userToAmountWithdrawnThisPeriod[_sender] += _amountBeforeFee; }
But it doesn't check the limit when it resets the period so users can withdraw without any limits at that time.
Manual Review
We should modify like below.
if (lastGlobalPeriodReset + globalPeriodLength < block.timestamp) { lastGlobalPeriodReset = block.timestamp; globalAmountWithdrawnThisPeriod = _amountBeforeFee; } else { globalAmountWithdrawnThisPeriod += _amountBeforeFee; } require(globalAmountWithdrawnThisPeriod <= globalWithdrawLimitPerPeriod, "global withdraw limit exceeded"); if (lastUserPeriodReset + userPeriodLength < block.timestamp) { lastUserPeriodReset = block.timestamp; userToAmountWithdrawnThisPeriod[_sender] = _amountBeforeFee; } else { userToAmountWithdrawnThisPeriod[_sender] += _amountBeforeFee; } require(userToAmountWithdrawnThisPeriod[_sender] <= userWithdrawLimitPerPeriod, "user withdraw limit exceeded");
#0 - hansfriese
2022-12-14T17:32:27Z
duplicate of #310
#1 - c4-judge
2022-12-17T21:38:07Z
Picodes marked the issue as duplicate of #310
#2 - c4-judge
2023-01-01T17:20:28Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-01-09T20:35:42Z
Picodes changed the severity to 3 (High Risk)
🌟 Selected for report: hansfriese
Also found by: Trust, cccz, unforgiven
689.5834 USDC - $689.58
If finalLongPayout
is changed twice by admin fault, the market would be insolvent as it should pay more collateral than it has.
If finalLongPayout
is less than MAX_PAYOUT
, it means the market is ended and longToken Price = finalLongPayout, shortToken Price = MAX_PAYOUT - finalLongPayout
.
So when users redeem their long/short tokens, the total amount of collateral tokens will be the same as the amount that users transferred during mint().
Btw in setFinalLongPayout()
, there is no validation that this function can't be called twice and the below scenario would be possible.
Bob
in the market for simplicity.Bob
transferred 100 amounts of collateral
and got 100 long/short tokens. The market has 100 collateral
.finalLongPayout = 60 * 1e16
and Bob
redeemed 100 longToken
and received 60 collateral
. The market has 40 collateral
now.finalLongPayout
is too high and changed finalLongPayout = 40 * 1e16
again.Bob
tries to redeem 100 shortToken
and receive 60 collateral
but the market can't offer as it has 40 collateral
only.When there are several users in the market, some users can't redeem their long/short tokens as the market doesn't have enough collaterals
.
Manual Review
We should modify setFinalLongPayout()
like below so it can't be finalized twice.
function setFinalLongPayout(uint256 _finalLongPayout) external override onlyOwner { require(finalLongPayout > MAX_PAYOUT, "Finalized already"); //++++++++++++++++++++++++ require(_finalLongPayout >= floorLongPayout, "Payout cannot be below floor"); require(_finalLongPayout <= ceilingLongPayout, "Payout cannot exceed ceiling"); finalLongPayout = _finalLongPayout; emit FinalLongPayoutSet(_finalLongPayout); }
#0 - c4-judge
2022-12-17T10:24:11Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2022-12-19T23:25:08Z
ramenforbreakfast marked the issue as sponsor confirmed
#2 - c4-judge
2023-01-07T15:30:26Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-01-07T15:31:39Z
Picodes marked the issue as selected for report
#4 - trust1995
2023-01-10T08:29:04Z
Would ask judge to consider the merits of #307 (mine) as the selected-for-report, primarily because it shows an exploitation flow where admin can make maximum profit. This report only reviews the properties of the bug.
🌟 Selected for report: obront
Also found by: 8olidity, HE1M, Madalad, Trust, cccz, csanuragjain, deliriusz, hansfriese, hihen, joestakey, rvierdiiev, wait, zaskoh
52.8446 USDC - $52.84
This is an issue about admin privilege.
The admin can withdraw all base tokens using Collateral.managerWithdraw()
.
As we can see from here, the Collateral
contract should keep a certain amount of base token for reserve.
There are several roles such as MANAGER_WITHDRAW_ROLE
and SET_MANAGER_WITHDRAW_HOOK_ROLE
and these roles can be set by DEFAULT_ADMIN_ROLE
in SafeAccessControlEnumerableUpgradeable.sol
.
So all base tokens might be withdrawn like below.
withdrawHook
by SET_MANAGER_WITHDRAW_HOOK_ROLE
so let withdrawHook
= address(0)managerWithdraw()
is called by MANAGER_WITHDRAW_ROLE
, there is no validation at all so can withdraw all base tokens.Manual Review
Recommend adding stick validation of reserve requirement or time lock for managerWithdraw()
.
#0 - hansfriese
2022-12-14T17:31:56Z
duplicate of #254
#1 - c4-judge
2022-12-17T09:47:30Z
Picodes marked the issue as duplicate of #254
#2 - c4-judge
2023-01-01T17:37:34Z
Picodes marked the issue as satisfactory