prePO contest - hansfriese's results

Decentralized Exchange for Pre-IPO Stocks & Pre-IDO Tokens.

General Information

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

prePO

Findings Distribution

Researcher Performance

Rank: 9/69

Findings: 3

Award: $953.20

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Trust

Also found by: 0Kage, Parth, aviggiano, ayeslick, bin2chen, cccz, chaduke, fs0c, hansfriese, imare, mert_eren, rvierdiiev

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-310

Awards

210.7761 USDC - $210.78

External Links

Lines of code

https://github.com/prepo-io/prepo-monorepo/blob/3541bc704ab185a969f300e96e2f744a572a3640/apps/smart-contracts/core/contracts/WithdrawHook.sol#L59-L72

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

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)

Findings Information

🌟 Selected for report: hansfriese

Also found by: Trust, cccz, unforgiven

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
M-05

Awards

689.5834 USDC - $689.58

External Links

Lines of code

https://github.com/prepo-io/prepo-monorepo/blob/3541bc704ab185a969f300e96e2f744a572a3640/apps/smart-contracts/core/contracts/PrePOMarket.sol#L119

Vulnerability details

Impact

If finalLongPayout is changed twice by admin fault, the market would be insolvent as it should pay more collateral than it has.

Proof of Concept

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.

  1. Let's assume there is one user Bob in the market for simplicity.
  2. Bob transferred 100 amounts of collateral and got 100 long/short tokens. The market has 100 collateral.
  3. The market admin set finalLongPayout = 60 * 1e16 and Bob redeemed 100 longToken and received 60 collateral. The market has 40 collateral now.
  4. After that, the admin realized finalLongPayout is too high and changed finalLongPayout = 40 * 1e16 again.
  5. 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.

Tools Used

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.

Findings Information

🌟 Selected for report: obront

Also found by: 8olidity, HE1M, Madalad, Trust, cccz, csanuragjain, deliriusz, hansfriese, hihen, joestakey, rvierdiiev, wait, zaskoh

Labels

bug
2 (Med Risk)
satisfactory
duplicate-254

Awards

52.8446 USDC - $52.84

External Links

Lines of code

https://github.com/prepo-io/prepo-monorepo/blob/3541bc704ab185a969f300e96e2f744a572a3640/apps/smart-contracts/core/contracts/Collateral.sol#L80

Vulnerability details

Impact

This is an issue about admin privilege.

The admin can withdraw all base tokens using Collateral.managerWithdraw().

Proof of Concept

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.

  1. Don't update withdrawHook by SET_MANAGER_WITHDRAW_HOOK_ROLE so let withdrawHook = address(0)
  2. When managerWithdraw() is called by MANAGER_WITHDRAW_ROLE, there is no validation at all so can withdraw all base tokens.

Tools Used

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter