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: 30/72
Findings: 2
Award: $292.95
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
111.7859 USDC - $111.79
Low
maxPerformanceFee
Funding.sol
accepts invalid discount limitsFunding.sol
accepts invalid price boundsFunding.sweep
sweeps all citadel
rather than just excessNon-Critical
Funding.sweep
maxPerformanceFee
`StakedCitadel defines a max performance fee which may be set at any value up to 30%
This is broken up into two components, performance fees paid to the strategist and performance fees paid to governance
https://github.com/code-423n4/2022-04-badger-citadel/blob/dab143a990a9c355578fbb15cd3c884614e33f42/src/StakedCitadel.sol#L618-L636 https://github.com/code-423n4/2022-04-badger-citadel/blob/dab143a990a9c355578fbb15cd3c884614e33f42/src/StakedCitadel.sol#L638-L656
When setting both these components of the performance fee, it's checked that they don't exceed the cap individually but not that they don't exceed the cap together. The real performance fee cap is then 2 * maxPerformanceFee
.
Funding.sol
accepts invalid discount limitsFunding.sol
checks that the provided max and min discount factors don't exceed a maximum discount but does not check that _minDiscount <= _maxDiscount
.
This results in the discountManager
being unable to provide a value which would satisfy these constraints in setDiscount()
.
Funding.sol
accepts invalid price boundsFunding.sol
accepts minimum and maximum price bounds without any validation that the minimum is less than the maximum.
It's then possible to put the contract in a state such that the price may not be updated until the bounds are fixed.
Funding.sweep
sweeps all citadel
rather than just excessThe natspec of Funding.sweep
suggests that calling sweep(citadel)
would be safe in that it would leave enough CTDL to satisfy any outstanding claims. The function implementation however has no special logic for CTDL.
https://github.com/code-423n4/2022-04-badger-citadel/blob/dab143a990a9c355578fbb15cd3c884614e33f42/src/Funding.sol#L310-L312
Funding.sweep
Here we say in a comment that asset
builds up on Funding
until manually swept to the treasury.
https://github.com/code-423n4/2022-04-badger-citadel/blob/dab143a990a9c355578fbb15cd3c884614e33f42/src/Funding.sol#L333
This is not true as on each sale it is sent directly to saleRecipient
.
https://github.com/code-423n4/2022-04-badger-citadel/blob/dab143a990a9c355578fbb15cd3c884614e33f42/src/Funding.sol#L180
🌟 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
181.1552 USDC - $181.16
Funding.sol
FundingParams
struct can be packedFunding
can be packedunchecked_at
in CitadelMinter.getFundingPoolWeights()
deposit
calls to avoid needing to query balancefundingBps
, stakingBps
and lockingBps
can all be packed in CitadelMinter
VestingParams
can be packed into 2/3 slotsReport
struct can be packed into a single slotMedianOracle
storage can be packedKnightingRound
variables boughtAmounts
and hasClaimed
can be merged into a packed struct_mintSharesFor
Funding.sol
In Funding.getStakedCitadelAmountOut
we may a query to get citadel.decimals()
on each call, we can avoid an external call by storing the value of 10**citadel.decimals()
on this contract (similar to assetDecimalsNormalizationValue
).
FundingParams
struct can be packed`FundingParams can be packed to reduce the number of SLOADs used when interacting with it.
discount
, minDiscount
and maxDiscount
all fit in uint16s as they're capped at 10000 to fit in the same slot as discountManager
.
assetCumulativeFunded
and assetCap
could likely be restricted to uint128s safely.
Funding
can be packedThese three variables can be safely packed into a single slot by restricting assetDecimalsNormalizationValue
to a uint88 or less. (uint88 will work for all token decimals up to 26)
unchecked_at
in CitadelMinter.getFundingPoolWeights()
We we know we won't query past the end of fundingPools
there's no need to use the checked version.
https://github.com/code-423n4/2022-04-badger-citadel/blob/dab143a990a9c355578fbb15cd3c884614e33f42/src/CitadelMinter.sol#L153 https://github.com/code-423n4/2022-04-badger-citadel/blob/dab143a990a9c355578fbb15cd3c884614e33f42/src/CitadelMinter.sol#L345
deposit
calls to avoid needing to query balanceHere we need to query the CitadelMinter
's balance of xCitadel before and after depositing in order to calculate how much was minted. This would be cheaper if xCitadel returned the amount of shares minted so we can avoid doing these external calls.
fundingBps
, stakingBps
and lockingBps
can all be packed in CitadelMinter
lastMintTimestamp
is a timestamp so will easily fit in a uint64.
fundingBps
, stakingBps
and lockingBps
all fit in uint16s as they're capped at 10000.
https://github.com/code-423n4/2022-04-badger-citadel/blob/dab143a990a9c355578fbb15cd3c884614e33f42/src/CitadelMinter.sol#L40 https://github.com/code-423n4/2022-04-badger-citadel/blob/dab143a990a9c355578fbb15cd3c884614e33f42/src/CitadelMinter.sol#L48-L50
VestingParams
can be packed into 2/3 slotsunlockBegin
and unlockEnd
are timestamps and so will easily fit in uint64s.
lockedAmounts
and claimedAmounts
could probably be safely made into uint128s as an overflow would require a user to cycle so my Citadel through the contract to be unfeasible.
Report
struct can be packed into a single slottimestamp
is a timestamp (obviously) so will easily fit in a uint64. If payload
can be restricted to uint192 then Report
will fit in a single struct.
MedianOracle
storage can be packedreportExpirationTimeSec
, reportDelaySec
and minimumProviders
will all fit in uint64s and so can be stored in a single slot.
KnightingRound
variables boughtAmounts
and hasClaimed
can be merged into a packed structThe two mappings boughtAmounts
and hasClaimed
can be merged into a single struct of the form:
struct BuyInfo{ uint248 boughtAmounts; bool hasClaimed; }
This will save a SLOAD when calling claim()
.
_mintSharesFor
Here we perform 2 SLOADs for the total supply whereas we could just query once and cache it in storage.