Badger Citadel contest - cmichel's results

Bringing BTC to DeFi

General Information

Platform: Code4rena

Start Date: 14/04/2022

Pot Size: $75,000 USDC

Total HM: 8

Participants: 72

Period: 7 days

Judge: Jack the Pug

Total Solo HM: 2

Id: 110

League: ETH

BadgerDAO

Findings Distribution

Researcher Performance

Rank: 3/72

Findings: 4

Award: $5,807.18

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: hyh

Also found by: 0xDjango, VAD37, berndartmueller, cmichel, danb

Labels

bug
duplicate
3 (High Risk)
disagree with severity
sponsor acknowledged

Awards

2782.1219 USDC - $2,782.12

External Links

Lines of code

https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L888

Vulnerability details

Impact

The StakedCitadel's deposit function (see _depositFor) mints initial shares equal to the deposited amount. The _mintSharesFor / _withdraw functions also use the balance() function, which includes the contract balance token.balanceOf(address(this)), to compute the shares.

It's possible to increase the share price to very high amounts, such that a future depositor 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 there is no whitelist and anyone can deposit. Assume the attacker is the first minter and balance() == 0 and token == USDC. Some victim V wants to deposit 1000.0 USDC = 1e9 USDC. The attacker frontruns the transaction and does the following:

  • deposit(amount = 1). This amount is transferred and the attacker receives shares = _amount = 1 share. Then balance() == 1 and totalSuplpy() == 1.
  • The attacker now increases contract's token balance by directly transfering 1e9 USDC to the controller such that balance() = token.balanceOf(this) = 1e9 + 1.
  • The victim deposit is now mined with their _amount = 1e9. The shares computation is now shares = (_amount * totalSupply()) / _pool = 1e9 * 1 / (1e9 + 1) = 0. They contributed 1e9 USDC to the strategy but receive zero shares.
  • The attacker can now call withdraw(1) to burn their single share and receive the entire balance() as they redeem 100% of the share total supply: r = (balance() * _shares) / totalSupply() = balance() * 1 / 1 = 2e9 + 1
  • They made 1e9 USDC 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 = _amount * 1e18.

#0 - GalloDaSballo

2022-04-23T01:56:52Z

Agree with premise and conclusion that we'd have to mint some tokens the first time, that said I have to disagree with severity, if anyone where to do that we could just redeploy as you basically need to be first (or pay up a ton) to perform this griefing attack which ultimately just rebases the shares to their denomination, it technically doesn't even break the accounting of the vault

#1 - jack-the-pug

2022-05-30T08:52:11Z

Dup #217

Findings Information

🌟 Selected for report: reassor

Also found by: cccz, cmichel

Labels

bug
duplicate
2 (Med Risk)
disagree with severity
sponsor confirmed

Awards

2289.8123 USDC - $2,289.81

External Links

Lines of code

https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/KnightingRound.sol#L336

Vulnerability details

Impact

The tokenOutPrice can be changed at any time, even while the sale is still running. Users who want to buy a certain amount of tokenOut using buy can (even accidentally) be frontrun by a setTokenOutPrice function call, which reduces the tokenOut amounts they receive. It could be that they didn't want to buy the tokens at the new price.

Only allow changes to the token price if the sale has not started or add a min out amount parameter to the buy function and revert if the amountOut is less than this parameter.

#0 - GalloDaSballo

2022-04-23T01:59:59Z

I believe the finding to have validity in that you're locking in a certain price when calling buy and that price can change.

I'd like to emphasize that your purchase is not being changed, you're locking in that specific prize at that specific time.

So I have to agree that the admin can change the prize, and we probably should add a slippage check

#1 - jack-the-pug

2022-06-05T05:54:37Z

Dup #73

Findings Information

🌟 Selected for report: gzeon

Also found by: 0xDjango, TrungOre, cccz, cmichel, minhquanym, rayn

Labels

bug
duplicate
2 (Med Risk)
sponsor confirmed

Awards

643.8625 USDC - $643.86

External Links

Lines of code

https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadelVester.sol#L143

Vulnerability details

Impact

When withdrawing in StakedCitadel, the vesting is reset in StakedCitadel.setupVest. (There was a bad commit, the vest function is supposed to be called setupVesting.)

Users that withdrew shares a month ago and did not claim will have their entire vesting reset again. Even if they claimed, the remaining vesting balance will now vest according to the new terms which is a worse vesting schedule than before. Users don't receive their tokens according to the vesting schedule, breaking core functionality of the vesting contract.

Furthermore, the note is incorrect:

note that a given address can only have one active vest at a time.

You can't get correct vesting by having a single vesting schedule for an account and overwriting the single existing vesting schedule all the time. Each vest should be non-fungible as it has its own (recipient, amount, vestStart, vestEnd) parameters.

#0 - GalloDaSballo

2022-04-23T01:54:10Z

@dapp-whisperer wdyt?

#1 - GalloDaSballo

2022-05-12T15:51:18Z

Will confirm, would like @dapp-whisperer to also confirm

#2 - jack-the-pug

2022-06-05T06:02:24Z

Dup #158

Lines of code

https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L822

Vulnerability details

Impact

The withdraw function burns _shares amounts and the caller is supposed to receive their fair share of r = (balance() * _shares) / totalSupply() tokens. However, this r value can be reduced if not enough tokens are in the contract and the strategy's withdrawal return fewer tokens.

if (_diff < _toWithdraw) {
    // @audit burns more shares than the fair share for b + _diff
    r = b + _diff;
}

When this code is executed, the user receives fewer tokens than the _shares amount they burned. The received amount is not fair anymore.

If the strategy can currently not withdraw the entire requires _toWithdraw amount, should the _shares to burn be readjusted to match r = b + _diff?

// pseudo-code: burning of _shares needs to happen later
if (_diff < _toWithdraw) {
    r = b + _diff;
+   // @audit important to round up here so user's can't abuse
+   _shares = r * totalSupply() / balance() + 1;
}

#0 - GalloDaSballo

2022-04-23T01:58:02Z

Disagree that shares need to be re-computed as if the math previously required x to be withdrawn and then we just got y (with y << x) then we incurred in slippage, and you would never want to have the vault pay the slippage as that's how you actually get rekt

#1 - jack-the-pug

2022-06-05T06:00:40Z

Downgrading to QA as per the sponsor's comment on #210

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