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
Rank: 11/72
Findings: 6
Award: $2,317.41
π Selected for report: 0
π Solo Findings: 0
896.081 USDC - $896.08
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
User can lose their fund because of incorrect function
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
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
π Selected for report: cccz
Also found by: 0xBug, 0xDjango, CertoraInc, TrungOre, VAD37, berndartmueller, georgypetrov, horsefacts, m9800, pedroais, rayn, reassor, scaraven, wuwe1
431.1404 USDC - $431.14
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/test/BaseFixture.sol#L92 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/test/BaseFixture.sol#L157 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadelVester.sol#L132
Users can't withdraw
As we can see in BaseFixture.sol
, vesting contract of StakedCitadel
is a StakedCitadelVester
contract
In _withdraw
function of StakedCitadel.sol
, there is a call to function setupVesting
of vesting contract, but there is no function which has a name like that in StakedCitadelVester.sol
Manual review
use vest
function instead of setupVesting
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadelVester.sol#L132
#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
184.248 USDC - $184.25
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
Users can get the wrong amount of citadel when depositing to funding
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
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
643.8625 USDC - $643.86
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
User can't withdraw their token in their second vesting
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
= 2000block.timestamp
= 12vesting[recipient].unlockBegin
= 11vesting[recipient].unlockBegin
= 21claimed
= 1000
==> claimable
= 200 - 1000 = -800 < 0javascript
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
π Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0xDjango, 0xkatana, AmitN, CertoraInc, Dravee, Funen, Hawkeye, Jujic, MaratCerby, Picodes, Ruhum, SolidityScan, TerrierLover, TomFrenchBlockchain, TrungOre, VAD37, Yiko, berndartmueller, cmichel, csanuragjain, danb, defsec, delfin454000, dipp, ellahi, fatherOfBlocks, georgypetrov, gs8nrv, gzeon, horsefacts, hubble, hyh, ilan, jah, joestakey, kebabsec, kenta, kyliek, m9800, minhquanym, oyc_109, p_crypt0, peritoflores, rayn, reassor, remora, rfa, robee, scaraven, securerodd, shenwilly, sorrynotsorry, tchkvsky, teryanarmen, z3s
98.4337 USDC - $98.43
tags: c4
, 2022-04-badger-citadel
Afftected code
Proof of concept
Affected code
Affected code
π Selected for report: Dravee
Also found by: 0v3rf10w, 0x1f8b, 0xAsm0d3us, 0xBug, 0xDjango, 0xNazgul, 0xkatana, CertoraInc, Cityscape, Funen, Hawkeye, IllIllI, MaratCerby, SolidityScan, TerrierLover, TomFrenchBlockchain, Tomio, TrungOre, bae11, berndartmueller, csanuragjain, defsec, delfin454000, ellahi, fatherOfBlocks, gs8nrv, gzeon, horsefacts, ilan, jah, joestakey, joshie, kebabsec, kenta, nahnah, oyc_109, rayn, rfa, robee, saian, securerodd, simon135, slywaters, sorrynotsorry, tchkvsky, teryanarmen, z3s
63.6465 USDC - $63.65
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
delete
instead of manual set value to 0affected code
solution: use delete fundingPoolWeights[_pool]
instead
calldata
instead of memory
affected code
reentrancy
affected code
fundingBps
affected code
Proof of concept
fundingBps + stakingBps + lockingBps == MAX_BPS
so we can deduce fundingBps
from stakingBps
and lockingBps