prePO contest - rvierdiiev'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: 4/69

Findings: 4

Award: $1,932.93

QA:
grade-a

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: zaskoh

Also found by: Tricko, deliriusz, rvierdiiev, unforgiven

Labels

bug
3 (High Risk)
satisfactory
duplicate-116

Awards

1273.0771 USDC - $1,273.08

External Links

Lines of code

https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/WithdrawHook.sol#L66-L68

Vulnerability details

Impact

Because WithdrawHook.hook doesn't track user's withdraw time separately, when someone withdraws before user, he can't withdraw all amount

Proof of Concept

Function WithdrawHook.hook should not allow users to withdraw more then is allowed for some period. https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/WithdrawHook.sol#L66-L72

    if (lastUserPeriodReset + userPeriodLength < block.timestamp) {
      lastUserPeriodReset = block.timestamp;
      userToAmountWithdrawnThisPeriod[_sender] = _amountBeforeFee;
    } else {
      require(userToAmountWithdrawnThisPeriod[_sender] + _amountBeforeFee <= userWithdrawLimitPerPeriod, "user withdraw limit exceeded");
      userToAmountWithdrawnThisPeriod[_sender] += _amountBeforeFee;
    }

This is logic controls how many tokens user can withdraw during some period. What you should note here is that there is no lastUserPeriodReset per account, it is used globally. And once any account withdraws when (lastUserPeriodReset + userPeriodLength < block.timestamp it will be reset to block.timestamp.

I want to show situation to explain the problem. Let's assume that allowed amount to withdraw per user is 100 tokens per hour. 1.At t1=0 lastUserPeriodReset is 0. userA calls withdraw with amount 100. userToAmountWithdrawnThisPeriod[_sender] becomes 100. And userA has no ability to withdraw more tokens during this hour. He will be able to do that at t2=1 hour and 1 minute. 2.At t2=1 hour and 1 minute userB calls withdraw 100 tokens. Because this condition if (lastUserPeriodReset + userPeriodLength < block.timestamp) is true, lastUserPeriodReset becomes block.timestamp and == 1 hour and 1 minute. What happened next to this userB is not interested for us. 3.At t3 = 1 hour and 5 minutes userA calls withdraw 100 tokens. Because this condition if (lastUserPeriodReset + userPeriodLength < block.timestamp) is false, this check will be done require(userToAmountWithdrawnThisPeriod[_sender] + _amountBeforeFee <= userWithdrawLimitPerPeriod, "user withdraw limit exceeded");. And it will fail, because userToAmountWithdrawnThisPeriod[_sender] is currently 100 and it was not cleared when userB updated lastUserPeriodReset. As result userA can't withdraw money currently. To withdraw them he should be first one who will call withdraw after lastUserPeriodReset + userPeriodLength time amount, as only in that case his userToAmountWithdrawnThisPeriod amount will not be checked.

Tools Used

VsCode

You need to add one more bool field that will indicate that lastUserPeriodReset was recently reset.

    if (lastUserPeriodReset + userPeriodLength < block.timestamp) {
      lastUserPeriodReset = block.timestamp;
      userToAmountWithdrawnThisPeriod[_sender] = _amountBeforeFee;
      userPeriodWasReset = true;
    } else {
      require(userPeriodWasReset  || userToAmountWithdrawnThisPeriod[_sender] + _amountBeforeFee <= userWithdrawLimitPerPeriod, "user withdraw limit exceeded");
      if (userPeriodWasReset ) {
          userToAmountWithdrawnThisPeriod[_sender] = 0;
          userPeriodWasReset = false;
      }
      userToAmountWithdrawnThisPeriod[_sender] += _amountBeforeFee;
    }

#0 - Picodes

2022-12-13T21:06:13Z

Your mitigation does not fully solves the issue. It works only for up to 2 users.

The PoC is correct but it doesn't look like the warden identified the root issue.

#1 - c4-judge

2022-12-13T21:06:58Z

Picodes marked the issue as duplicate of #325

#2 - c4-judge

2022-12-13T21:07:39Z

Picodes marked the issue as partial-50

#3 - rvierdiyev

2022-12-21T21:36:03Z

I respectfully disagree with your decision. First of all i understood the problem, because i have created correct POC as you said. Also i don't agree that my solution is incorrect. And the last thing is that i have identified the problem, even if i didn't find correct solution, this is not so important as i am not developer of protocol, my job is to find issues(what i did here), also i can recommend smth, but that doesn't guarantee that i will suggest the best solution as i am not developer.

and about recommended steps: i wanted to recommend to track time for each user separatly(as you can see in report title), but i tried to solve issue in another way(to not waste gas).

#4 - c4-judge

2023-01-01T17:07:44Z

Picodes marked the issue as satisfactory

#5 - Picodes

2023-01-01T17:07:45Z

@rvierdiyev indeed the issue is correctly identified so I'll remove the partial credit label, my first pass was too harsh. This being said, I'd appreciate if you could keep this kind of comments for the post judging Q&A s decisions are not supposed to be finalized until then :)

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/feat/2022-12-prepo/apps/smart-contracts/core/contracts/WithdrawHook.sol#L59-L62 https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/WithdrawHook.sol#L140-L146

Vulnerability details

Impact

WithdrawHook doesn't check that userWithdrawLimitPerPeriod is less than globalWithdrawLimitPerPeriod. It allows to user withdraw more then globalWithdrawLimitPerPeriod per period.

Proof of Concept

Function WithdrawHook.hook should not allow to withdraw more than globalWithdrawLimitPerPeriod amount of tokens per globalPeriodLength of time. But it's possible when userWithdrawLimitPerPeriod > globalWithdrawLimitPerPeriod. Because there is no input checkings it's possible to provide userWithdrawLimitPerPeriod > globalWithdrawLimitPerPeriod.

This is how this global withdraw limit is handled. https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/WithdrawHook.sol#L59-L65

    if (lastGlobalPeriodReset + globalPeriodLength < block.timestamp) {
      lastGlobalPeriodReset = block.timestamp;
      globalAmountWithdrawnThisPeriod = _amountBeforeFee;
    } else {
      require(globalAmountWithdrawnThisPeriod + _amountBeforeFee <= globalWithdrawLimitPerPeriod, "global withdraw limit exceeded");
      globalAmountWithdrawnThisPeriod += _amountBeforeFee;
    }

As you can see the check is done only when globalPeriodLength has not passed since last lastGlobalPeriodReset update. When this condition if (lastGlobalPeriodReset + globalPeriodLength < block.timestamp) is true, then there is no amount validation. And if userWithdrawLimitPerPeriod is bigger than globalWithdrawLimitPerPeriod then user can withdraw more than globalWithdrawLimitPerPeriod. If userWithdrawLimitPerPeriod is less than globalWithdrawLimitPerPeriod then it will be not possible to do that, as user limit checking will fail(if they will be fixed as i described in another reports).

Tools Used

VsCode

Add check that userWithdrawLimitPerPeriod < globalWithdrawLimitPerPeriod in setters. Or add checking require(_amountBeforeFee <= globalWithdrawLimitPerPeriod, "global withdraw limit exceeded"); to if clause.

#0 - hansfriese

2022-12-14T18:01:26Z

duplicate of #310 but explains slightly different point

#1 - c4-judge

2022-12-19T09:22:47Z

Picodes marked the issue as duplicate of #310

#2 - c4-judge

2023-01-01T17:21:02Z

Picodes marked the issue as satisfactory

#3 - c4-judge

2023-01-09T20:35:22Z

Picodes changed the severity to 3 (High Risk)

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/feat/2022-12-prepo/apps/smart-contracts/core/contracts/ManagerWithdrawHook.sol#L17 https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/ManagerWithdrawHook.sol#L29-L33 https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/Collateral.sol#L82

Vulnerability details

Impact

Collateral can become insolvent because of managerWithdraw function.

Proof of Concept

Collateral.managerWithdraw allows to withdraw base tokens to manager. https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/Collateral.sol#L80-L83

  function managerWithdraw(uint256 _amount) external override onlyRole(MANAGER_WITHDRAW_ROLE) nonReentrant {
    if (address(managerWithdrawHook) != address(0)) managerWithdrawHook.hook(msg.sender, _amount, _amount);
    baseToken.transfer(manager, _amount);
  }

In case if managerWithdrawHook is 0 then there is no any checks and _amount is sent to manager. This can make Collateral insolvent. Scenario. 1.User deposit 1 million tokens to Collateral. 2.managerWithdraw is called and all tokens were sent to manager. 3.Collateral is insolvent, users can't withdraw.

In case if managerWithdrawHook is not 0 then managerWithdrawHook.hook is called. https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/ManagerWithdrawHook.sol#L17

  function hook(address, uint256, uint256 _amountAfterFee) external view override { require(collateral.getReserve() - _amountAfterFee >= getMinReserve(), "reserve would fall below minimum"); }

The check that is done here is that balance of base token of Collateral will stay above some provided percentage(minReservePercentage) multiplied by all amount recorded in depositRecord after withdraw. https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/ManagerWithdrawHook.sol#L41

  function getMinReserve() public view override returns (uint256) { return (depositRecord.getGlobalNetDepositAmount() * minReservePercentage) / PERCENT_DENOMINATOR; }

Even if this percentage is small, it's enough to make Collateral insolvent as well and Collateral will not hold enough funds to cover users withdraws.

Tools Used

VsCode

I believe that manager should not be able to withdraw users funds. He should not be able to withdraw any amount that will make Collateral balance less than depositRecord.getGlobalNetDepositAmount().

#0 - hansfriese

2022-12-14T17:58:27Z

duplicate of #254

#1 - c4-judge

2022-12-17T09:57:01Z

Picodes marked the issue as duplicate of #254

#2 - c4-judge

2023-01-01T17:41:51Z

Picodes marked the issue as satisfactory

Awards

396.2326 USDC - $396.23

Labels

bug
downgraded by judge
grade-a
QA (Quality Assurance)
Q-20

External Links

Lines of code

https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/DepositTradeHelper.sol#L24

Vulnerability details

Impact

DepositTradeHelper.depositAndTrade will not work normally with ERC20 that need to clear allowance as it sets allowance to uint256.max and do not set it to 0 after. As result next time when user calls depositAndTrade function it will fail as allowance should be set to 0 before.

Proof of Concept

DepositTradeHelper.depositAndTrade function allows user to deposit base tokens into Collateral token and then swap it using Uniswap. https://github.com/prepo-io/prepo-monorepo/blob/feat/2022-12-prepo/apps/smart-contracts/core/contracts/DepositTradeHelper.sol#L22-L34

  function depositAndTrade(uint256 baseTokenAmount, Permit calldata baseTokenPermit, Permit calldata collateralPermit, OffChainTradeParams calldata tradeParams) external override {
    if (baseTokenPermit.deadline != 0) {
      IERC20Permit(address(_baseToken)).permit(msg.sender, address(this), type(uint256).max, baseTokenPermit.deadline, baseTokenPermit.v, baseTokenPermit.r, baseTokenPermit.s);
    }
    _baseToken.transferFrom(msg.sender, address(this), baseTokenAmount);
    if (collateralPermit.deadline != 0) {
      _collateral.permit(msg.sender, address(this), type(uint256).max, collateralPermit.deadline, collateralPermit.v, collateralPermit.r, collateralPermit.s);
    }
    uint256 _collateralAmountMinted = _collateral.deposit(msg.sender, baseTokenAmount);
    _collateral.transferFrom(msg.sender, address(this), _collateralAmountMinted);
    ISwapRouter.ExactInputSingleParams memory exactInputSingleParams = ISwapRouter.ExactInputSingleParams(address(_collateral), tradeParams.tokenOut, POOL_FEE_TIER, msg.sender, tradeParams.deadline, _collateralAmountMinted, tradeParams.amountOutMinimum, tradeParams.sqrtPriceLimitX96);
    _swapRouter.exactInputSingle(exactInputSingleParams);
  }

It uses permit function to provide allowance to himself and it sets allowance to type(uint256).max. After the transfer it doesn't set allowance to 0. And next time when user will call this function it will revert if _baseToken doesn't support providing allowance when current allowance is not set to 0(like USDT).

Tools Used

VsCode

Do not set allowance to type(uint256).max, set it to baseTokenAmount value.

#0 - c4-judge

2022-12-17T21:46:16Z

Picodes changed the severity to QA (Quality Assurance)

#1 - c4-judge

2022-12-19T14:04:19Z

Picodes marked the issue as grade-c

#2 - c4-judge

2022-12-19T14:04:33Z

Picodes marked the issue as grade-b

#3 - c4-judge

2022-12-19T14:04:46Z

Picodes marked the issue as grade-a

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