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: 1/72
Findings: 4
Award: $9,864.86
🌟 Selected for report: 2
🚀 Solo Findings: 1
184.248 USDC - $184.25
The amount of citadel bought when there is no discount is always zero. If the user doesn't specify, or specifies zero as the _minCitadelOut
, then the user will get no xCitadel and will still have to pay the full price.
If funding.discount
is equal to zero, citadelAmount_
will remain at the uninitialized value of zero rather than the correct value of citadelAmountWithoutDiscount
File: src/Funding.sol (lines 202-216)
function getAmountOut(uint256 _assetAmountIn) public view returns (uint256 citadelAmount_) { uint256 citadelAmountWithoutDiscount = _assetAmountIn * citadelPriceInAsset; if (funding.discount > 0) { citadelAmount_ = (citadelAmountWithoutDiscount * MAX_BPS) / (MAX_BPS - funding.discount); } citadelAmount_ = citadelAmount_ / assetDecimalsNormalizationValue; }
Code inspection
Initialize citadelAmount_
to citadelAmountWithoutDiscount
#0 - GalloDaSballo
2022-04-22T22:56:21Z
Agree
#1 - jack-the-pug
2022-05-29T07:12:01Z
Dup #149
🌟 Selected for report: IllIllI
8480.7862 USDC - $8,480.79
During the video it was explained that the policy operations team was meant to be a nimble group that could change protocol values considered to be safe. Further, it was explained that since pricing comes from an oracle, and there would have to be unusual coordination between the two to affect outcomes, the group was given the ability to clear the pricing flag to get things moving again once the price was determined to be valid
If an oracle price falls out of the valid min/max range, the citadelPriceFlag
is set to true, but the out-of-bounds value is not stored. If the policy operations team calls clearCitadelPriceFlag()
, the stale price from before the flag will be used. Not only is it an issue because of stale prices, but this means the policy op team now has a way to affect pricing not under the control of the oracle (i.e. no unusual coordination required to affect an outcome). Incorrect pricing leads to incorrect asset valuations, and loss of funds.
The flag is set but the price is not stored File: src/Funding.sol (lines 427-437)
if ( _citadelPriceInAsset < minCitadelPriceInAsset || _citadelPriceInAsset > maxCitadelPriceInAsset ) { citadelPriceFlag = true; emit CitadelPriceFlag( _citadelPriceInAsset, minCitadelPriceInAsset, maxCitadelPriceInAsset ); } else {
Code inspection
Always set the citadelPriceInAsset
#0 - GalloDaSballo
2022-04-22T22:54:37Z
I don't think here's a Medium Severity finding here, but would like @dapp-whisperer thoughts
#1 - shuklaayush
2022-05-11T21:53:31Z
Makes sense. It's best to update the price even when it's flagged
#2 - jack-the-pug
2022-06-05T04:56:44Z
This is a very good catch! citadelPriceInAsset
is not updated when citadelPriceFlag
is set, therefore clearing the flag will not approve the out of range price but continues with a stale price before the out of range price.
🌟 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
894.3077 USDC - $894.31
Vulnerability details:
If citadelPriceInAsset
is above the new max or below the new min, the next update will likely have a similar value and immediately cause problems. The code should require that the current value falls within the new range
minCitadelPriceInAsset = _minPrice; maxCitadelPriceInAsset = _maxPrice;
If tokenOutPrice
is less than tokenInNormalizationValue
, then the amount will be zero for some amounts. The caller of getAmountOut()
should revert if tokenOutAmount
ends up being zero
tokenOutAmount_ = (_tokenInAmount * tokenOutPrice) / tokenInNormalizationValue;
decimals()
, name()
and symbol()
are optional parts of the ERC20 specification, so there are tokens that do not implement them. It's not safe to cast arbitrary token addresses in order to call these functions. If IERC20Metadata
is to be relied on, that should be the variable type of the token variable, rather than it being address
, so the compiler can verify that types correctly match, rather than this being a runtime failure. See this prior instance of this issue which was marked as Low risk. Do this to resolve the issue.
function name() external view returns (string memory); function symbol() external view returns (string memory); function decimals() external view returns (uint256);
tokenInNormalizationValue = 10**tokenIn.decimals();
abi.encodePacked(_defaultNamePrefix, namedToken.name())
abi.encodePacked(_symbolSymbolPrefix, namedToken.symbol())
address(0x0)
when assigning values to address
state variablesstakingProxy = _staking;
strategist = _strategist;
keeper = _keeper;
governance = _governance;
initialize
functions can be front-runSee this finding from a prior badger-dao contest for details
function initialize(
) external initializer {
) external initializer {
) public initializer whenNotPaused {
now
is deprecatedUse block.timestamp
instead
require(timestamps[index_recent].add(reportDelaySec) <= now);
reports[index_past].timestamp = now;
emit ProviderReportPushed(providerAddress, payload, now);
uint256 minValidTimestamp = now.sub(reportExpirationTimeSec);
uint256 maxValidTimestamp = now.sub(reportDelaySec);
safeApprove()
is deprecatedDeprecated in favor of safeIncreaseAllowance()
and safeDecreaseAllowance()
IERC20Upgradeable(_citadelToken).safeApprove(_xCitadel, type(uint256).max);
IERC20Upgradeable(_xCitadel).safeApprove(_xCitadelLocker, type(uint256).max);
IERC20(_citadel).safeApprove(address(_xCitadel), type(uint256).max);
Code architecture, incentives, and error handling/reporting questions/issues should be resolved before deployment
* TODO: Better revert strings
// TODO: we should conform to some interface here
// TODO: Check gas costs. How does this relate to market buying if you do want to deposit to xCTDL?
/// TODO: Add string -> hash EnumerableSet to a new RoleRegistry contract for easy on-chain viewing.
* TODO: Better revert strings
// TODO: Require this epoch is in the future. What happens if no data is set? (It just fails to mint until set)
__gap[50]
storage variable to allow for new storage variables in later versionsSee this link for a description of this storage variable. While some contracts may not currently be sub-classed, adding the variable now protects against forgetting to add it in the future.
contract StakedCitadelLocker is Initializable, ReentrancyGuardUpgradeable, OwnableUpgradeable {
contract CitadelMinter is GlobalAccessControlManaged, ReentrancyGuardUpgradeable
contract CitadelToken is GlobalAccessControlManaged, ERC20Upgradeable {
contract Funding is GlobalAccessControlManaged, ReentrancyGuardUpgradeable {
contract GlobalAccessControl is AccessControlEnumerableUpgradeable, PausableUpgradeable
contract KnightingRound is GlobalAccessControlManaged, ReentrancyGuardUpgradeable {
contract GlobalAccessControlManaged is PausableUpgradeable {
contract StakedCitadel is ERC20Upgradeable, SettAccessControl, PausableUpgradeable, ReentrancyGuardUpgradeable
contract StakedCitadelVester is GlobalAccessControlManaged, ReentrancyGuardUpgradeable
The value of transferFromDisabled
is never updated, let alone in an initialize()
function. I don't see any bugs related to this, but this comment makes it seem as though something was overlooked when branching.
bool public transferFromDisabled; // Set to true in initialize
If there are too many pools, the function may run out of gas while returning them. It's best to allow for a start offset and maximum length, so data can be returned in batches that don't run out of gas
function getFundingPoolWeights() external view returns (address[] memory pools, uint256[] memory weights) {
These are the only two differences between IEmptyStrategy
and IStrategy
. IEmptyStrategy
should be changed to be is IStrategy
and remove the duplicate functions
function initialize(address vault, address want) external; function getName() external view returns (string memory);
// SPDX-License-Identifier: MIT
/** * @title Badger Geyser @dev Tracks stakes and pledged tokens to be distributed, for use with @dev BadgerTree merkle distribution system. An arbitrary number of tokens to distribute can be specified. */
// @dev duplicate of getMintable() with debug print added // @dev this function is out of scope for reviews and audits
There are a lot of references to the old owner-related code. The comments should be updated to talk about the new RBAC system
$ grep owner src/KnightingRound.sol * @notice Finalize the sale after sale duration. Can only be called by owner * @notice Update the sale start time. Can only be called by owner * @notice Update sale duration. Can only be called by owner * @notice Modify the tokenOut price in. Can only be called by owner * @notice Update the `tokenIn` receipient address. Can only be called by owner * @notice Update the guestlist address. Can only be called by owner * @notice Modify the max tokenIn that this contract can take. Can only be called by owner * @notice Transfers out any tokens accidentally sent to the contract. Can only be called by owner
The price calulation seems inverted since this comment was first written, so it should be updated to reflect the new calculation: 2. File: src/KnightingRound.sol (line 43)
/// eg. 1 WBTC (8 decimals) = 40,000 CTDL ==> price = 10^8 / 40,000
include
for ds-testTest code should not be mixed in with production code. The production version should be extended and have its functions overridden for testing purposes
import "ds-test/test.sol";
nonReentrant
modifier
should occur before all other modifiersThis is a best-practice to protect against reentrancy in other modifiers
nonReentrant
nonReentrant
) external onlyRole(POLICY_OPERATIONS_ROLE) gacPausable nonReentrant {
nonReentrant
nonReentrant
function sweep(address _token) external gacPausable nonReentrant onlyRole(TREASURY_OPERATIONS_ROLE) {
The below pragmas should be <
0.9.0, not <=
$ grep '<= 0.9.0' src/*/*/* src/interfaces/badger/IBadgerGuestlist.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/badger/IBadgerVipGuestlist.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/badger/IEmptyStrategy.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/badger/IStrategy.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/badger/IVault.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/citadel/ICitadelToken.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/citadel/IGac.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/citadel/IStakedCitadelLocker.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/citadel/ISupplySchedule.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/citadel/IVesting.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/convex/BoringMath.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/convex/IRewardStaking.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/convex/IStakingProxy.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/convex/MathUtil.sol:pragma solidity >= 0.5.0 <= 0.9.0; src/interfaces/erc20/IERC20.sol:pragma solidity >= 0.5.0 <= 0.9.0;
return
statement when the function defines a named return variable, is redundantreturn userRewards;
return amount;
return amount;
return supply;
return supply;
require()
/revert()
statements should have descriptive reason stringsrequire(reportExpirationTimeSec_ <= MAX_REPORT_EXPIRATION_TIME);
require(minimumProviders_ > 0);
require(reportExpirationTimeSec_ <= MAX_REPORT_EXPIRATION_TIME);
require(minimumProviders_ > 0);
require(timestamps[0] > 0);
require(timestamps[index_recent].add(reportDelaySec) <= now);
require (providerReports[providerAddress][0].timestamp > 0);
require(providerReports[provider][0].timestamp == 0);
require(_stakingToken != address(0)); // dev: _stakingToken address should not be zero
require(rewardData[_rewardsToken].lastUpdateTime == 0);
require(rewardData[_rewardsToken].lastUpdateTime > 0);
require(rewardDistributors[_rewardsToken][msg.sender]);
require(gac.hasRole(PAUSER_ROLE, msg.sender));
require(gac.hasRole(UNPAUSER_ROLE, msg.sender));
require(_token != address(0)); // dev: _token address should not be zero
require(_governance != address(0)); // dev: _governance address should not be zero
require(_keeper != address(0)); // dev: _keeper address should not be zero
require(_guardian != address(0)); // dev: _guardian address should not be zero
require(_treasury != address(0)); // dev: _treasury address should not be zero
require(_strategist != address(0)); // dev: _strategist address should not be zero
require(_badgerTree != address(0)); // dev: _badgerTree address should not be zero
require(_vesting != address(0)); // dev: _vesting address should not be zero
public
functions not called by the contract should be declared external
insteadContracts are allowed to override their parents' functions and change the visibility from external
to public
.
function initialize( address _stakingToken, string calldata name, string calldata symbol ) public initializer {
function decimals() public view returns (uint8) {
function name() public view returns (string memory) {
function symbol() public view returns (string memory) {
function version() public view returns(uint256){
function addReward( address _rewardsToken, address _distributor, bool _useBoost ) public onlyOwner {
function lastTimeRewardApplicable(address _rewardsToken) public view returns(uint256) {
function initialize( string memory _name, string memory _symbol, address _gac ) public initializer {
function getStakedCitadelAmountOut(uint256 _assetAmountIn) public view returns (uint256 xCitadelAmount_) {
function __GlobalAccessControlManaged_init(address _globalAccessControl) public onlyInitializing
function setGovernance(address _governance) public {
function initialize( address _token, address _governance, address _keeper, address _guardian, address _treasury, address _strategist, address _badgerTree, address _vesting, string memory _name, string memory _symbol, uint256[4] memory _feeConfig ) public initializer whenNotPaused {
function getPricePerFullShare() public view returns (uint256) {
function initialize(address _gac) public initializer {
function getEmissionsForCurrentEpoch() public view returns (uint256) {
constant
s should be defined rather than using magic numbers_decimals = 18;
require(_max < 1500, "over max payment"); //max 15%
require(_rate < 30000, "over max rate"); //max 3x
require(_rate <= 500, "over max rate"); //max 5% per epoch
rewardData[_rewardsToken].rewardRate).mul(1e18).div(rewardData[_rewardsToken].useBoost ? boostedSupply : lockedSupply)
).div(1e18).add(rewards[_user][_rewardsToken]);
for (uint256 i = 0; i < 128; i++) {
require(_weight <= 10000, "exceed max funding pool weight");
uint256[4] memory _feeConfig
_feeConfig[3] <= MANAGEMENT_FEE_HARD_CAP,
managementFee = _feeConfig[3];
toEarnBps = 9_500; // initial value of toEarnBps // 95% is invested to the strategy, 5% for cheap withdrawals
epochRate[0] = 593962000000000000000000 / epochLength;
epochRate[1] = 591445000000000000000000 / epochLength;
epochRate[2] = 585021000000000000000000 / epochLength;
epochRate[3] = 574138000000000000000000 / epochLength;
epochRate[3] = 574138000000000000000000 / epochLength;
epochRate[4] = 558275000000000000000000 / epochLength;
epochRate[4] = 558275000000000000000000 / epochLength;
epochRate[5] = 536986000000000000000000 / epochLength;
epochRate[5] = 536986000000000000000000 / epochLength;
There are units for seconds, minutes, hours, days, and weeks
uint256 public constant rewardsDuration = 86400; // 1 day
uint256 public constant rewardsDuration = 86400; // 1 day
uint256 public constant INITIAL_VESTING_DURATION = 86400 * 21; // 21 days of vesting
uint256 public constant INITIAL_VESTING_DURATION = 86400 * 21; // 21 days of vesting
Consider defining in only one contract so that values cannot become out of sync when only one location is updated
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
seen in src/CitadelMinter.sol
bytes32 public constant POLICY_OPERATIONS_ROLE = keccak256("POLICY_OPERATIONS_ROLE");
seen in src/CitadelMinter.sol
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
seen in src/Funding.sol
bytes32 public constant POLICY_OPERATIONS_ROLE = keccak256("POLICY_OPERATIONS_ROLE");
seen in src/Funding.sol
bytes32 public constant TREASURY_OPERATIONS_ROLE = keccak256("TREASURY_OPERATIONS_ROLE");
seen in src/Funding.sol
bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");
seen in src/Funding.sol
bytes32 public constant CITADEL_MINTER_ROLE = keccak256("CITADEL_MINTER_ROLE");
seen in src/CitadelToken.sol
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
seen in src/GlobalAccessControl.sol
bytes32 public constant TREASURY_GOVERNANCE_ROLE = keccak256("TREASURY_GOVERNANCE_ROLE");
seen in src/GlobalAccessControl.sol
bytes32 public constant TECH_OPERATIONS_ROLE = keccak256("TECH_OPERATIONS_ROLE");
seen in src/GlobalAccessControl.sol
bytes32 public constant TREASURY_OPERATIONS_ROLE = keccak256("TREASURY_OPERATIONS_ROLE");
seen in src/GlobalAccessControl.sol
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
seen in src/GlobalAccessControl.sol
bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE");
seen in src/GlobalAccessControl.sol
uint256 public constant MAX_BPS = 10_000;
seen in src/Funding.sol
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
seen in src/KnightingRound.sol
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
seen in src/StakedCitadelVester.sol
pragma solidity ^0.8.0;
pragma solidity ^0.8.0;
pragma solidity ^0.8.12;
//stop now as no futher checks are needed
futher
* @dev this contract is intended to be the only way citadel is minted, with the expection of the initial minting event
expection
* @param _assetCap New max cumulatiive amountIn
cumulatiive
/// @dev We let assets accumulate and batch transfer to treasury (rather than transfer atomically on each deposi)t for user gas savings
deposi)t
* @notice Update the `tokenIn` receipient address. Can only be called by owner
receipient
* @dev this is assumed to be used in the initializer of the inhereiting contract
inhereiting
// @dev used to faciliate extra contract-specific permissioned accounts
faciliate
address public badgerTree; // Address we send tokens too via reportAdditionalTokens
too -> to
pragma solidity 0.4.24;
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
/// SPDX-License-Identifier: MIT
/// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
Wrong parameter description
* @param _minCitadelOut ID of DAO to vote for
/** * @notice Initializer. * @param _gac Global access control * @param _citadel The token this contract will return in a trade * @param _asset The token this contract will receive in a trade * @param _xCitadel Staked citadel, citadel will be granted to funders in this form * @param _saleRecipient The address receiving the proceeds of the sale - will be citadel multisig * @param _assetCap The max asset that the contract can take */ function initialize( address _gac, address _citadel, address _asset, address _xCitadel, address _saleRecipient, address _citadelPriceInAssetOracle, uint256 _assetCap ) external initializer {
Missing: @param _citadelPriceInAssetOracle
/** * @notice Initializer. * @param _tokenOut The token this contract will return in a trade (citadel) * @param _tokenIn The token this contract will receive in a trade * @param _saleStart The time when tokens can be first purchased * @param _saleDuration The duration of the token sale * @param _tokenOutPrice The tokenOut per tokenIn price * @param _saleRecipient The address receiving the proceeds of the sale - will be citadel multisig * @param _guestlist Address that will manage auction approvals * @param _tokenInLimit The max tokenIn that the contract can take */ function initialize( address _globalAccessControl, address _tokenOut, address _tokenIn, uint256 _saleStart, uint256 _saleDuration, uint256 _tokenOutPrice, address _saleRecipient, address _guestlist, uint256 _tokenInLimit ) external initializer {
Missing: @param _globalAccessControl
/// @notice Initializes the Sett. Can only be called once, ideally when the contract is deployed. /// @param _token Address of the token that can be deposited into the sett. /// @param _governance Address authorized as governance. /// @param _keeper Address authorized as keeper. /// @param _guardian Address authorized as guardian. /// @param _treasury Address to distribute governance fees/rewards to. /// @param _strategist Address authorized as strategist. /// @param _badgerTree Address of badgerTree used for emissions. /// @param _name Specify a custom sett name. Leave empty for default value. /// @param _symbol Specify a custom sett symbol. Leave empty for default value. /// @param _feeConfig Values for the 4 different types of fees charges by the sett /// [performanceFeeGovernance, performanceFeeStrategist, withdrawToVault, managementFee] /// Each fee should be less than the constant hard-caps defined above. function initialize( address _token, address _governance, address _keeper, address _guardian, address _treasury, address _strategist, address _badgerTree, address _vesting, string memory _name, string memory _symbol, uint256[4] memory _feeConfig ) public initializer whenNotPaused {
Missing: @param _vesting
/// @notice Deposits `_amount` tokens, issuing shares to `recipient`. /// Checks the guestlist to verify that `recipient` is authorized to make a deposit for the specified `_amount`. /// Note that deposits are not accepted when the Sett is paused or when `pausedDeposit` is true. /// @dev See `_depositForWithAuthorization` for details on guestlist authorization. /// @param _recipient Address to issue the Sett shares to. /// @param _amount Quantity of tokens to deposit. function depositFor( address _recipient, uint256 _amount, bytes32[] memory proof ) external whenNotPaused {
Missing: @param proof
indexed
fieldsEach event
should use three indexed
fields if there are three or more fields
event ProviderAdded(address provider);
event ProviderRemoved(address provider);
event ReportTimestampOutOfRange(address provider);
event ProviderReportPushed(address indexed provider, uint256 payload, uint256 timestamp);
event RewardAdded(address indexed _token, uint256 _reward);
event Staked(address indexed _user, uint256 indexed _epoch, uint256 _paidAmount, uint256 _lockedAmount, uint256 _boostedAmount);
event Withdrawn(address indexed _user, uint256 _amount, bool _relocked);
event KickReward(address indexed _user, address indexed _kicked, uint256 _reward);
event RewardPaid(address indexed _user, address indexed _rewardsToken, uint256 _reward);
event Recovered(address _token, uint256 _amount);
event FundingPoolWeightSet( address pool, uint256 weight, uint256 totalFundingPoolWeight );
event CitadelDistributionSplitSet( uint256 fundingBps, uint256 stakingBps, uint256 lockingBps );
event CitadelDistribution( uint256 fundingAmount, uint256 stakingAmount, uint256 lockingAmount );
event CitadelDistributionToFunding( uint256 startTime, uint256 endTime, uint256 citadelAmount );
event CitadelDistributionToFundingPool( uint256 startTime, uint256 endTime, address pool, uint256 citadelAmount );
event CitadelDistributionToStaking( uint256 startTime, uint256 endTime, uint256 citadelAmount );
event CitadelDistributionToLocking( uint256 startTime, uint256 endTime, uint256 citadelAmount, uint256 xCitadelAmount );
event Deposit( address indexed buyer, uint256 assetIn, uint256 citadelOutValue );
event CitadelPriceInAssetUpdated(uint256 citadelPrice);
event CitadelPriceBoundsSet(uint256 minPrice, uint256 maxPrice);
event CitadelPriceFlag(uint256 price, uint256 minPrice, uint256 maxPrice);
event AssetCapUpdated(uint256 assetCap);
event Sweep(address indexed token, uint256 amount);
event ClaimToTreasury(address indexed token, uint256 amount);
event DiscountLimitsSet(uint256 minDiscount, uint256 maxDiscount);
event DiscountSet(uint256 discount);
event DiscountManagerSet(address discountManager);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval( address indexed owner, address indexed spender, uint256 value );
event Sale( address indexed buyer, uint8 indexed daoId, uint256 amountIn, uint256 amountOut );
event Claim(address indexed claimer, uint256 amount);
event SaleStartUpdated(uint256 saleStart);
event SaleDurationUpdated(uint256 saleDuration);
event TokenOutPriceUpdated(uint256 tokenOutPrice);
event TokenInLimitUpdated(uint256 tokenInLimit);
event Sweep(address indexed token, uint256 amount);
event TreeDistribution( address indexed token, uint256 amount, uint256 indexed blockNumber, uint256 timestamp );
event Harvested( address indexed token, uint256 amount, uint256 indexed blockNumber, uint256 timestamp );
event SetToEarnBps(uint256 newEarnToBps);
event SetMaxWithdrawalFee(uint256 newMaxWithdrawalFee);
event SetMaxPerformanceFee(uint256 newMaxPerformanceFee);
event SetMaxManagementFee(uint256 newMaxManagementFee);
event SetWithdrawalFee(uint256 newWithdrawalFee);
event SetPerformanceFeeStrategist(uint256 newPerformanceFeeStrategist);
event SetPerformanceFeeGovernance(uint256 newPerformanceFeeGovernance);
event SetManagementFee(uint256 newManagementFee);
event Vest( address indexed recipient, uint256 _amount, uint256 _unlockBegin, uint256 _unlockEnd );
event Claimed( address indexed owner, address indexed recipient, uint256 amount );
event SetVestingDuration(uint256 duration);
event MintingStartTimeSet(uint256 globalStartTimestamp);
event EpochSupplyRateSet(uint256 epoch, uint256 rate);
Reentrancy in CitadelMinter.mintAndDistribute() (src/CitadelMinter.sol#169-243): External calls: - citadelToken.mint(address(this),mintable) (src/CitadelMinter.sol#178-180) - IVault(cachedXCitadel).deposit(lockingAmount) (src/CitadelMinter.sol#195-197) - xCitadelLocker.notifyRewardAmount(address(cachedXCitadel),xCitadelToLockers) (src/CitadelMinter.sol#201-205) - IERC20Upgradeable(address(citadelToken)).safeTransfer(address(cachedXCitadel),stakingAmount) (src/CitadelMinter.sol#217-218) - _transferToFundingPools(fundingAmount) (src/CitadelMinter.sol#230-231) - returndata = address(token).functionCall(data,SafeERC20: low-level call failed) (node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol#93) - (success,returndata) = target.call{value: value}(data) (node_modules/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol#137) - IERC20Upgradeable(address(citadelToken)).safeTransfer(pool,amount) (src/CitadelMinter.sol#351-353) External calls sending eth: - _transferToFundingPools(fundingAmount) (src/CitadelMinter.sol#230-231) - (success,returndata) = target.call{value: value}(data) (node_modules/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol#137) State variables written after the call(s): - lastMintTimestamp = block.timestamp (src/CitadelMinter.sol#240-241)
Reentrancy in StakedCitadel._withdraw(uint256) (src/StakedCitadel.sol#808-840): External calls: - IStrategy(strategy).withdraw(_toWithdraw) (src/StakedCitadel.sol#818-819) - IVesting(vesting).setupVesting(msg.sender,_amount,block.timestamp) (src/StakedCitadel.sol#830-831) - token.safeTransfer(vesting,_amount) (src/StakedCitadel.sol#831-833) State variables written after the call(s): - _mintSharesFor(treasury,_fee,balance() - _fee) (src/StakedCitadel.sol#836-837) - _balances[account] += amount (node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol#268) - _mintSharesFor(treasury,_fee,balance() - _fee) (src/StakedCitadel.sol#836-837) - _totalSupply += amount (node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol#267)
Reentrancy in StakedCitadel._depositFor(address,uint256) (src/StakedCitadel.sol#764-779): External calls: - token.safeTransferFrom(msg.sender,address(this),_amount) (src/StakedCitadel.sol#774-775) State variables written after the call(s): - _mintSharesFor(_recipient,_after - _before,_pool) (src/StakedCitadel.sol#776-777) - _balances[account] += amount (node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol#268) - _mintSharesFor(_recipient,_after - _before,_pool) (src/StakedCitadel.sol#776-777) - _totalSupply += amount (node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol#267) Reentrancy in Funding.updateCitadelPriceInAsset() (src/Funding.sol#414-443): External calls: - (_citadelPriceInAsset,_valid) = IMedianOracle(citadelPriceInAssetOracle).getData() (src/Funding.sol#422-423) State variables written after the call(s): - citadelPriceFlag = true (src/Funding.sol#431-432) - citadelPriceInAsset = _citadelPriceInAsset (src/Funding.sol#438-439)
#0 - liveactionllama
2022-04-20T22:26:02Z
Warden created this issue as a placeholder, because their submission was too large for the contest form. They then emailed their md file to our team. I've updated this issue with their md file content.
#1 - liveactionllama
2022-05-06T22:35:40Z
Note: my original copy/paste did not capture all of the warden's content. I've updated this issue to now contain the entirety of the original submission. I've also notified the sponsor. Contest has not yet moved to judging.
#2 - jack-the-pug
2022-06-05T15:30:18Z
Misleading comment, now is deprecated and Open TODOs in the Low Risk Issues section should be Non-critical
.
🌟 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
305.51 USDC - $305.51
Vulnerability details:
Use a solidity version of at least 0.8.0 to get overflow protection without SafeMath
Use a solidity version of at least 0.8.2 to get compiler automatic inlining
Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads
Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require()
strings
Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value
pragma solidity 0.4.24;
pragma solidity 0.6.12;
Use a solidity version of at least 0.8.2 to get compiler automatic inlining
Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads
Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require()
strings
Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value
pragma solidity ^0.8.0;
pragma solidity ^0.8.0;
pragma solidity ^0.8.0;
These unused state variables are wasting a lot of gas. The compiler automatically creates non-payable getter functions which is expensive and each variable, since it is given a value, costs a ton of gas to initialize. Variables initialized to zero cost Gsreset (2900 gas) and variables initialized to non-zero cost Gsset (20000 gas)
// ========== Not used ========== //boost address public boostPayment = address(0); uint256 public maximumBoostPayment = 0; uint256 public boostRate = 10000; uint256 public nextMaximumBoostPayment = 0; uint256 public nextBoostRate = 10000; uint256 public constant denominator = 10000; // ==============================
immutable
to just private
wastes a lot of gasOne of the changes made after branching this file was to remove the immutable
keyword. Doing this changes the variable from a deployment-time replacement to a state variable that gets assigned, costing Gsset (20000 gas). The variable's value does not change after initialization, so it should remain immutable
uint8 private _decimals;
address
mappings can be combined into a single mapping
of an address
to a struct
, where appropriateSaves a storage slot for the mapping as well as a non-payable getter for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas). Reads and subsequent writes are also cheaper
// user -> reward token -> amount mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid; mapping(address => mapping(address => uint256)) public rewards;
mapping(address => Balances) public balances; mapping(address => LockedBalance[]) public userLocks;
mapping(address => uint256) public boughtAmounts; /// Whether an account has claimed tokens /// NOTE: can reset boughtAmounts after a claim to optimize gas /// but we need to persist boughtAmounts mapping(address => bool) public hasClaimed;
mapping(address => uint256) public additionalTokensEarned; mapping(address => uint256) public lastAdditionalTokenAmount;
If variables occupying the same slot are both written the same function or by the constructor, avoids a separate Gsset (20000 gas). Reads of the variables are also cheaper
IERC20Upgradeable public stakingToken; // xCTDL token
Variable ordering with two fewer slots: address:rewardTokens, mapping(32):rewardData, mapping(32):rewardDistributors, mapping(32):userRewardPerTokenPaid, mapping(32):rewards, uint256(32):lockedSupply, uint256(32):boostedSupply, user-defined:epochs, mapping(32):balances, mapping(32):userLocks, uint256(32):maximumBoostPayment, uint256(32):boostRate, uint256(32):nextMaximumBoostPayment, uint256(32):nextBoostRate, uint256(32):minimumStake, uint256(32):maximumStake, uint256(32):kickRewardPerEpoch, uint256(32):kickRewardEpochDelay, string(32):_name, string(32):_symbol, user-defined(20):stakingToken, bool(1):isShutdown, uint8(1):_decimals, address(20):boostPayment, address(20):stakingProxy
IERC20 public citadel; /// token to distribute (in vested xCitadel form)
Variable ordering with one fewer slots: uint256(32):citadelPriceInAsset, uint256(32):minCitadelPriceInAsset, uint256(32):maxCitadelPriceInAsset, uint256(32):assetDecimalsNormalizationValue, user-defined(20):citadel, bool(1):citadelPriceFlag, user-defined(20):xCitadel, user-defined(20):asset, address(20):citadelPriceInAssetOracle, address(20):saleRecipient, user-defined(*):funding
Reading from storage is expensive, so it saves gas when only one variable has to be read versus multiple. If there is a calculation which requires multiple storage reads, the calculation should be optimized to pre-calculate as much as possible, and store the intermediate result in storage. Replace the saleDuration
variable with a private
pre-calculated saleEnd
timestamp, and reference that rather than checking both saleStart
and saleDuration
in multiple places. The duration can be calculated by subtracting the start from the end in a view
function. Making this change saves a Gcoldsload (2100 gas) for each call to buy()
and saleEnded()
require( block.timestamp < saleStart + saleDuration, "KnightingRound: already ended" );
hasEnded_ = (block.timestamp >= saleStart + saleDuration) || (totalTokenIn >= tokenInLimit);
The instances below point to the second+ access of a state variable within a function. With each of these individually, caching will replace a Gwarmaccess (100 gas) with a much cheaper stack read.
Less obvious optimizations flagged include having local storage variables of mappings within state variable mappings or mappings within state variable structs, having local storage variables of structs within mappings, or having local caches of state variable contracts/addresses.
providerReports[providerAddress][0].timestamp=1;
uint256 reportTimestampPast = providerReports[providerAddress][index_past].timestamp;
providerReports[provider][0].timestamp = 1;
stakingToken.safeTransfer(stakingProxy, increase);
rewardData[_rewardsToken].lastUpdateTime = uint40(block.timestamp);
uint256(rewardData[_rewardsToken].rewardPerTokenStored).add(
rewards[_account][_rewardsToken] = 0;
rewardData[_rewardsToken].rewardRate).mul(1e18).div(rewardData[_rewardsToken].useBoost ? boostedSupply : lockedSupply)
amount = balances[_user].boosted;
if (idx == 0 || userLocks[_account][idx - 1].unlockTime < unlockTime) {
uint256 boostRatio = boostRate.mul(_spendRatio).div(maximumBoostPayment==0?1:maximumBoostPayment);
maximumBoostPayment = nextMaximumBoostPayment;
boostRate = nextBoostRate;
uint256 min = MathUpgradeable.min(minimumStake, minimumStake - _offset);
uint256 max = maximumStake.add(_offset);
IStakingProxy(stakingProxy).withdraw(remove);
IERC20Upgradeable(address(citadelToken)).safeTransfer(address(cachedXCitadel), stakingAmount);
uint256 _newTotalWeight = totalFundingPoolWeight;
assetDecimalsNormalizationValue = 10**asset.decimals();
asset.safeTransfer(saleRecipient, amount);
minCitadelPriceInAsset,
minCitadelPriceInAsset,
maxCitadelPriceInAsset
maxCitadelPriceInAsset
tokenInNormalizationValue = 10**tokenIn.decimals();
block.timestamp < saleStart + saleDuration,
totalTokenIn = totalTokenIn + _tokenInAmount;
limitLeft_ = tokenInLimit - totalTokenIn;
limitLeft_ = tokenInLimit - totalTokenIn;
require(guestlist.authorized(msg.sender, _proof), "not authorized");
token.safeTransferFrom(msg.sender, address(this), _amount);
uint256 _after = token.balanceOf(address(this));
guestList.authorized(_recipient, _amount, proof),
IStrategy(strategy).balanceOf() == 0,
IStrategy(strategy).earn();
token.safeTransfer(vesting, _amount);
? (managementFee * (balance() - _harvestedAmount) * duration) /
return (_timestamp - globalStartTimestamp) / epochLength;
lastMintTimestamp > globalStartTimestamp,
unchecked {}
for subtractions where the operands cannot underflow because of a previous require()
require(a <= b); x = b - a
=> require(a <= b); unchecked { x = b - a }
uint256 newAllowance = oldAllowance - value;
Checking of equality to account
is cheaper than looking up the gac
role via static call, so that check should be on the left of the condition to shortcut the logic
gac.hasRole(role, msg.sender) || msg.sender == account,
.length
should not be looked up in every loop of a for
-loopEven memory arrays incur the overhead of bit tests and bit shifts to calculate the array length. Storage array length checks incur an extra Gwarmaccess (100 gas) PER-LOOP.
for (uint256 i = 0; i < providers.length; i++) {
for (uint256 i = 0; i < userRewards.length; i++) {
for (uint i = nextUnlockIndex; i < locks.length; i++) {
for (uint i; i < rewardTokens.length; i++) {
for (uint i = 0; i < rewardTokens.length; i++) {
for (uint256 i = 0; i < roles.length; i++) {
++i
/i++
should be unchecked{++i}
/unchecked{++i}
when it is not possible for them to overflow, as is the case when used in for
- and while
-loopsfor (uint256 i = 0; i < reportsCount; i++) {
for (uint256 i = 0; i < providers.length; i++) {
for (uint256 i = 0; i < userRewards.length; i++) {
for (uint i = nextUnlockIndex; i < locksLength; i++) {
for (uint i = locks.length - 1; i + 1 != 0; i--) {
for (uint i = locks.length - 1; i + 1 != 0; i--) {
for (uint i = epochindex - 1; i + 1 != 0; i--) {
for (uint i = _epoch; i + 1 != 0; i--) {
for (uint256 i = 0; i < 128; i++) {
for (uint i = nextUnlockIndex; i < locks.length; i++) {
for (uint i = nextUnlockIndex; i < length; i++) {
for (uint i; i < rewardTokens.length; i++) {
for (uint i = 0; i < rewardTokens.length; i++) {
for (uint256 i = 0; i < numPools; i++) {
for (uint256 i; i < length; ++i) {
for (uint256 i = 0; i < roles.length; i++) {
for (uint256 i = startingEpoch; i <= endingEpoch; i++) {
require()
/revert()
strings longer than 32 bytes cost extra gasrequire( _fundingBps + _stakingBps + _lockingBps == MAX_BPS, "CitadelMinter: Sum of propvalues must be 10000 bps" );
require( lastMintTimestamp == 0, "CitadelMinter: last mint timestamp already initialized" );
require( globalStartTimestamp != 0, "CitadelMinter: supply schedule start not initialized" );
require( fundingPools.remove(_pool), "CitadelMinter: funding pool does not exist for removal" );
require( fundingPools.add(_pool), "CitadelMinter: funding pool already exists" );
require( citadelPriceFlag == false, "Funding: citadel price from oracle flagged and pending review" );
require( _assetCap > funding.assetCumulativeFunded, "cannot decrease cap below global sum of assets in" );
require( _token != address(asset), "cannot sweep funding asset, use claimAssetToTreasury()" );
require( _saleRecipient != address(0), "Funding: sale recipient should not be zero" );
require( keccak256(bytes(roleString)) == role, "Role string and role do not match" );
require( _saleStart >= block.timestamp, "KnightingRound: start date may not be in the past" );
require( _saleDuration > 0, "KnightingRound: the sale duration must not be zero" );
require( _tokenOutPrice > 0, "KnightingRound: the price must not be zero" );
require( _saleRecipient != address(0), "KnightingRound: sale recipient should not be zero" );
require(!finalized, "KnightingRound: already finalized");
require( tokenOut.balanceOf(address(this)) >= totalTokenOutBought, "KnightingRound: not enough balance" );
require( _saleStart >= block.timestamp, "KnightingRound: start date may not be in the past" );
require(!finalized, "KnightingRound: already finalized");
require( _saleDuration > 0, "KnightingRound: the sale duration must not be zero" );
require(!finalized, "KnightingRound: already finalized");
require( _tokenOutPrice > 0, "KnightingRound: the price must not be zero" );
require( _saleRecipient != address(0), "KnightingRound: sale recipient should not be zero" );
require(!finalized, "KnightingRound: already finalized");
require( gac.hasRole(role, msg.sender) || msg.sender == account, "GAC: invalid-caller-role-or-address" );
require( (value == 0) || (token.allowance(address(this), spender) == 0), "SafeERC20: approve from non-zero to non-zero allowance" );
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
require( _feeConfig[0] <= PERFORMANCE_FEE_HARD_CAP, "performanceFeeGovernance too high" );
require( _feeConfig[1] <= PERFORMANCE_FEE_HARD_CAP, "performanceFeeStrategist too high" );
require( IStrategy(strategy).balanceOf() == 0, "Please withdrawToVault before changing strat" );
require( _fees <= PERFORMANCE_FEE_HARD_CAP, "performanceFeeStrategist too high" );
require( _performanceFeeStrategist <= maxPerformanceFee, "Excessive strategist performance fee" );
require( _performanceFeeGovernance <= maxPerformanceFee, "Excessive governance performance fee" );
require(msg.sender == vault, "StakedCitadelVester: only xCTDL vault");
require(_amount > 0, "StakedCitadelVester: cannot vest 0");
require( globalStartTimestamp > 0, "SupplySchedule: minting not started" );
require( cachedGlobalStartTimestamp > 0, "SupplySchedule: minting not started" );
require( block.timestamp > lastMintTimestamp, "SupplySchedule: already minted up to current block" );
require( globalStartTimestamp == 0, "SupplySchedule: minting already started" );
require( _globalStartTimestamp >= block.timestamp, "SupplySchedule: minting must start at or after current time" );
require( epochRate[_epoch] == 0, "SupplySchedule: rate already set for given epoch" );
require( globalStartTimestamp > 0, "SupplySchedule: minting not started" );
require( lastMintTimestamp > globalStartTimestamp, "SupplySchedule: attempting to mint before start block" );
require( block.timestamp > lastMintTimestamp, "SupplySchedule: already minted up to current block" );
return balances[_user].boosted;
return balances[_user].locked;
return locks[locksLength - 1].boosted;
return 0;
return locks[i].boosted;
return 0;
return mid;
return min;
return (userBalance.locked, unlockable, locked, lockData);
return (userBalance.locked, unlockable, locked, lockData);
return (userBalance.locked, unlockable, locked, lockData);
return (userBalance.locked, unlockable, locked, lockData);
uint256 balance = stakingToken.balanceOf(address(this));
bool
s for storage incurs overhead// Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
mapping(address => mapping(address => bool)) public rewardDistributors;
bool public isShutdown = false;
bool public citadelPriceFlag; /// Flag citadel price for review by guardian if it exceeds min and max bounds;
bool public transferFromDisabled; // Set to true in initialize
bool public finalized;
mapping(address => bool) public hasClaimed;
bool public pausedDeposit; // false by default Allows to only block deposits, use pause for the normal pause state
> 0
costs more gas than != 0
when used on a uint
in a require()
statementrequire(minimumProviders_ > 0);
require(minimumProviders_ > 0);
require(timestamps[0] > 0);
require(_amount > 0, "Cannot stake 0");
require(locked > 0, "no exp locks");
require(_reward > 0, "No reward");
require(length > 0, "CitadelMinter: no funding pools");
require(_assetAmountIn > 0, "_assetAmountIn must not be 0");
require(amount > 0, "nothing to sweep");
require(amount > 0, "nothing to claim");
require(_citadelPriceInAsset > 0, "citadel price must not be zero");
require(_citadelPriceInAsset > 0, "citadel price must not be zero");
require(b > 0, "BoringMath: division by zero");
require(b > 0, "BoringMath: division by zero");
require(b > 0, "BoringMath: division by zero");
require(b > 0, "BoringMath: division by zero");
require( _saleDuration > 0, "KnightingRound: the sale duration must not be zero" );
require( _tokenOutPrice > 0, "KnightingRound: the price must not be zero" );
require(_tokenInAmount > 0, "_tokenInAmount should be > 0");
require(tokenOutAmount_ > 0, "nothing to claim");
require( _saleDuration > 0, "KnightingRound: the sale duration must not be zero" );
require( _tokenOutPrice > 0, "KnightingRound: the price must not be zero" );
require(amount > 0, "nothing to sweep");
require(_amount > 0, "StakedCitadelVester: cannot vest 0");
require( globalStartTimestamp > 0, "SupplySchedule: minting not started" );
require( cachedGlobalStartTimestamp > 0, "SupplySchedule: minting not started" );
require( globalStartTimestamp > 0, "SupplySchedule: minting not started" );
uint256 size = 0;
for (uint256 i = 0; i < reportsCount; i++) {
for (uint256 i = 0; i < providers.length; i++) {
for (uint256 i = 0; i < userRewards.length; i++) {
uint256 min = 0;
for (uint256 i = 0; i < 128; i++) {
uint256 reward = 0;
for (uint i = 0; i < rewardTokens.length; i++) {
for (uint256 i = 0; i < numPools; i++) {
uint256 lockingAmount = 0;
uint256 stakingAmount = 0;
uint256 fundingAmount = 0;
for (uint256 i = 0; i < roles.length; i++) {
uint256 mintable = 0;
uint256 mintable = 0;
internal
functions only called once can be inlined to save gasfunction updateStakeRatio(uint256 _offset) internal {
function _notifyReward(address _rewardsToken, uint256 _reward) internal {
function _transferToFundingPools(uint256 _citadelAmount) internal {
function _removeFundingPool(address _pool) internal {
function _addFundingPool(address _pool) internal {
function _depositFor(address _recipient, uint256 _amount) internal nonReentrant
function _calculatePerformanceFee(uint256 _amount) internal view returns (uint256, uint256)
function _handleFees(uint256 _harvestedAmount, uint256 harvestTime)
function _setEpochRates() internal { epochRate[0] = 593962000000000000000000 / epochLength;
calldata
instead of memory
for read-only arguments in external
functions saves gasstring memory roleString,
function deposit(uint256 _amount, bytes32[] memory proof)
function depositAll(bytes32[] memory proof) external whenNotPaused {
bytes32[] memory proof
++i
costs less gas than ++i
, especially when it's used in for
-loops (--i
/i--
too)for (uint256 i = 0; i < reportsCount; i++) {
for (uint256 i = 0; i < providers.length; i++) {
for (uint256 i = 0; i < userRewards.length; i++) {
for (uint i = nextUnlockIndex; i < locksLength; i++) {
for (uint i = locks.length - 1; i + 1 != 0; i--) {
for (uint i = locks.length - 1; i + 1 != 0; i--) {
for (uint i = epochindex - 1; i + 1 != 0; i--) {
for (uint i = _epoch; i + 1 != 0; i--) {
for (uint256 i = 0; i < 128; i++) {
for (uint i = nextUnlockIndex; i < locks.length; i++) {
for (uint i = nextUnlockIndex; i < length; i++) {
for (uint i; i < rewardTokens.length; i++) {
for (uint i = 0; i < rewardTokens.length; i++) {
for (uint256 i = 0; i < numPools; i++) {
for (uint256 i = 0; i < roles.length; i++) {
for (uint256 i = startingEpoch; i <= endingEpoch; i++) {
uints
/ints
smaller than 32 bytes (256 bits) incurs overheadWhen using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html Use a larger size then downcast where needed
uint8 index_recent = timestamps[0] >= timestamps[1] ? 0 : 1;
uint8 index_past = 1 - index_recent;
uint8 index_recent = reports[0].timestamp >= reports[1].timestamp ? 0 : 1;
uint8 index_past = 1 - index_recent;
uint40 periodFinish;
uint208 rewardRate;
uint40 lastUpdateTime;
uint208 rewardPerTokenStored;
uint112 locked;
uint112 boosted;
uint32 nextUnlockIndex;
uint112 amount;
uint112 boosted;
uint32 unlockTime;
uint224 supply; //epoch boosted supply
uint32 date; //epoch start date
uint8 private _decimals;
function decimals() public view returns (uint8) {
uint112 lockAmount = _amount.sub(spendAmount).to112();
uint112 boostedAmount = _amount.add(_amount.mul(boostRatio).div(denominator)).to112();
uint112 locked;
uint112 boostedAmount;
uint32 nextUnlockIndex = userBalance.nextUnlockIndex;
uint8 indexed daoId,
uint8 _daoId,
keccak256()
, should use immutable
rather than constant
See this issue for a detail description of the issue
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
bytes32 public constant POLICY_OPERATIONS_ROLE = keccak256("POLICY_OPERATIONS_ROLE");
bytes32 public constant CITADEL_MINTER_ROLE = keccak256("CITADEL_MINTER_ROLE");
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
bytes32 public constant POLICY_OPERATIONS_ROLE = keccak256("POLICY_OPERATIONS_ROLE");
bytes32 public constant TREASURY_OPERATIONS_ROLE = keccak256("TREASURY_OPERATIONS_ROLE");
bytes32 public constant TREASURY_VAULT_ROLE = keccak256("TREASURY_VAULT_ROLE");
bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
bytes32 public constant TREASURY_GOVERNANCE_ROLE = keccak256("TREASURY_GOVERNANCE_ROLE");
bytes32 public constant TECH_OPERATIONS_ROLE = keccak256("TECH_OPERATIONS_ROLE");
bytes32 public constant POLICY_OPERATIONS_ROLE = keccak256("POLICY_OPERATIONS_ROLE");
bytes32 public constant TREASURY_OPERATIONS_ROLE = keccak256("TREASURY_OPERATIONS_ROLE");
bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE");
bytes32 public constant BLOCKLIST_MANAGER_ROLE = keccak256("BLOCKLIST_MANAGER_ROLE");
bytes32 public constant BLOCKLISTED_ROLE = keccak256("BLOCKLISTED_ROLE");
bytes32 public constant CITADEL_MINTER_ROLE = keccak256("CITADEL_MINTER_ROLE");
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
bytes32 public constant TREASURY_GOVERNANCE_ROLE = keccak256("TREASURY_GOVERNANCE_ROLE");
bytes32 public constant TECH_OPERATIONS_ROLE = keccak256("TECH_OPERATIONS_ROLE");
bytes32 public constant TREASURY_OPERATIONS_ROLE = keccak256("TREASURY_OPERATIONS_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE");
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
private
rather than public
for constants, saves gasIf needed, the value can be read from the verified contract source code
uint256 public constant rewardsDuration = 86400; // 1 day
uint256 public constant lockDuration = rewardsDuration * 7 * 21; // 21 weeks
uint256 public constant denominator = 10000;
uint256 public constant stakeOffsetOnLock = 500; //allow broader range for staking when depositing
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
bytes32 public constant POLICY_OPERATIONS_ROLE = keccak256("POLICY_OPERATIONS_ROLE");
bytes32 public constant CITADEL_MINTER_ROLE = keccak256("CITADEL_MINTER_ROLE");
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
bytes32 public constant POLICY_OPERATIONS_ROLE = keccak256("POLICY_OPERATIONS_ROLE");
bytes32 public constant TREASURY_OPERATIONS_ROLE = keccak256("TREASURY_OPERATIONS_ROLE");
bytes32 public constant TREASURY_VAULT_ROLE = keccak256("TREASURY_VAULT_ROLE");
bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");
uint256 public constant MAX_BPS = 10000;
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
bytes32 public constant TREASURY_GOVERNANCE_ROLE = keccak256("TREASURY_GOVERNANCE_ROLE");
bytes32 public constant TECH_OPERATIONS_ROLE = keccak256("TECH_OPERATIONS_ROLE");
bytes32 public constant POLICY_OPERATIONS_ROLE = keccak256("POLICY_OPERATIONS_ROLE");
bytes32 public constant TREASURY_OPERATIONS_ROLE = keccak256("TREASURY_OPERATIONS_ROLE");
bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE");
bytes32 public constant BLOCKLIST_MANAGER_ROLE = keccak256("BLOCKLIST_MANAGER_ROLE");
bytes32 public constant BLOCKLISTED_ROLE = keccak256("BLOCKLISTED_ROLE");
bytes32 public constant CITADEL_MINTER_ROLE = keccak256("CITADEL_MINTER_ROLE");
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
bytes32 public constant TREASURY_GOVERNANCE_ROLE = keccak256("TREASURY_GOVERNANCE_ROLE");
bytes32 public constant TECH_OPERATIONS_ROLE = keccak256("TECH_OPERATIONS_ROLE");
bytes32 public constant TREASURY_OPERATIONS_ROLE = keccak256("TREASURY_OPERATIONS_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE");
uint256 public constant MAX_BPS = 10_000;
uint256 public constant SECS_PER_YEAR = 31_556_952; // 365.2425 days
uint256 public constant WITHDRAWAL_FEE_HARD_CAP = 200; // Never higher than 2%
uint256 public constant PERFORMANCE_FEE_HARD_CAP = 3_000; // Never higher than 30% // 30% maximum performance fee // We usually do 20, so this is insanely high already
uint256 public constant MANAGEMENT_FEE_HARD_CAP = 200; // Never higher than 2%
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
uint256 public constant INITIAL_VESTING_DURATION = 86400 * 21; // 21 days of vesting
bytes32 public constant CONTRACT_GOVERNANCE_ROLE = keccak256("CONTRACT_GOVERNANCE_ROLE");
uint256 public constant epochLength = 21 days;
if ( == true)
=> if ()
, if ( == false)
=> if (!)
citadelPriceFlag == false,
require()
/revert()
checks should be refactored to a modifier or functionrequire(reportExpirationTimeSec_ <= MAX_REPORT_EXPIRATION_TIME);
require(minimumProviders_ > 0);
require(_citadelPriceInAsset > 0, "citadel price must not be zero");
require( _saleStart >= block.timestamp, "KnightingRound: start date may not be in the past" );
require( _saleDuration > 0, "KnightingRound: the sale duration must not be zero" );
require( _tokenOutPrice > 0, "KnightingRound: the price must not be zero" );
require( _saleRecipient != address(0), "KnightingRound: sale recipient should not be zero" );
require(!finalized, "KnightingRound: already finalized");
require(address(token) != _token, "No want");
require(!pausedDeposit, "pausedDeposit"); // dev: deposits are paused
require( globalStartTimestamp > 0, "SupplySchedule: minting not started" );
require( block.timestamp > lastMintTimestamp, "SupplySchedule: already minted up to current block" );
* 2
is equivalent to << 1
and / 2
is the same as >> 1
uint256 mid = (min + max + 1) / 2;
If the variable is only accessed once, it's cheaper to use the state variable directly that one time
uint256 epochindex = epochs.length;
require()
or revert()
statements that check input arguments should be at the top of the functionChecks that involve constants should come before checks that involve state variables
require(_reward > 0, "No reward");
require( _token != address(asset), "cannot sweep funding asset, use claimAssetToTreasury()" );
require( keccak256(bytes(roleString)) == role, "Role string and role do not match" );
require(_tokenInAmount > 0, "_tokenInAmount should be > 0");
require(address(token) != _token, "No want");
require(_treasury != address(0), "Address 0");
require(_strategy != address(0), "Address 0");
require(_fees <= WITHDRAWAL_FEE_HARD_CAP, "withdrawalFee too high");
require( _fees <= PERFORMANCE_FEE_HARD_CAP, "performanceFeeStrategist too high" );
require(_fees <= MANAGEMENT_FEE_HARD_CAP, "managementFee too high");
require(_guardian != address(0), "Address cannot be 0x0");
require(_vesting != address(0), "Address cannot be 0x0");
require(_newToEarnBps <= MAX_BPS, "toEarnBps should be <= MAX_BPS");
require(address(token) != _token, "No want");
require(_amount > 0, "StakedCitadelVester: cannot vest 0");
require( block.timestamp > lastMintTimestamp, "SupplySchedule: already minted up to current block" );
require( _globalStartTimestamp >= block.timestamp, "SupplySchedule: minting must start at or after current time" );
require( block.timestamp > lastMintTimestamp, "SupplySchedule: already minted up to current block" );
block.timestamp
and block.number
are added to event information by default so adding them manually wastes gas
event ProviderReportPushed(address indexed provider, uint256 payload, uint256 timestamp);
uint256 indexed blockNumber,
uint256 timestamp
uint256 indexed blockNumber,
uint256 timestamp
revert()
/require()
strings to save deployment gaspayable
If a function modifier such as onlyOwner
is used, the function will revert if a normal user tries to pay the function. Marking the function as payable
will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided.
function setReportExpirationTimeSec(uint256 reportExpirationTimeSec_) external onlyOwner
function setReportDelaySec(uint256 reportDelaySec_) external onlyOwner
function setMinimumProviders(uint256 minimumProviders_) external onlyOwner
function addProvider(address provider) external onlyOwner
function removeProvider(address provider) external onlyOwner
function addReward( address _rewardsToken, address _distributor, bool _useBoost ) public onlyOwner {
function approveRewardDistributor( address _rewardsToken, address _distributor, bool _approved ) external onlyOwner {
function setStakingContract(address _staking) external onlyOwner {
function setStakeLimits(uint256 _minimum, uint256 _maximum) external onlyOwner {
function setBoost(uint256 _max, uint256 _rate, address _receivingAddress) external onlyOwner {
function setKickIncentive(uint256 _rate, uint256 _delay) external onlyOwner {
function shutdown() external onlyOwner {
function recoverERC20(address _tokenAddress, uint256 _tokenAmount) external onlyOwner {
function mintAndDistribute() external onlyRole(POLICY_OPERATIONS_ROLE) gacPausable nonReentrant
function setFundingPoolWeight(address _pool, uint256 _weight) external onlyRole(POLICY_OPERATIONS_ROLE) gacPausable nonReentrant
function setCitadelDistributionSplit( uint256 _fundingBps, uint256 _stakingBps, uint256 _lockingBps ) external onlyRole(POLICY_OPERATIONS_ROLE) gacPausable nonReentrant {
function initializeLastMintTimestamp() external onlyRole(CONTRACT_GOVERNANCE_ROLE) gacPausable
function mint(address dest, uint256 amount) external onlyRole(CITADEL_MINTER_ROLE) gacPausable
function deposit(uint256 _assetAmountIn, uint256 _minCitadelOut) external onlyWhenPriceNotFlagged gacPausable nonReentrant returns (uint256 citadelAmount_)
function setDiscount(uint256 _discount) external gacPausable onlyRoleOrAddress(POLICY_OPERATIONS_ROLE, funding.discountManager)
function clearCitadelPriceFlag() external gacPausable onlyRole(POLICY_OPERATIONS_ROLE)
function setAssetCap(uint256 _assetCap) external gacPausable onlyRole(POLICY_OPERATIONS_ROLE)
function sweep(address _token) external gacPausable nonReentrant onlyRole(TREASURY_OPERATIONS_ROLE)
function claimAssetToTreasury() external gacPausable onlyRole(TREASURY_OPERATIONS_ROLE)
function setDiscountLimits(uint256 _minDiscount, uint256 _maxDiscount) external gacPausable onlyRole(CONTRACT_GOVERNANCE_ROLE)
function setDiscountManager(address _discountManager) external gacPausable onlyRole(CONTRACT_GOVERNANCE_ROLE)
function setSaleRecipient(address _saleRecipient) external gacPausable onlyRole(CONTRACT_GOVERNANCE_ROLE)
function setCitadelAssetPriceBounds(uint256 _minPrice, uint256 _maxPrice) external gacPausable onlyRole(CONTRACT_GOVERNANCE_ROLE)
function updateCitadelPriceInAsset() external gacPausable onlyRole(KEEPER_ROLE)
function updateCitadelPriceInAsset(uint256 _citadelPriceInAsset) external gacPausable onlyCitadelPriceInAssetOracle
function finalize() external onlyRole(CONTRACT_GOVERNANCE_ROLE) {
function setSaleStart(uint256 _saleStart) external onlyRole(CONTRACT_GOVERNANCE_ROLE)
function setSaleDuration(uint256 _saleDuration) external onlyRole(CONTRACT_GOVERNANCE_ROLE)
function setTokenOutPrice(uint256 _tokenOutPrice) external onlyRole(CONTRACT_GOVERNANCE_ROLE)
function setSaleRecipient(address _saleRecipient) external onlyRole(CONTRACT_GOVERNANCE_ROLE)
function setGuestlist(address _guestlist) external onlyRole(TECH_OPERATIONS_ROLE)
function setTokenInLimit(uint256 _tokenInLimit) external onlyRole(TECH_OPERATIONS_ROLE)
function sweep(address _token) external gacPausable nonReentrant onlyRole(TREASURY_OPERATIONS_ROLE) {
function __GlobalAccessControlManaged_init(address _globalAccessControl) public onlyInitializing
function setVestingDuration(uint256 _duration) external onlyRole(CONTRACT_GOVERNANCE_ROLE)
function setMintingStart(uint256 _globalStartTimestamp) external onlyRole(CONTRACT_GOVERNANCE_ROLE) gacPausable
function setEpochRate(uint256 _epoch, uint256 _rate) external onlyRole(CONTRACT_GOVERNANCE_ROLE) gacPausable
#0 - liveactionllama
2022-04-20T22:21:55Z
Warden created this issue as a placeholder, because their submission was too large for the contest form. They then emailed their md file to our team. I've updated this issue with their md file content.
#1 - liveactionllama
2022-05-06T22:35:26Z
Note: my original copy/paste did not capture all of the warden's content. I've updated this issue to now contain the entirety of the original submission. I've also notified the sponsor. Contest has not yet moved to judging.