Badger Citadel contest - TrungOre'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: 11/72

Findings: 6

Award: $2,317.41

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: Ruhum

Also found by: TrungOre, VAD37, cccz, danb, gs8nrv, kyliek, minhquanym, rayn, shenwilly, wuwe1

Labels

bug
duplicate
3 (High Risk)
sponsor confirmed

Awards

896.081 USDC - $896.08

External Links

Lines of code

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

Vulnerability details

Impact

User can lose their fund because of incorrect function

Proof of Concept

In the comment, balance() function should return the total balance of citadel within sett and strategy system. But in the current code, the function just returns the balance within sett It can make users lose their token when strategy takes a part of fund for yield-generating activities (strategy can do it by using earn function). As you can see on the image above

  • b = balance() (they are both equal to token.balanceOf(address(this)))
  • shares <= totalSupply() (if not the transaction will be reverted because of _burn function) ==> r <= b

It will make the code never reach into if block and the fund won't ever be withdrawn out of strategy

Tools Used

Manual review

Modify the function balance() in StakedCitadel.sol

function balance() public view returns (uint256) { return token.balanceOf(address(this)) + token.balanceOf(strategy); }

#0 - GalloDaSballo

2022-04-23T02:01:28Z

Balance is indeed wrong

#1 - jack-the-pug

2022-05-29T06:42:02Z

Dup #74

Findings Information

Labels

bug
duplicate
3 (High Risk)
resolved
sponsor confirmed

Awards

431.1404 USDC - $431.14

External Links

#0 - dapp-whisperer

2022-04-15T17:47:31Z

valid, resolved to setupVesting() in previous PR

#1 - jack-the-pug

2022-05-29T06:46:11Z

Dup #9

Findings Information

🌟 Selected for report: Ruhum

Also found by: 0xBug, 0xDjango, IllIllI, MaratCerby, TrungOre, danb, hyh, m9800, minhquanym, pedroais, remora, shenwilly

Labels

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

Awards

184.248 USDC - $184.25

External Links

Lines of code

https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/Funding.sol#L202 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/Funding.sol#L209 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/Funding.sol#L215 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/Funding.sol#L177

Vulnerability details

Impact

Users can get the wrong amount of citadel when depositing to funding

Proof of Concept

if funding.discount == 0, it won't go into if block https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/Funding.sol#L209 It make citadelAmount_ still equal to 0 in line 215 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/Funding.sol#L215

Tools Used

manual review

modify getAmountOut function

function getAmountOut(uint256 _assetAmountIn) public view returns (uint256 citadelAmount_) { citadelAmount_ = _assetAmountIn * citadelPriceInAsset; if (funding.discount > 0) { citadelAmount_ = (citadelAmount_ * MAX_BPS) / (MAX_BPS - funding.discount); } citadelAmount_ = citadelAmount_ / assetDecimalsNormalizationValue; }

#0 - shuklaayush

2022-05-11T20:40:31Z

I would characterize this as medium risk since discount is set by an admin

#1 - jack-the-pug

2022-05-29T06:48:20Z

Dup #218

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#L108 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadelVester.sol#L86

Vulnerability details

Impact

User can't withdraw their token in their second vesting

Proof of Concept

it("user cannot claim their balance in second vest", async() => { // for easy to understand, i will set vesting duration = 10s, instead of 21 days await vester.setVestingDuration(10); // setup vesting for alice (1000 CTDL) at time 0 await vester.setBlockTime(0); await vester.vest( alice.address, 1000, 0 ); // alice claim her reward at timestamp = 10 await vester.setBlockTime(10); await vester.connect(alice).claim( alice.address, 1000 ); // alice get 1000 token reward expect(await citadel.balanceOf(alice.address)).to.equal(1000); // setup another vesting for alice (1000 CTDL) at time 11 await vester.setBlockTime(11); await vester.vest( alice.address, 1000, 11 ); // setup vesting for bob (1000 CTDL) at time 11 await vester.vest( bob.address, 1000, 11 ); // bob and alice both claim their token at time 12, bob can claim 100 token when alice can claim nothing because of reverted transaction await vester.setBlockTime(12); expect(await vester.claimableBalance(bob.address)).to.equal(100); await expect(vester.claimableBalance(alice.address)).to.be.reverted; });

In example above, alice has 2 vesting with 1000 CTDL each, in the second vesting, she tries to claim her tokens after 1 second from unlockBegin. As normal (like bob) she might get 100 tokens, but unfortunately, she claims nothing, because the formula

((locked * (block.timestamp - vesting[recipient].unlockBegin)) / (vesting[recipient].unlockEnd - vesting[recipient].unlockBegin)) - claimed;

give a negative result, which make tx revert.

  • locked = 2000
  • block.timestamp = 12
  • vesting[recipient].unlockBegin = 11
  • vesting[recipient].unlockBegin = 21
  • claimed = 1000 ==> claimable = 200 - 1000 = -800 < 0

Tools Used

javascript

Add a variable lastClaimTimestamp to store the last time that user has claimed the token. In another word, lastClaimTimestamp will be updated to block.timestamp for each time user claim.

And the function will be like this

function claimableBalance(address recipient) public view returns (uint256) { uint256 locked = vesting[recipient].lockedAmounts - vesting[recipient].claimedAmounts; // uint256 claimed = vesting[recipient].claimedAmounts; if (block.timestamp >= vesting[recipient].unlockEnd) { return locked; } return locked * (min(block.timestamp, vesting[recipient].unlockEnd) - max(vesting[recipient].lastClaimTimestamp, vesting[recipient].unlockBegin)) / (vesting[recipient].unlockEnd - max(vesting[recipient].lastClaimTimestamp, vesting[recipient].unlockBegin)); }

#0 - shuklaayush

2022-04-26T22:04:55Z

#158

[2022-04-badger-citadel] Gas optimization

tags: c4, 2022-04-badger-citadel

totalTokenIn always < tokenInLimit

affected code

solution: place them into unchecked block

assetCumulativeFunded always < assetCap

afftected code

solution: place them into unchecked block

use delete instead of manual set value to 0

affected code

solution: use delete fundingPoolWeights[_pool] instead

use calldata instead of memory

affected code

redundant reentrancy

affected code

redundant fundingBps

affected code

Proof of concept

  • Because fundingBps + stakingBps + lockingBps == MAX_BPS so we can deduce fundingBps from stakingBps and lockingBps
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