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: 43/72
Findings: 2
Award: $144.74
🌟 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
92.0693 USDC - $92.07
April 18, 2022
@securerodd
Within StakedCitadel.sol
, the function vest(address recipient, uint256 _amount, uint256 _unlockBegin)
allows any value of _unlockBegin
to be passed in. This could effectively defeat the purpose of the vestingDuration, as _unlockBegin
can be set to something arbitrarily high (such that funds will not vest) or something arbitrarily low (meaning the vestingDuration could be bypassed).
Current 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].unlockBegin = _unlockBegin; vesting[recipient].unlockEnd = _unlockBegin + vestingDuration;
Recommendation:
Consider calculating the vesting[recipient].unlockEnd
based on the time instead of a passed in value.
Ex: vesting[recipient].unlockEnd = block.timestamp + vestingDuration;
The CitadelToken.sol
does not contain a supplycap for minting.
Code:
function mint(address dest, uint256 amount) external onlyRole(CITADEL_MINTER_ROLE) gacPausable { _mint(dest, amount); }
CTDL is the base token for the system and as such, not having supply restrictions increases the potential for inflationary issues and/or concerns.
Recommendation: Consider establishing a cap on the supply of CTDL for the system.
🌟 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.6692 USDC - $52.67
April 18, 2022
@securerodd
The parameters for the vesting schedule in StakedCitadelVester.sol
are stored in the below struct:
struct VestingParams { uint256 unlockBegin; uint256 unlockEnd; uint256 lockedAmounts; uint256 claimedAmounts; }
Since the unlockBegin and unlockEnd variables inside of the struct should be based on current time and vesting durations, they are good candidates for struct packing. They could be reduced to uint128 quite easily so that they both fit in the same storage slot and yet they would still allow for ridiculously large times.
Recommended:
struct VestingParams { uint128 unlockBegin; uint128 unlockEnd; uint256 lockedAmounts; uint256 claimedAmounts; }
The for loops used throughout the codebase can benefit from one or more of the following gas saving techniques:
Example code in GlobalAccessControlManaged.sol
:
modifier onlyRoles(bytes32[] memory roles) { bool validRoleFound = false; for (uint256 i = 0; i < roles.length; i++) { bytes32 role = roles[i]; ... }
Recommended code:
modifier onlyRoles(bytes32[] memory roles) { bool validRoleFound = false; uint256 numRoles = roles.length; for (uint256 i = 0; i < numRoles;) { bytes32 role = roles[i]; ... unchecked {++i}; }
Example code in getFundingPoolWeights()
within CitadelMinter.sol
:
uint256 numPools = fundingPools.length(); pools = new address[](numPools); weights = new uint256[](numPools); for (uint256 i = 0; i < numPools; i++) { address pool = fundingPools.at(i); uint256 weight = fundingPoolWeights[pool]; ... }
Recommended code:
uint256 numPools = fundingPools.length(); pools = new address[](numPools); weights = new uint256[](numPools); for (uint256 i = 0; i < numPools;) { address pool = fundingPools.at(i); uint256 weight = fundingPoolWeights[pool]; ... unchecked {++i}; }
Example code in _transferToFundingPools(uint256 _citadelAmount)
within CitadelMinter.sol
:
uint256 length = fundingPools.length(); // Use cached to save 96 gas per loop read uint256 cachedTotalFundingPoolWeight = totalFundingPoolWeight; require(length > 0, "CitadelMinter: no funding pools"); for (uint256 i; i < length; ++i) { address pool = fundingPools.at(i); ... }
Recommended code:
uint256 length = fundingPools.length(); // Use cached to save 96 gas per loop read uint256 cachedTotalFundingPoolWeight = totalFundingPoolWeight; require(length > 0, "CitadelMinter: no funding pools"); for (uint256 i; i < length;) { address pool = fundingPools.at(i); ... unchecked {++i}; }
In the `` function within .sol
, the value stored in _pool
is calculated by calling balance()
. Immediately following, the value stored in _before is calculated using token.balanceOf(address(this))
.
balance()
is a wrapper around token.balanceOf(address(this))
making this set of actions inefficient.
Current Code:
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);
Recommended Code:
uint256 _poolBefore = balance(); token.safeTransferFrom(msg.sender, address(this), _amount); uint256 _after = token.balanceOf(address(this)); _mintSharesFor(_recipient, _after - _poolBefore, _poolBefore);
within require statements, != comparisons are slightly more gas efficient than > and when a uint is being compared to 0, != is effectively the same as > 0.
Gas savings is typically 3 per statement.
Locations: