prePO contest - cmichel's results

Gain exposure to pre-IPO companies & pre-token projects.

General Information

Platform: Code4rena

Start Date: 17/03/2022

Pot Size: $30,000 USDC

Total HM: 8

Participants: 43

Period: 3 days

Judge: gzeon

Total Solo HM: 5

Id: 100

League: ETH

prePO

Findings Distribution

Researcher Performance

Rank: 2/43

Findings: 4

Award: $4,453.58

🌟 Selected for report: 2

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: GreyArt

Also found by: 0xDjango, CertoraInc, TomFrenchBlockchain, WatchPug, cmichel, rayn

Labels

bug
duplicate
3 (High Risk)

Awards

511.5587 USDC - $511.56

External Links

Lines of code

https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L89 https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/SingleStrategyController.sol#L80

Vulnerability details

Impact

The Collateral.deposit function mints initial shares equal to the deposited amount. The deposit / withdraw functions also use the _strategyController.totalValue(), which includes the strategy contract balance, to compute the shares.

It's possible to increase the share price to very high amounts, such that a depositors mints zero shares for their deposit. They lose all funds and they can then be stolen by the attacker when they redeem their fair share of the strategy TVL.

POC

Assume the attacker is the first minter and collateral.totalSupply() == 0 and _baseToken == USDC. Some victim V wants to deposit 1000.0 USDC = 1e9 USDC. The attacker frontruns the transaction and does the following:

  • deposit(amount = 2). Then _fee = 1 and final _amountToDeposit = 2 - 1 = 1. This amount is deposited into the strategy controller and the attacker receives _shares = _amountToDeposit = 1 share. Assume the 1 token is deposited into the strategy and _strategy.totalValue() = 1
  • The attacker now increases the strategy controller's balance by directly transfering 1e9 USDC to the controller such that _baseToken.balanceOf(_strategyController) = 1e9 and _strategyController.totalValue() equals _baseToken.balanceOf(address(this)) + _strategy.totalValue() = 1e9 + 1
  • The victim deposit is now mined and (even assuming no fees) their _amountToDeposit = 1e9. The shares computation is now _shares = (_amountToDeposit * totalSupply()) / (_valueBefore) = 1e9 * 1 / (1e9 + 1) = 0. They contributed 1e9 USDC to the strategy but receives zero shares.
  • The attacker can now call withdraw(1) to burn their single share and receive the entire strategy TVL as they redeem 100% of the total supply: _owed = (_strategyController.totalValue() * _amount) / totalSupply() = _strategyController.totalValue() * 1) / 1 = _strategyController.totalValue() = 2e9 + 1
  • They made 1e9 in profit, the totalSupply() is back to zero and they can repeat this attack for the next depositor.

The way UniswapV2 prevents this is by requiring a minimum deposit amount and sending 1000 initial shares to the zero address to make this attack more expensive. The same mitigation can be done here. Alternatively, scale the initial amount by a large value like 1e18: if (totalSupply() == 0) _shares = _amountToDeposit * 1e18.

#0 - ramenforbreakfast

2022-03-23T00:39:15Z

duplicate of #27

#1 - gzeoneth

2022-04-03T13:30:56Z

Agree with sponsor

Findings Information

🌟 Selected for report: cmichel

Also found by: IllIllI, leastwood

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

1819.2911 USDC - $1,819.29

External Links

Lines of code

https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L97

Vulnerability details

Impact

After initiating a withdrawal with initiateWithdrawal, it's still possible to transfer the collateral tokens. This can be used to create a second account, transfer the accounts to them and initiate withdrawals at a different time frame such that one of the accounts is always in a valid withdrawal window, no matter what time it is. If the token owner now wants to withdraw they just transfer the funds to the account that is currently in a valid withdrawal window.

Also, note that each account can withdraw the specified amount. Creating several accounts and circling & initiating withdrawals with all of them allows withdrawing larger amounts even at the same block as they are purchased in the future.

I consider this high severity because it breaks core functionality of the Collateral token.

POC

For example, assume the _delayedWithdrawalExpiry = 20 blocks. Account A owns 1000 collateral tokens, they create a second account B.

  • At block=0, A calls initiateWithdrawal(1000). They send their balance to account B.
  • At block=10, B calls initiateWithdrawal(1000). They send their balance to account A.
  • They repeat these steps, alternating the withdrawal initiation every 10 blocks.
  • One of the accounts is always in a valid withdrawal window (initiationBlock < block && block <= initiationBlock + 20). They can withdraw their funds at any time.

If there's a withdrawal request for the token owner (_accountToWithdrawalRequest[owner].blockNumber > 0), disable their transfers for the time.

// pseudo-code not tested
beforeTransfer(from, to, amount) {
  super();
  uint256 withdrawalStart =  _accountToWithdrawalRequest[from].blockNumber;
  if(withdrawalStart > 0 && withdrawalStart + _delayedWithdrawalExpiry < block.number) {
    revert(); // still in withdrawal window
  }
}

#0 - ramenforbreakfast

2022-03-23T00:44:52Z

This is a valid claim and is the higher quality submission among its duplicates.

#1 - gzeoneth

2022-04-03T13:31:05Z

Agree with sponsor

Findings Information

🌟 Selected for report: cmichel

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

2021.4346 USDC - $2,021.43

External Links

Lines of code

https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L328

Vulnerability details

Impact

The getSharesForAmount function returns 0 if totalAssets == 0.

However, if totalSupply == 0, the actual shares that are minted in a deposit are _amount even if totalAssets == 0.

Contracts / frontends that use this function to estimate their deposit when totalSupply == 0 will return a wrong value.

function getSharesForAmount(uint256 _amount)
    external
    view
    override
    returns (uint256)
{
+   // to match the code in `deposit`
+   if (totalSupply() == 0) return _amount;

    uint256 _totalAssets = totalAssets();
    return
        (_totalAssets > 0)
            ? ((_amount * totalSupply()) / _totalAssets)
            : 0; // @audit this should be _amount according to `deposit`
}

#0 - ramenforbreakfast

2022-03-23T00:45:28Z

Valid claim, I believe this is a bug in our code, still need to verify.

#1 - gzeoneth

2022-04-03T13:28:26Z

Agree with sponsor

Awards

101.302 USDC - $101.30

Labels

bug
QA (Quality Assurance)
sponsor disputed

External Links

QA

  • allowAccounts emits an AccountAllowed even if account is already allowed
  • Gas: DepositHook/WithdrawHook.hook never uses _initialAmount variable. Can be removed to save gas on calldata.
  • Gas: _floor/_ceilingValuation are never used and never updated. can be removed from storage to save gas, should just read from MarketCreated event as they are never updated.

#0 - ramenforbreakfast

2022-03-23T00:57:35Z

First issue is a duplicate of #14 Second issue is a duplicate of #5 Third issue is not an issue, we originally only emitted it via an event, but it was decided to keep it obtainable from the contract state.

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