Platform: Code4rena
Start Date: 16/02/2023
Pot Size: $144,750 USDC
Total HM: 17
Participants: 154
Period: 19 days
Judge: Trust
Total Solo HM: 5
Id: 216
League: ETH
Rank: 151/154
Findings: 1
Award: $42.07
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: c3phas
Also found by: 0x3b, 0x6980, 0x73696d616f, 0xSmartContract, 0xackermann, 0xhacksmithh, 0xsomeone, Bnke0x0, Bough, Budaghyan, Darshan, DeFiHackLabs, Deivitto, GalloDaSballo, JCN, LethL, Madalad, MiniGlome, Morraez, P-384, PaludoX0, Phantasmagoria, Praise, RHaO-sec, Rageur, RaymondFam, ReyAdmirado, Rickard, Rolezn, SaeedAlipoor01988, Saintcode_, Sathish9098, TheSavageTeddy, Tomio, Viktor_Cortess, abiih, arialblack14, atharvasama, banky, codeislight, cryptonue, ddimitrov22, dec3ntraliz3d, descharre, dharma09, emmac002, favelanky, hl_, hunter_w3b, kaden, kodyvim, matrix_0wl, oyc_109, pavankv, scokaf, seeu, yamapyblack
42.0697 USDC - $42.07
ID | Issue | Contexts | Instances | Total Gas Saved |
---|---|---|---|---|
G-01 | Using bools for storage incurs overhead | 4 | 9 | 153900 |
G-02 | Array length not cached outside of loop | 4 | 8 | 24 |
G-03 | Variables initialized with default value | 7 | 22 | - |
G-04 | Unsigned integer comparison with > 0 | 6 | 26 | - |
G-05 | Long revert/require strings | 3 | 7 | - |
G-06 | Postfix increment/decrease used | 7 | 17 | - |
G-07 | Mark payable functions guaranteed to revert when called by normal users | 2 | 7 | 147 |
G-08 | Use assembly to check for address(0) | 5 | 15 | 90 |
G-09 | Use "require" instead of "assert" when possible | 4 | 20 | - |
G-10 | Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead | 2 | 2 | - |
G-11 | Split require() statements that use && to save gas | 1 | 1 | - |
G-12 | x += y costs more gas than x = x + y for state variables | 3 | 22 | 2486 |
G-13 | When possible, use non-strict comparison >= and/or =< instead of > < | 9 | 64 | 960 |
G-14 | If possible, use private rather than public for constants | 10 | 30 | 102180 |
G-15 | Use a more recent version of Solidity to save gas | 12 | 12 | - |
Total issues | Total contexts | Total instances | Total minimum gas saved |
---|---|---|---|
15 | 80 | 262 | 259787 |
Use uint256 for true/false to avoid a Gwarmaccess (100 gas), and to avoid Gsset (20000 gas) when changing from โfalseโ to โtrueโ, after having been โtrueโ in the past.
::54 => bool isCollIncrease; ::182 => bool isRecoveryMode = _checkRecoveryMode(_collateral, vars.price, vars.collCCR, vars.collDecimals); ::290 => bool isRecoveryMode = _checkRecoveryMode(_collateral, vars.price, vars.collCCR, vars.collDecimals);
::28 => bool allowed;
::62 => mapping (address => bool) public troveManagers; ::63 => mapping (address => bool) public stabilityPools; ::64 => mapping (address => bool) public borrowerOperations;
::135 => bool recoveryModeAtStart; ::152 => bool backToNormalMode;
Caching the length eliminates the additional DUP<N>
required to store the stack offset and converts each of them to a DUP<N>
(3 gas).
::56 => for(uint256 i = 0; i < _collaterals.length; i++) {
::206 => for (uint i = 0; i < assets.length; i++) { ::228 => for (uint i = 0; i < collaterals.length; i++) {
::640 => for (uint i = 0; i < assets.length; i++) { ::810 => for (uint i = 0; i < collaterals.length; i++) { ::831 => for (uint i = 0; i < collaterals.length; i++) {
::808 => for (vars.i = 0; vars.i < _troveArray.length; vars.i++) { ::882 => for (vars.i = 0; vars.i < _troveArray.length; vars.i++) {
Explicitly initialize a variable with its default value wastes gas
::108 => for(uint256 i = 0; i < numCollaterals; i++) {
::56 => for(uint256 i = 0; i < _collaterals.length; i++) {
::206 => for (uint i = 0; i < assets.length; i++) { ::228 => for (uint i = 0; i < collaterals.length; i++) { ::240 => for (uint i = 0; i < numCollaterals; i++) {
::351 => for (uint i = 0; i < numCollaterals; i++) { ::397 => for (uint i = 0; i < numCollaterals; i++) { ::640 => for (uint i = 0; i < assets.length; i++) { ::810 => for (uint i = 0; i < collaterals.length; i++) { ::831 => for (uint i = 0; i < collaterals.length; i++) { ::859 => for (uint i = 0; i < numCollaterals; i++) {
::117 => for (uint256 i = 0; i < numSteps; i = i.uncheckedInc()) { ::165 => for (uint256 i = 0; i < numSteps; i = i.uncheckedInc()) {
::128 => for (uint256 i = 0; i < numStrategists; i = i.uncheckedInc()) { ::264 => for (uint256 i = 0; i < queueLength; i = i.uncheckedInc()) { ::369 => uint256 totalLoss = 0; ::371 => uint256 vaultBalance = 0; ::372 => for (uint256 i = 0; i < queueLength; i = i.uncheckedInc()) {
::77 => for (uint256 i = 0; i < numStrategists; i = i.uncheckedInc()) { ::100 => uint256 amountFreed = 0; ::112 => uint256 debt = 0; ::117 => uint256 repayment = 0;
Comparisons done using != 0
are cheaper than > 0
when dealing with unsigned integer types
::278 => if (vars.netAssetMovement > 0) {
::128 => assert(MIN_NET_DEBT > 0); ::197 => assert(vars.compositeDebt > 0); ::301 => assert(msg.sender == _borrower || (msg.sender == stabilityPoolAddress && _collTopUp > 0 && _LUSDChange == 0)); ::337 => if (!_isDebtIncrease && _LUSDChange > 0) { ::552 => require(_LUSDChange > 0, "BorrowerOps: Debt increase requires non-zero debtChange");
::152 => if (_LQTYamount > 0) { ::181 => if (totalLQTYStaked > 0) {collFeePerLQTYStaked = _collFee.mul(DECIMAL_PRECISION).div(totalLQTYStaked);} ::191 => if (totalLQTYStaked > 0) {LUSDFeePerLQTYStaked = _LUSDFee.mul(DECIMAL_PRECISION).div(totalLQTYStaked);} ::266 => require(currentStake > 0, 'LQTYStaking: User must have a non-zero stake'); ::270 => require(_amount > 0, 'LQTYStaking: Amount must be non-zero');
::591 => assert(newP > 0); ::870 => require(_initialDeposit > 0, 'StabilityPool: User must have a non-zero deposit'); ::874 => require(_amount > 0, 'StabilityPool: Amount must be non-zero');
::424 => if (singleLiquidation.collSurplus > 0) { ::452 => if (_LUSDInStabPool > 0) { ::551 => require(totals.totalDebtInSequence > 0); ::563 => if (totals.totalCollSurplus > 0) { ::754 => require(totals.totalDebtInSequence > 0); ::766 => if (totals.totalCollSurplus > 0) { ::916 => if (_LUSD > 0) { ::920 => if (_collAmount > 0) { ::1224 => assert(totalStakesSnapshot[_collateral] > 0); ::1414 => assert(newBaseRate > 0); // Base rate is always non-zero after redemption
::502 => } else if (_roi > 0) { ::518 => } else if (vars.available > 0) {
Strings in require()
/ revert()
longer than 32 bytes cost extra gas
::107 => require(numCollaterals == _erc4626vaults.length, "Vaults array length must match number of collaterals"); ::133 => require(_driftBps <= 500, "Exceeds max allowed value of 500 BPS");
::150 => require(!emergencyShutdown, "Cannot add strategy during emergency shutdown"); ::321 => require(!emergencyShutdown, "Cannot deposit during emergency shutdown"); ::436 => require(loss <= allocation, "Strategy loss cannot be greater than allocation");
::98 => require(_amount <= balanceOf(), "Ammount must be less than balance"); ::193 => "Upgrade cooldown not initiated or still ongoing"
The prefix increment / decrease expression returns the updated value after it's incremented while the postfix increment / decrease expression returns the original value.
Be careful when employing this optimization anytime the return value of the expression is utilized later; for instance, uint a = i++
and uint a = ++i
produce different values for a
.
::108 => for(uint256 i = 0; i < numCollaterals; i++) {
::56 => for(uint256 i = 0; i < _collaterals.length; i++) {
::206 => for (uint i = 0; i < assets.length; i++) { ::228 => for (uint i = 0; i < collaterals.length; i++) { ::240 => for (uint i = 0; i < numCollaterals; i++) {
::286 => _nonces[owner]++, deadline))));
::351 => for (uint i = 0; i < numCollaterals; i++) { ::397 => for (uint i = 0; i < numCollaterals; i++) { ::640 => for (uint i = 0; i < assets.length; i++) { ::810 => for (uint i = 0; i < collaterals.length; i++) { ::831 => for (uint i = 0; i < collaterals.length; i++) { ::859 => for (uint i = 0; i < numCollaterals; i++) {
::608 => for (vars.i = 0; vars.i < _n && vars.user != firstUser; vars.i++) { ::690 => for (vars.i = 0; vars.i < _n; vars.i++) { ::808 => for (vars.i = 0; vars.i < _troveArray.length; vars.i++) { ::882 => for (vars.i = 0; vars.i < _troveArray.length; vars.i++) {
::273 => if (x % y != 0) q++;
If a function modifier, like onlyOwner, is applied, the function will revert if a regular user attempts to pay it. Making the function payable will save valid callers money on gas since the compiler won't do checks to see if a payment was made.
The extra opcodes avoided are CALLVALUE(2)
, DUP1(3)
, ISZERO(3)
, PUSH2(3)
, JUMPI(10)
, PUSH1(3)
, DUP1(3)
, REVERT(0)
, JUMPDEST(1)
, POP(2)
which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
::125 => function setYieldingPercentage(address _collateral, uint256 _bps) external onlyOwner { ::132 => function setYieldingPercentageDrift(uint256 _driftBps) external onlyOwner { ::138 => function setYieldClaimThreshold(address _collateral, uint256 _threshold) external onlyOwner { ::144 => function setYieldDistributionParams(uint256 _treasurySplit, uint256 _SPSplit, uint256 _stakingSplit) external onlyOwner { ::214 => function manualRebalance(address _collateral, uint256 _simulatedAmountLeavingPool) external onlyOwner {
::101 => function fund(uint amount) external onlyOwner { ::120 => function updateDistributionPeriod(uint256 _newDistributionPeriod) external onlyOwner {
By checking for address(0)
using assembly language, you can avoid the use of more gas-expensive operations such as calling a smart contract or reading from storage. This can save 6 gas per instance.
::93 => require(_treasuryAddress != address(0), "Treasury cannot be 0 address");
::312 => assert(sender != address(0)); ::313 => assert(recipient != address(0)); ::321 => assert(account != address(0)); ::325 => emit Transfer(address(0), account, amount); ::329 => assert(account != address(0)); ::333 => emit Transfer(account, address(0), amount); ::337 => assert(owner != address(0)); ::338 => assert(spender != address(0)); ::348 => _recipient != address(0) &&
::294 => owner = address(0);
::167 => require(step[0] != address(0)); ::168 => require(step[1] != address(0));
::151 => require(_strategy != address(0), "Invalid strategy address"); ::629 => require(newTreasury != address(0), "Invalid address");
If assert()
returns a false assertion, it compiles to the invalid opcode 0xfe
, which eats up all the gas left and completely undoes the changes. require()
compiles to 0xfd
, which is the opcode for a REVERT
, indicating that it will return the remaining gas if it returns a false assertion.
::128 => assert(MIN_NET_DEBT > 0); ::197 => assert(vars.compositeDebt > 0); ::301 => assert(msg.sender == _borrower || (msg.sender == stabilityPoolAddress && _collTopUp > 0 && _LUSDChange == 0)); ::331 => assert(_collWithdrawal <= vars.coll);
::312 => assert(sender != address(0)); ::313 => assert(recipient != address(0)); ::321 => assert(account != address(0)); ::329 => assert(account != address(0)); ::337 => assert(owner != address(0)); ::338 => assert(spender != address(0));
::526 => assert(_debtToOffset <= _totalLUSDDeposits); ::551 => assert(_LUSDLossPerUnitStaked <= DECIMAL_PRECISION); ::591 => assert(newP > 0);
::417 => assert(_LUSDInStabPool != 0); ::1224 => assert(totalStakesSnapshot[_collateral] > 0); ::1279 => assert(closedStatus != Status.nonExistent && closedStatus != Status.active); ::1342 => assert(troveStatus != Status.nonExistent && troveStatus != Status.active); ::1348 => assert(index <= idxLast); ::1414 => assert(newBaseRate > 0); // Base rate is always non-zero after redemption ::1489 => assert(decayedBaseRate <= DECIMAL_PRECISION); // The baseRate can decay to 0
Gas consumption can be greater if you use items that are less than 32 bytes in size. This is such that the EVM can only handle 32 bytes at once. In order to increase the element's size from 32 bytes to the necessary amount, the EVM must do extra operations if it is lower than that. When necessary, it is advised to utilize a bigger size and then downcast.
::35 => uint8 constant internal _DECIMALS = 18;
::35 => uint16 private constant LENDER_REFERRAL_CODE_NONE = 0;
It is advised to use two require
instead of using one require
with the operator &&
to save gas.
::653 => require(_maxFeePercentage >= BORROWING_FEE_FLOOR && _maxFeePercentage <= DECIMAL_PRECISION,
Gas can be saved by substituting the addition operator with plus-equals, same for minus. Saves 113 gas per instance.
::133 => toFree += profit; ::141 => roi -= int256(loss);
::168 => totalAllocBPS += _allocBPS; ::194 => totalAllocBPS -= strategies[_strategy].allocBPS; ::196 => totalAllocBPS += _allocBPS; ::214 => totalAllocBPS -= strategies[_strategy].allocBPS; ::390 => value -= loss; ::391 => totalLoss += loss; ::395 => strategies[stratAddr].allocated -= actualWithdrawn; ::396 => totalAllocated -= actualWithdrawn; ::444 => stratParams.allocBPS -= bpsChange; ::445 => totalAllocBPS -= bpsChange; ::450 => stratParams.losses += loss; ::451 => stratParams.allocated -= loss; ::452 => totalAllocated -= loss; ::505 => strategy.gains += vars.gain; ::514 => strategy.allocated -= vars.debtPayment; ::515 => totalAllocated -= vars.debtPayment; ::516 => vars.debt -= vars.debtPayment; // tracked for return value ::520 => strategy.allocated += vars.credit; ::521 => totalAllocated += vars.credit;
::128 => repayment -= uint256(-roi);
Non-strict inequalities are cheaper than strict ones due to some supplementary checks (ISZERO
, 3 gas). It will save 15โ20 gas.
::252 => if (vars.profit < yieldClaimThreshold[_collateral]) { ::264 => if (vars.percentOfFinalBal > vars.yieldingPercentage && vars.percentOfFinalBal.sub(vars.yieldingPercentage) > yieldingPercentageDrift) { ::269 => } else if(vars.percentOfFinalBal < vars.yieldingPercentage && vars.yieldingPercentage.sub(vars.percentOfFinalBal) > yieldingPercentageDrift) { ::278 => if (vars.netAssetMovement > 0) { ::281 => } else if (vars.netAssetMovement < 0) {
::128 => assert(MIN_NET_DEBT > 0); ::197 => assert(vars.compositeDebt > 0); ::337 => if (!_isDebtIncrease && _LUSDChange > 0) { ::552 => require(_LUSDChange > 0, "BorrowerOps: Debt increase requires non-zero debtChange");
::86 => if (lastIssuanceTimestamp < lastDistributionTime) { ::106 => if (lastIssuanceTimestamp < lastDistributionTime) {
::152 => if (_LQTYamount > 0) { ::266 => require(currentStake > 0, 'LQTYStaking: User must have a non-zero stake'); ::270 => require(_amount > 0, 'LQTYStaking: Amount must be non-zero');
::279 => if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
::583 => } else if (currentP.mul(newProductFactor).div(DECIMAL_PRECISION) < SCALE_FACTOR) { ::591 => assert(newP > 0); ::742 => if (epochSnapshot < currentEpoch) { return 0; } ::768 => if (compoundedDeposit < initialDeposit.div(1e9)) {return 0;} ::870 => require(_initialDeposit > 0, 'StabilityPool: User must have a non-zero deposit'); ::874 => require(_amount > 0, 'StabilityPool: Amount must be non-zero');
::397 => } else if ((_ICR > _100pct) && (_ICR < _MCR)) { ::424 => if (singleLiquidation.collSurplus > 0) { ::452 => if (_LUSDInStabPool > 0) { ::493 => if (_collDecimals < LiquityMath.CR_CALCULATION_DECIMALS) { ::495 => } else if (_collDecimals > LiquityMath.CR_CALCULATION_DECIMALS) { ::551 => require(totals.totalDebtInSequence > 0); ::563 => if (totals.totalCollSurplus > 0) { ::651 => else if (vars.backToNormalMode && vars.ICR < vars.collMCR) { ::694 => if (vars.ICR < _MCR) { ::754 => require(totals.totalDebtInSequence > 0); ::766 => if (totals.totalCollSurplus > 0) { ::853 => else if (vars.backToNormalMode && vars.ICR < vars.collMCR) { ::886 => if (vars.ICR < _MCR) { ::916 => if (_LUSD > 0) { ::920 => if (_collAmount > 0) { ::1160 => return (rewardSnapshots[_borrower][_collateral].collAmount < L_Collateral[_collateral]); ::1224 => assert(totalStakesSnapshot[_collateral] > 0); ::1386 => return TCR < _CCR; ::1414 => assert(newBaseRate > 0); // Base rate is always non-zero after redemption ::1450 => require(redemptionFee < _collateralDrawn); ::1539 => require (TroveOwnersArrayLength > 1 && sortedTroves.getSize(_collateral) > 1);
::80 => if (wantBalance > _debt) { ::92 => if (wantBal < _amountNeeded) { ::99 => if (_amountNeeded > liquidatedAmount) { ::131 => if (totalAssets > allocated) { ::135 => } else if (totalAssets < allocated) {
::234 => if (stratCurrentAllocation > stratMaxAllocation) { ::236 => } else if (stratCurrentAllocation < stratMaxAllocation) { ::368 => if (value > token.balanceOf(address(this))) { ::400 => if (value > vaultBalance) { ::420 => if (lockedFundsRatio < DEGRADATION_COEFFICIENT) { ::499 => if (_roi < 0) { ::502 => } else if (_roi > 0) { ::509 => if (vars.available < 0) { ::518 => } else if (vars.available > 0) { ::525 => if (vars.credit > vars.freeWantInStrat) { ::527 => } else if (vars.credit < vars.freeWantInStrat) { ::534 => if (vars.lockedProfitBeforeLoss > vars.loss) {
::113 => if (availableCapital < 0) { ::120 => if (amountFreed < debt) { ::122 => } else if (amountFreed > debt) { ::127 => if (roi < 0) { ::192 => upgradeProposalTime + UPGRADE_TIMELOCK < block.timestamp,
If necessary, the values can be obtained from the verified contract source code; alternatively, if there are many values, a single getter function that returns a tuple containing the values of all currently-public constants can be used. An example: FraxlendPair.sol#L156-L178.
The compiler doesn't have to write non-payable getter functions for deployment calldata, store the value's bytes somewhere other than where it will be used, or add another entry to the method ID table, which saves 3406-3606 gas in deployment gas.
::30 => string constant public NAME = "ActivePool";
::21 => string constant public NAME = "BorrowerOperations";
::21 => uint256 constant public MIN_ALLOWED_MCR = 1.1 ether; // 110% ::25 => uint256 constant public MIN_ALLOWED_CCR = 1.5 ether; // 150%
::19 => string constant public NAME = "CommunityIssuance";
::23 => string constant public NAME = "LQTYStaking";
::150 => string constant public NAME = "StabilityPool"; ::197 => uint public constant SCALE_FACTOR = 1e9;
::48 => uint constant public SECONDS_IN_ONE_MINUTE = 60; ::53 => uint constant public MINUTE_DECAY_FACTOR = 999037758833783000; ::54 => uint constant public override REDEMPTION_FEE_FLOOR = DECIMAL_PRECISION / 1000 * 5; // 0.5% ::55 => uint constant public MAX_BORROWING_FEE = DECIMAL_PRECISION / 100 * 5; // 5% ::61 => uint constant public BETA = 2;
::24 => address public constant VELO_ROUTER = 0xa132DAB612dB5cB9fC9Ac426A0Cc215A3423F9c9; ::25 => ILendingPoolAddressesProvider public constant ADDRESSES_PROVIDER = ::27 => IAaveProtocolDataProvider public constant DATA_PROVIDER = ::29 => IRewardsController public constant REWARDER = IRewardsController(0x6A0406B8103Ec68EE9A713A073C7bD587c5e04aD);
::40 => uint256 public constant DEGRADATION_COEFFICIENT = 10**18; // The unit for calculating profit degradation. ::41 => uint256 public constant PERCENT_DIVISOR = 10000; ::73 => bytes32 public constant DEPOSITOR = keccak256("DEPOSITOR"); ::74 => bytes32 public constant STRATEGIST = keccak256("STRATEGIST"); ::75 => bytes32 public constant GUARDIAN = keccak256("GUARDIAN"); ::76 => bytes32 public constant ADMIN = keccak256("ADMIN");
::23 => uint256 public constant PERCENT_DIVISOR = 10_000; ::24 => uint256 public constant UPGRADE_TIMELOCK = 48 hours; // minimum 48 hours for RF ::25 => uint256 public constant FUTURE_NEXT_PROPOSAL_TIME = 365 days * 100; ::49 => bytes32 public constant KEEPER = keccak256("KEEPER"); ::50 => bytes32 public constant STRATEGIST = keccak256("STRATEGIST"); ::51 => bytes32 public constant GUARDIAN = keccak256("GUARDIAN"); ::52 => bytes32 public constant ADMIN = keccak256("ADMIN");
Use a version of Solidity at least 0.8.10
to:
0.8.10
);revert()
/require()
strings (from v 0.8.4
);0.8.3
);0.8.2
).pragma solidity 0.6.11;
pragma solidity 0.6.11;
pragma solidity 0.6.11;
pragma solidity 0.6.11;
pragma solidity 0.6.11;
pragma solidity 0.6.11;
pragma solidity 0.6.11;
pragma solidity 0.6.11;
pragma solidity ^0.8.0;
pragma solidity ^0.8.0;
pragma solidity ^0.8.0;
pragma solidity ^0.8.0;
#0 - c4-judge
2023-03-09T17:42:00Z
trust1995 marked the issue as grade-b