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: 12/72
Findings: 5
Award: $2,114.65
🌟 Selected for report: 0
🚀 Solo Findings: 0
896.081 USDC - $896.08
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L293 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L890 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L881 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L764
In StakedCitadel.sol/_mintSharesFor()
, it doesn’t check whether the pool
is equal to zero. If pool == 0
and totalSupply() != 0
, _mintSharesFor()
will revert. And In _depositFor()
, it uses balance()
as pool
. In consequence, when the CTDL balance of StakedCitadel
is zero and the total supply of xCTDL is not zero, _depositFor()
always reverts. No one can ever deposit any CTDL.
earn()
, and it transfer 95% of CTDL balance of StakedCitadel to strategyhttps://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L717
function earn() external { require(!pausedDeposit, "pausedDeposit"); // dev: deposits are paused, we don't earn as well _onlyAuthorizedActors(); uint256 _bal = available(); token.safeTransfer(strategy, _bal); IStrategy(strategy).earn(); }
uint256 public constant MAX_BPS = 10_000 … toEarnBps = 9_500; … function available() public view returns (uint256) { return (token.balanceOf(address(this)) * toEarnBps) / MAX_BPS; }
withdrawAll()
, It should take 10 CTDLs in this case. The CTDL balance of StakedCitadel is not enough after earn()
, So it will transfer CTDLs from strategy to ensure r == token.balanceOf(address(this))
. Since withdrawalFee is never higher than 2%, there is no fee in this transaction. In the end, the CTDL balance of StakedCitadel becomes zero.function _withdraw(uint256 _shares) internal nonReentrant { require(_shares != 0, "0 Shares"); uint256 r = (balance() * _shares) / totalSupply(); _burn(msg.sender, _shares); // Check balance uint256 b = token.balanceOf(address(this)); if (b < r) { uint256 _toWithdraw = r - b; IStrategy(strategy).withdraw(_toWithdraw); uint256 _after = token.balanceOf(address(this)); uint256 _diff = _after - b; if (_diff < _toWithdraw) { r = b + _diff; } } uint256 _fee = _calculateFee(r, withdrawalFee); uint256 _amount = r - _fee; // Send funds to vesting contract and setup vesting IVesting(vesting).setupVesting(msg.sender, _amount, block.timestamp); token.safeTransfer(vesting, _amount); // After you burned the shares, and you have sent the funds, adding here is equivalent to depositing // Process withdrawal fee if(_fee > 0) { _mintSharesFor(treasury, _fee, balance() - _fee); } }
balance()
in _depositFor()
returns zero, And totalSupply()
in _mintSharesFor()
returns non-zero, since Alice still has xCTDL. In consequence, _mintSharesFor()
always reverts at shares = (_amount * totalSupply()) / _pool;
, no one can deposit any CTDL intohttps://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L764
function _depositFor(address _recipient, uint256 _amount) internal nonReentrant { require(_recipient != address(0), "Address 0"); require(_amount != 0, "Amount 0"); require(!pausedDeposit, "pausedDeposit"); // dev: deposits are paused uint256 _pool = balance(); uint256 _before = token.balanceOf(address(this)); token.safeTransferFrom(msg.sender, address(this), _amount); uint256 _after = token.balanceOf(address(this)); _mintSharesFor(_recipient, _after - _before, _pool); }
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L881
function _mintSharesFor( address recipient, uint256 _amount, uint256 _pool ) internal { uint256 shares; if (totalSupply() == 0) { shares = _amount; } else { shares = (_amount * totalSupply()) / _pool; } _mint(recipient, shares); }
vim
Add a check for pool
in _mintSharesFor()
#0 - GalloDaSballo
2022-04-22T22:37:22Z
The code is written in solidity 0.8.12 a division by zero will cause a revert.
For that reason I must disagree with the finding, I would like to see a code POC but I believe the POC would just revert per the reason above
#1 - jack-the-pug
2022-05-29T07:07:06Z
I think this issue can be considered as a dup of #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/main/src/StakedCitadel.sol#L830 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L125
When doing withdraw()
or withdrawAll()
in StakedCitadel.sol
, it will call the internal function _withdraw()
. The function then transfers tokens to the vesting contract, which should be StakedCitadelVester.sol
. However, IVesting(vesting).setupVesting()
is not implemented in StakedCitadelVester.sol
. This mistake makes the withdraw functions totally unusable.
In StakedCitadel.sol/_withdraw()
, it uses IVesting(vesting).setupVesting(msg.sender, _amount, block.timestamp);
to setup vesting.
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L830
function _withdraw(uint256 _shares) internal nonReentrant { … // Send funds to vesting contract and setup vesting IVesting(vesting).setupVesting(msg.sender, _amount, block.timestamp); token.safeTransfer(vesting, _amount); … }
However in StakedCitadelVester.sol
. The setup function is actually vest()
. setupVesting()
is not implemented in StakedCitadelVester.sol
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L125
/** * @dev setup vesting for recipient. * @notice note that a given address can only have one active vest at a time. * @notice adding a new vest before claiming completely from the previous will re-lock the previous amount according to the new vesting timeline. * @param recipient The account for which vesting will be setup. * @param _amount amount that will be vested * @param _unlockBegin The time at which unlocking of tokens will begin. */ function vest( address recipient, uint256 _amount, uint256 _unlockBegin ) external { require(msg.sender == vault, "StakedCitadelVester: only xCTDL vault"); require(_amount > 0, "StakedCitadelVester: cannot vest 0"); vesting[recipient].lockedAmounts = vesting[recipient].lockedAmounts + _amount; vesting[recipient].unlockBegin = _unlockBegin; vesting[recipient].unlockEnd = _unlockBegin + vestingDuration; emit Vest( recipient, vesting[recipient].lockedAmounts, _unlockBegin, vesting[recipient].unlockEnd ); }
vim
Either
replace IVesting(vesting).setupVesting
with IVesting(vesting).vest
in StakedCitadel.sol
or
replace vest
with setupVesting
in StakedCitadelVester.sol
#0 - GalloDaSballo
2022-04-22T22:38:43Z
@dapp-whisperer wdyt?
#1 - jack-the-pug
2022-05-29T07:03:39Z
Dup #9
643.8625 USDC - $643.86
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L132 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L85 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L108
When users vest in StakedCitadelVester, the tokens will be locked in vest duration. Users can call claim()
to get back their tokens. The claimable amount is calculated in claimableBalance()
. Before the duration ends, the claimable amount is
((locked * (block.timestamp - vesting[recipient].unlockBegin)) / (vesting[recipient].unlockEnd - vesting[recipient].unlockBegin)) - claimed
It seems to be a fair claimable amount. However if users keep vesting more and more, the claimableBalance()
will become unfair and sometimes revert.
vesting[recipient].lockedAmounts
becomes 10000https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L132
function vest( address recipient, uint256 _amount, uint256 _unlockBegin ) external { … vesting[recipient].lockedAmounts = vesting[recipient].lockedAmounts + _amount; vesting[recipient].unlockBegin = _unlockBegin; vesting[recipient].unlockEnd = _unlockBegin + vestingDuration; … }
vesting[msg.sender].claimedAmounts
becomes 10000https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L85
function claim(address recipient, uint256 amount) external { uint256 claimable = claimableBalance(msg.sender); if (amount > claimable) { amount = claimable; } if (amount != 0) { vesting[msg.sender].claimedAmounts = vesting[msg.sender].claimedAmounts + amount; vestingToken.safeTransfer(recipient, amount); emit Claimed(msg.sender, recipient, amount); } }
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L108
function claimableBalance(address recipient) public view returns (uint256) { uint256 locked = vesting[recipient].lockedAmounts; uint256 claimed = vesting[recipient].claimedAmounts; if (block.timestamp >= vesting[recipient].unlockEnd) { return locked - claimed; } return ((locked * (block.timestamp - vesting[recipient].unlockBegin)) / (vesting[recipient].unlockEnd - vesting[recipient].unlockBegin)) - claimed; }
vesting[recipient].lockedAmounts
becomes 10100function vest( address recipient, uint256 _amount, uint256 _unlockBegin ) external { … vesting[recipient].lockedAmounts = vesting[recipient].lockedAmounts + _amount; … }
claimableBalance()
Since
locked = vesting[recipient].lockedAmounts = 10100 claimed = vesting[recipient].claimedAmounts = 10000 ((locked * (block.timestamp - vesting[recipient].unlockBegin)) / (vesting[recipient].unlockEnd - vesting[recipient].unlockBegin)) = 10100 * (1/2) = 5050 ((locked * (block.timestamp - vesting[recipient].unlockBegin)) / (vesting[recipient].unlockEnd - vesting[recipient].unlockBegin)) - claimed = 5050 - 10000
This line will revert
return ((locked * (block.timestamp - vesting[recipient].unlockBegin)) / (vesting[recipient].unlockEnd - vesting[recipient].unlockBegin)) - claimed;
vim
vest()
could be modified like the following code.
function vest( address recipient, uint256 _amount, uint256 _unlockBegin ) external { require(msg.sender == vault, "StakedCitadelVester: only xCTDL vault"); require(_amount > 0, "StakedCitadelVester: cannot vest 0"); vesting[recipient].lockedAmounts = vesting[recipient].lockedAmounts + _amount - vesting[recipient].claimedAmounts ; vesting[recipient].claimedAmounts = 0; vesting[recipient].unlockBegin = _unlockBegin; vesting[recipient].unlockEnd = _unlockBegin + vestingDuration; emit Vest( recipient, vesting[recipient].lockedAmounts, _unlockBegin, vesting[recipient].unlockEnd ); }
#0 - GalloDaSballo
2022-04-22T22:31:45Z
@dapp-whisperer Wdyt?
#1 - shuklaayush
2022-04-26T21:40:34Z
#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
91.5484 USDC - $91.55
We list 3 low-critical findings here:
transferFromDisabled
is unused_daoId
In conclusion, it's better to check unused variables, check the validation of DAO ID, and check the boundaries of original values.
transferFromDisabled
is unusedtransferFromDisabled
is unused and not initialized, it may cause some problems when we need it in the future.
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/GlobalAccessControl.sol#L51
vim, ganache-cli
Delete unused variables or set transferFromDisabled
in initialize()
.
_daoId
No check for _daoId
of buy()
function in KnightingRound.sol, resulting in a user being able to buy non-exist or invalid _daoId
.
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/KnightingRound.sol#L164
vim, ganache-cli
Check valid range of _daoId
in buy()
function.
In set limit function (e.g. setDiscountLimits
), it doesn’t consider the original value (funding.discount
), resulting in the original value being out of bound.
For example, the original value funding.discount
is 20, minDiscount
is 10, and maxDiscount
is 100. If Governance calls setDiscountLimits
, changes minDiscount
to 30, and changes maxDiscount
to 50, the original funding.discount
will be out of bound.
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L356 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L397 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L521 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L533 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L548
vim, ganache-cli
Check the original value in set limit functions.
🌟 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
52.0246 USDC - $52.02
The for loop has no overflow risk of i
. Use an unchecked block to save gas.
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/lib/GlobalAccessControlManaged.sol#L48 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/SupplySchedule.sol#L111 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/SupplySchedule.sol#L208 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L152 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L344
Use unchecked
blocks to avoid overflow checks, or use ++i
rather than i++
if you don't use unchecked blocks.
for (uint256 i = 0; i < length; ) { ... unchecked { ++i; } }