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: 36/72
Findings: 2
Award: $175.44
🌟 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
getMintable function uses uint, but this can be rewritten into uint256. https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/interfaces/citadel/ISupplySchedule.sol#L6
function getMintable(uint lastMintTimestamp) external view returns (uint256);
uint will be converted to uint256, so just using uint256 is better.
The caller of getMintable expects uint256 to be used, so it is reasonable to use uint256 instead of uint. https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L175-L177
uint256 cachedLastMintTimestamp = lastMintTimestamp; uint256 mintable = supplySchedule.getMintable(cachedLastMintTimestamp);
Simply using uint256 instead of uint.
function getMintable(uint256 lastMintTimestamp) external view returns (uint256);
function setupVesting( address recipient, uint256 _amount, uint256 _unlockBegin ) external;
Throughout the codebase, _
is added at the prefixes of the arguments. However, recipient
does not have _
in its prefix. To be consistent with other codebase, recipient
should be _recipient
.
function setupVesting( address _recipient, uint256 _amount, uint256 _unlockBegin ) external;
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L257
require( address(_pool) != address(0), "CitadelMinter: address(0) check" );
It uses address() function to convert _pool to address, but _pool is already an address. It does not need to use address() at all.
Simply remove address function.
require( _pool != address(0), "CitadelMinter: address(0) check" );
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelToken.sol#L40
function mint(address dest, uint256 amount)
In CitadelToken.sol, the arguments of the functions have _
at their prefixes.
Add _
at their prefixes.
function mint(address _dest, uint256 _amount)
ABIEncoderV2
is not experimental anymorehttps://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L3-L4
pragma solidity 0.8.12; pragma experimental ABIEncoderV2;
See the solidity document. https://docs.soliditylang.org/en/v0.8.12/layout-of-source-files.html?highlight=ABIEncoderV2#abiencoderv2
Because the ABI coder v2 is not considered experimental anymore, it can be selected via pragma abicoder v2 (please see above) since Solidity 0.7.4.
Use following instead of pragma experimental ABIEncoderV2;
pragma solidity 0.8.12; pragma abicoder v2;
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L147
modifier onlyWhenPriceNotFlagged() { require( citadelPriceFlag == false, "Funding: citadel price from oracle flagged and pending review" ); _; }
Instead of checking by == false
, simply !
can be used. This can decrease the deployment gas cost.
modifier onlyWhenPriceNotFlagged() { require( !citadelPriceFlag, "Funding: citadel price from oracle flagged and pending review" ); _; }
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L137
minCitadelPriceInAsset = 0;
The default value of minCitadelPriceInAsset is 0. So setting minCitadelPriceInAsset seems not needed.
Just avoid setting 0 at the above code. This should reduce the gas cost as well.
minCitadelPriceInAsset;
citadelAmount_ >= _minCitadelOut
can be moved up at deposit function in Funding.solhttps://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L175-L178
funding.assetCumulativeFunded = funding.assetCumulativeFunded + _assetAmountIn; // Take in asset from user citadelAmount_ = getAmountOut(_assetAmountIn); require(citadelAmount_ >= _minCitadelOut, "minCitadelOut");
funding.assetCumulativeFunded
is updated before the require(citadelAmount_ >= _minCitadelOut, "..")
check. The storage variable update should happen after the require check.
Simply re-order the codebase.
// Take in asset from user citadelAmount_ = getAmountOut(_assetAmountIn); require(citadelAmount_ >= _minCitadelOut, "minCitadelOut"); funding.assetCumulativeFunded = funding.assetCumulativeFunded + _assetAmountIn;
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L224
uint citadelAmount = getAmountOut(_assetAmountIn);
The return value of getAmountOut function is uint256. Although it does not have any issues, but uint256 should be used for the consistency.
uint256 citadelAmount = getAmountOut(_assetAmountIn);
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L328-L329
emit Sweep(_token, amount); IERC20(_token).safeTransfer(saleRecipient, amount);
It emits event first, and the transfer of the token happens. If there is no special reason to have this order, to be consistent with other codebase in this product, it should transfer token first, and then emit event,
IERC20(_token).safeTransfer(saleRecipient, amount); emit Sweep(_token, amount);
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/KnightingRound.sol#L413-L414
emit Sweep(_token, amount); ERC20Upgradeable(_token).safeTransfer(saleRecipient, amount);
It emits event first, and the transfer of the token happens. If there is no special reason to have this order, to be consistent with other codebase in this product, it should transfer token first, and then emit event,
ERC20Upgradeable(_token).safeTransfer(saleRecipient, amount); emit Sweep(_token, amount);
Can only be called by owner
is used in the comments below. However, the allowed roles vary on functions. (In the codebase, CONTRACT_GOVERNANCE_ROLE, TECH_OPERATIONS_ROLE, and TREASURY_OPERATIONS_ROLE are used.)
citadel/2022-04-badger-citadel/src/KnightingRound.sol
269: * @notice Finalize the sale after sale duration. Can only be called by owner
286: * @notice Update the sale start time. Can only be called by owner
305: * @notice Update sale duration. Can only be called by owner
324: * @notice Modify the tokenOut price in. Can only be called by owner
342: * @notice Update the tokenIn
receipient address. Can only be called by owner
364: * @notice Update the guestlist address. Can only be called by owner
377: * @notice Modify the max tokenIn that this contract can take. Can only be called by owner
396: * @notice Transfers out any tokens accidentally sent to the contract. Can only be called by owner
If the team thinks that owner
is too broad, it should specify the specific roles for each function.
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L85-L86
string internal constant _defaultNamePrefix = "Staked "; string internal constant _symbolSymbolPrefix = "x";
They are constant variables, but camel case is used. Other codebase in this project uses capital letters for constant variables, so this seems not consistent.
string internal constant _DEFAULT_NAME_PREFIX = "Staked "; string internal constant _SYMBOL_SYMBOL_PREFIX = "x";
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L86
string internal constant _symbolSymbolPrefix = "x";
symbol is used twice in the naming. This seems not necessary.
Just rename as follows:
string internal constant _symbolPrefix = "x";
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/SupplySchedule.sol#L25
uint256 public constant epochLength = 21 days;
epochLength
is constant variable, but camel case is used. Other codebase in this project uses capital letters for constant variable, so this seems not consistent.
uint256 public constant EPOCH_LENGTH = 21 days;
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L698-L700
function sweepExtraToken(address _token) external { _onlyGovernanceOrStrategist(); require(address(token) != _token, "No want");
Currently _token can be zero address. It should check zero address as well.
function sweepExtraToken(address _token) external { _onlyGovernanceOrStrategist(); require(address(token) != _token, "No want"); require(_token != address(0), "...");
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L59-L65
function initialize( address _gac, address _vestingToken, address _vault ) external initializer { require(_vestingToken != address(0), "Address zero invalid"); require(_vault != address(0), "Address zero invalid");
There are zero address check for _vestingToken
and _vault
variables, but _gac
does not have one. If this is not intentional, it should also check zero address for _gac
.
function initialize( address _gac, address _vestingToken, address _vault ) external initializer { require(_gac != address(0), "Address zero invalid"); require(_vestingToken != address(0), "Address zero invalid"); require(_vault != address(0), "Address zero invalid");
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L133
function vest( address recipient, uint256 _amount, uint256 _unlockBegin ) external {
only recipient
does not have _
in its prefix. To be consistent with other codebase, it should also have _
.
function vest( address _recipient, uint256 _amount, uint256 _unlockBegin ) external {
_globalAccessControl
variable can be zero addressfunction __GlobalAccessControlManaged_init(address _globalAccessControl) public onlyInitializing { __Pausable_init_unchained(); gac = IGac(_globalAccessControl); }
There is not zero address check for _globalAccessControl variable.
It should do zero address check.
function __GlobalAccessControlManaged_init(address _globalAccessControl) public onlyInitializing { require(_globalAccessControl != address(0), "..."); __Pausable_init_unchained(); gac = IGac(_globalAccessControl); }
https://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);
There is no zero address check for recipient
argument.
Add zero address check for recipient
not to cause the unexpected behavior for users.
function claim(address recipient, uint256 amount) external { require(recipient != address(0), "..."); uint256 claimable = claimableBalance(msg.sender);
amount
early at claim function in StakedCitadelVester.solhttps://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L90
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); } }
If amount is 0, users cannot do meaningful operations but simply pay higher gas fee. It can do zero check for amount at the first part of the function.
function claim(address recipient, uint256 amount) external { require(amount != 0, "..."); uint256 claimable = claimableBalance(msg.sender); if (amount > claimable) { amount = claimable; } vesting[msg.sender].claimedAmounts = vesting[msg.sender].claimedAmounts + amount; vestingToken.safeTransfer(recipient, amount); emit Claimed(msg.sender, recipient, amount); }
setDiscountLimits function sets _minDiscount and _maxDiscount.
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L362-L363
funding.minDiscount = _minDiscount; funding.maxDiscount = _maxDiscount;
Since there is no check to confirm that _minDiscount < _maxDiscount
is true, if _maxDiscount variable has value which is lower than _minDiscount, it potentially blocks the user operation.
Simply setting the additional condition can be helpful
require(_minDiscount < _maxDiscount , "...");
nonReentrant
modifier for the external functions which call other contractsHere are codebase which contain external functions which call the other contracts.
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L309 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L319 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L329 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L341 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L350 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L363 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L375 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L382 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadelVester.sol#L85
Since nonReentrant
modifier is used at other function, it is worth considering adding nonReentrant
modifier for the above mentioned functions.
Add nonReentrant
modifier for the above mentioned 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
63.6465 USDC - $63.65
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L152
for (uint256 i = 0; i < numPools; i++) {
i
will be initialized with 0 so no need to set 0.
Simply avoid setting 0 can save small gas cost.
for (uint256 i; i < numPools; i++) {
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L153-L157
for (uint256 i = 0; i < numPools; i++) { address pool = fundingPools.at(i); uint256 weight = fundingPoolWeights[pool]; pools[i] = pool; weights[i] = weight; }
weight varialbe is not used multiple times, so it can omit declaring them.
Simply omitting declaring weight variable can save gas cost.
for (uint256 i = 0; i < numPools; i++) { address pool = fundingPools.at(i); pools[i] = pool; weights[i] = fundingPoolWeights[pool]; }
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L180-L182
uint256 lockingAmount = 0; uint256 stakingAmount = 0; uint256 fundingAmount = 0;
No need to set 0 at uint256 variable since as a default uint256 variable becomes 0.
Avoiding declaring these variables can save some gas cost.
uint256 lockingAmount; uint256 stakingAmount; uint256 fundingAmount;
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L270
if (_weight == 0 && poolExists) { _removeFundingPool(_pool); emit FundingPoolWeightSet(_pool, _weight, totalFundingPoolWeight); } else if (_weight > 0) {
It can use != 0 instead of > 0 to reduce the gas cost.
if (_weight == 0 && poolExists) { _removeFundingPool(_pool); emit FundingPoolWeightSet(_pool, _weight, totalFundingPoolWeight); } else if (_weight != 0) {
The deployment gas cost of CitadelMinter contract can be reduced.
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L276-L280
uint256 _newTotalWeight = totalFundingPoolWeight; _newTotalWeight = _newTotalWeight - fundingPoolWeights[_pool]; fundingPoolWeights[_pool] = _weight; _newTotalWeight = _newTotalWeight + _weight; totalFundingPoolWeight = _newTotalWeight;
Simply avoiding the numbers of setting values at variables can reduce the gast cost.
uint256 _newTotalWeight = totalFundingPoolWeight - fundingPoolWeights[_pool] + _weight; fundingPoolWeights[_pool] = _weight; totalFundingPoolWeight = _newTotalWeight;
The gas cost of testSetFundingPoolWeight() decreases with this change from 247334 to 247300
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L343
require(length > 0, "CitadelMinter: no funding pools");
It can use != 0 instead of > 0 to reduce the gas cost.
require(length != 0, "CitadelMinter: no funding pools");
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L374
function _addFundingPool(address _pool) internal { require( fundingPools.add(_pool), "CitadelMinter: funding pool already exists" ); }
_addFundingPool function is only used by 1 place (https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L273-L275). In this case, _addFundingPool does not need to be defined and it simply can put the logic instead of calling the function. This simplification can reduce gas cost as well.
Remove _addFundingPool function and port the logic to line 274 of CitadelMinter.sol
if (!poolExists) { require( fundingPools.add(_pool), "CitadelMinter: funding pool already exists" ); }
The gas cost of testSetFundingPoolWeight() decreases with this change from 247334 to 247300
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/SupplySchedule.sol#L112-L120
uint256 rate = epochRate[i]; uint256 epochStartTime = cachedGlobalStartTimestamp + i * epochLength; uint256 epochEndTime = cachedGlobalStartTimestamp + (i + 1) * epochLength; uint256 time = MathUpgradeable.min(block.timestamp, epochEndTime) - MathUpgradeable.max(lastMintTimestamp, epochStartTime); mintable += rate * time;
There are following refactoring opportunities:
rate
variable does not need to be definedepochEndTime
variable can use epochStartTime
variableepochEndTime
does not need to be defineduint256 epochStartTime = cachedGlobalStartTimestamp + i * epochLength; uint256 time = MathUpgradeable.min(block.timestamp, epochStartTime + epochLength) - MathUpgradeable.max(lastMintTimestamp, epochStartTime); mintable += epochRate[i] * time;
onlyRoles
modifier is not used throughout the codebase// @dev only holders of any of the given set of roles on the GAC can call modifier onlyRoles(bytes32[] memory roles) { bool validRoleFound = false; for (uint256 i = 0; i < roles.length; i++) { bytes32 role = roles[i]; if (gac.hasRole(role, msg.sender)) { validRoleFound = true; break; } } require(validRoleFound, "GAC: invalid-caller-role"); _; }
onlyRoles can be removed.
modifier onlyRoleOrAddress(bytes32 role, address account) { require( gac.hasRole(role, msg.sender) || msg.sender == account, "GAC: invalid-caller-role-or-address" ); _; }
gac.hasRole(...) requires the call to the external contract. Hence, msg.sender == account
check should happen first.
modifier onlyRoleOrAddress(bytes32 role, address account) { require( msg.sender == account || gac.hasRole(role, msg.sender), "GAC: invalid-caller-role-or-address" ); _; }
modifier gacPausable() { require(!gac.paused(), "global-paused"); require(!paused(), "local-paused"); _; }
gac.paused() requires the call to the external contract. Hence, !paused()
check should happen first.
modifier gacPausable() { require(!paused(), "local-paused"); require(!gac.paused(), "global-paused"); _; }
mintable
variable at getMintable function inhttps://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/SupplySchedule.sol#L103
uint256 mintable = 0;
Not setting 0 at mintable variable can reduce the gas cost.
uint256 mintable;
https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L236
if (assetCumulativeFunded < assetCap) { limitLeft_ = assetCap - assetCumulativeFunded; }
assetCap - assetCumulativeFunded
will not be underflown. So it can be wrapped with unchecked.
if (assetCumulativeFunded < assetCap) { unchecked { limitLeft_ = assetCap - assetCumulativeFunded; } }