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: 17/72
Findings: 2
Award: $1,102.25
🌟 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#L293-L295 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L881-L893
When earn()
is called by authorized actors (keeper or governance), 95% of the balance of CTDL token in the StakedCitadel
contract will be transferred to strategy.
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L722
Thus, the balance()
will be roughly only 5% of the totalSupply(). At this juncture, if an attacker deposit(amt)
for _amt
amount of CDTL tokens, he will get _amt * totalSupply()/balance()
, which is about 20 times more than he should.
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L776 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L890
This affects also _withdraw
calculation where r
can be very small, or rounds to 0 when _shares
is less than 20. Thus the users won't be able to withdraw right after earn()
is called.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Step 1: prepare a big sum of CTDL tokens (as much as one get can)
Step 2: monitor the contract activity and wait for earn()
to be called by authorised actors.
Step 3: deposit CTDL tokens from Step1 to deposit(uint256)
function and get up to 20x staked xCDTL tokens.
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L309-L311
Step 4: monitor the contract activity and wait for withdrawToVault()
is triggered
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L676-L679
Step 5: withdraw all staked tokens by calling withdrawAll()
to get back CTDL tokens that correspond to the inflated shares.
balance() function needs to keep track of both this contract balance and how much is in strategy contract, which may be compounding as well.
#0 - GalloDaSballo
2022-04-23T01:18:09Z
Most likely a duplicate but I do believe that the balance
function has been improperly ported over from the 1.5 code where it would also include the balanceOf the strategy as well as the balanceOfPool of the strategy
#1 - jack-the-pug
2022-05-30T06:45:25Z
Dup #74
🌟 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
206.1723 USDC - $206.17
The version number is hardcoded, which cannot be updated in the future. Also the function is not virtual, thus cannot be override
in future contract.
Test is really useful in development but not in production.
CITADEL_MINTER_ROLE
is assignedConsider assign minter role in the initialize
function to ensure that mint
can be called successfully.
ERC20 tokens can be sweeped by the following function. https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/KnightingRound.sol#L402-L415
In the unlikely event that some contract self-destruct to send ethers into this contract, one may as well implement a function to sweep ethers.
ABIEncoderV2
is no longer considered experimentalBecause the ABI coder v2 is not considered experimental anymore, it can be selected via pragma abicoder v2
see https://docs.soliditylang.org/en/v0.8.13/layout-of-source-files.html#abiencoderv2
There are some state variables that are not initialised in the initialize
function. For example,
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/CitadelMinter.sol#L44-L45
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/CitadelMinter.sol#L48-L50
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/CitadelMinter.sol#L40
guardian
role should be put in SettAccessControl instead of StakedCitadelGuardian role seems to be an externally owned account and is used the same way as other authorised actors such as governance. Thus, they could be all together in the SettAccessControl contract.
This will also put the modifiers related to Guardian and setGuardian to the access control contract. https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L261-L266 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L560-L566
SettAccessControl
implements a role-based access control system, which has already had a well-thoughtout implementation from OpenZepplin.
Also consider using OpenZepplin implementation for GlobalAccessControl
and GloablAccessControlManaged
.
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/GlobalAccessControl.sol
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/lib/GlobalAccessControlManaged.sol
Since SettAccessControl
is not meant to be deployed on its own, but only to inherit as a base contract, one could mark it abstract.
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/lib/SettAccessControl.sol#L9
Similarly, GloablAccessControlManaged can be abstract too.
modifier
keywordMany functions with only one require
check prior to function execution are not labelled using the modifier
keyword. Using the modifier
keyword helps with readability.
Instances like this are https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/lib/SettAccessControl.sol#L15-L31
lastAdditionalTokenAmount
is defined but not used.
_pool
is the same as _before
in https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L772-L773
The 8 fee variables below are meant to be between 0-10000. Thus they can use uint32 instead of uint256 to cover the entire range and get packed together into one single storage slot.
There are still TODOs in the production code. Instances are
There are many instances of comments that are in paragraphs but written with single line comment ///
instead of /* */
. For example
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L388-L395 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadel.sol#L154-L166