Platform: Code4rena
Start Date: 06/01/2023
Pot Size: $210,500 USDC
Total HM: 27
Participants: 73
Period: 14 days
Judge: 0xean
Total Solo HM: 18
Id: 203
League: ETH
Rank: 23/73
Findings: 2
Award: $1,077.08
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: CodingNameKiki
Also found by: 0xA5DF, 0xAgro, 0xNazgul, 0xSmartContract, Aymen0909, BRONZEDISC, Bnke0x0, Breeje, Cyfrin, GalloDaSballo, HollaDieWaldfee, IceBear, IllIllI, MyFDsYours, RaymondFam, Ruhum, SaharDevep, Sathish9098, Soosh, Udsen, __141345__, brgltd, btk, carlitox477, chaduke, chrisdior4, cryptonue, delfin454000, descharre, hihen, joestakey, ladboy233, lukris02, luxartvinsec, peanuts, pedr02b2, rotcivegaf, shark, tnevler, yongskiws
1004.6363 USDC - $1,004.64
- * - usually (but not always) contain sizeable state that require a proxy + * - usually (but not always) contain sizable state that requires a proxy
- * Typically freezing thaws on its own in a predetemined number of blocks. + * Typically freezing thaws on its own in a predetermined number of blocks.
- // lastPayoutBal' = rToken.balanceOf'(this) (balance now == at end of pay leriod) + // lastPayoutBal' = rToken.balanceOf'(this) (balance now == at end of pay period)
- /// @return The status of this collateral asset. (Is it defaulting? Might it soon?) + /// @return The status of this collateral asset. (Is it defaulting? Might it be soon?)
For cleaner Solidity code in conjunction with the rule of modularity and modular programming, use named imports with curly braces instead of adopting the global import approach.
For instance, the import instances below could be refactored as follows:
- import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + import {UUPSUpgradeable.sol} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; [ ... ]
Some contracts have interface(s) or library showing up in its/their entirety at the top/bottom of the contract facilitating an ease of references on the same file page. This has, in certain instances, made the already large contract size to house an excessive code base. Additionally, it might create difficulty locating them when attempting to cross reference the specific interfaces embedded elsewhere but not saved into a particular .sol file.
Consider saving the interfaces and libraries entailed respectively, and having them imported like all other files.
Here are some of the instances entailed:
File: IAsset.sol#L75 File: IMain.sol File: BasketHandler.sol#L61-L104 File: ATokenFiatCollateral.sol#L10-L27
Some interfaces in the code bases are named without the prefix I
that could cause confusion to developers and readers referencing or interacting with the protocol. Consider conforming to Solidity's naming conventions by having the specific instance below refactored as follows:
- interface TestIMain is IMain { + interface ITestIMain is IMain {
Contracts, interfaces and libraries with their names differing with the filenames could sometimes create confusion and difficulty in cross referencing.
For instance, the abstract contract below has been named ComponetP1
where as the file has been saved as Component.sol
. Consider sticking to the same name where possible:
abstract contract ComponentP1 is
Function names should also be conventionally camel cased where possible. If snake case is preferred/adopted, consider lower casing them.
Here is one of the numerous instances entailed:
File: ATokenFiatCollateral.sol#L26
function REWARD_TOKEN() external view returns (IERC20);
require()
over assert()
As documented by Solidity Documentations:
"Assert should only be used to test for internal errors, and to check invariants. Properly functioning code should never create a Panic, not even on invalid external input. If this happens, then there is a bug in your contract which you should fix. Language analysis tools can evaluate your contract to identify the conditions and function calls which will cause a Panic."
The protocol uses assert()
in multiple places of the code bases. Despite the strip of gas saving benefits for Solidity version ^0.8.0, require()
should still be used for checking error conditions on inputs and return values.
Here are some of the instances entailed:
File: RecollateralizationLib.sol#L110
assert(doTrade); // @ audit Associated with the return value, `doTrade`
// @ audit Associated with the input parameter, `trade` 44: assert(trade.buyPrice > 0 && trade.buyPrice < FIX_MAX && trade.sellPrice < FIX_MAX); // @ audit Associated with the input parameter, `trade` 108: assert( 109: trade.sellPrice > 0 && 110: trade.sellPrice < FIX_MAX && 111: trade.buyPrice > 0 && 112: trade.buyPrice < FIX_MAX 113: ); 168: assert(errorCode == 0x11 || errorCode == 0x12); // @ audit Associated with try catch 170: assert(keccak256(reason) == UIntOutofBoundsHash); // audit Associated with try catch
Consider making the naming of local variables more verbose and descriptive so all other peer developers would better be able to comprehend the intended statement logic, significantly enhancing the code readability.
Here are some of the instances entailed:
55: uint192 s = trade.sellAmount > maxSell ? maxSell : trade.sellAmount; // {sellTok} @ audit s 59: uint192 b = safeMulDivCeil( // @ audit b
According to:
https://docs.soliditylang.org/en/v0.8.14/units-and-global-variables.html
suffixes like seconds
, minutes
, hours
, days
and weeks
after literal numbers can be used to specify units of time where seconds are the base unit and units are considered naively in the following way:
1 == 1 seconds 1 minutes == 60 seconds 1 hours == 60 minutes 1 days == 24 hours 1 weeks == 7 days
As an example, to minimize human error while making the assignment more verbose, the constant declarations below may respectively be rewritten as follows:
- uint48 public constant MAX_TRADING_DELAY = 31536000; // {s} 1 year + uint48 public constant MAX_TRADING_DELAY = 365 days; // {s} 1 year
- uint48 public constant MAX_AUCTION_LENGTH = 604800; // {s} max valid duration - 1 week + uint48 public constant MAX_AUCTION_LENGTH = 1 weeks; // {s} max valid duration - 1 week
delete
to clear variablesdelete a
assigns the initial value for the type to a
. i.e. for integers it is equivalent to a = 0
, but it can also be used on arrays, where it assigns a dynamic array of length zero or a static array of the same length with all elements reset. For structs, it assigns a struct with all members reset. Similarly, it can also be used to set an address to zero address or a boolean to false. It has no effect on whole mappings though (as the keys of mappings may be arbitrary and are generally unknown). However, individual keys and what they map to can be deleted: If a
is a mapping, then delete a[x]
will delete the value stored at x.
The delete key better conveys the intention and is also more idiomatic.
For instance, the mapping instance below may be refactored as follows:
- assets[asset.erc20()] = IAsset(address(0)); + delete assets[asset.erc20()];
##Minimization of truncation The number of divisions in an equation should be reduced to minimize truncation frequency, serving to achieve higher precision. And, where deemed fit, comment the code line with the original multiple division arithmetic operation for clarity reason.
For instance, the equation below with three division may be refactored as follows:
- return (FIX_SCALE_SQ + (stakeRate / 2)) / stakeRate; // ROUND method + return ((FIX_SCALE_SQ * 2) + stakeRate) / (stakeRate * 2); // ROUND method
Consider having events associated with setter functions emit both the new and old values instead of just the new value.
Here are some of the instances entailed:
function setName(string calldata name_) external governance { name = name_; } function setSymbol(string calldata symbol_) external governance { symbol = symbol_; }
Code line repeatedly used may be grouped into a modifier.
For instance, a modifier for the identical require statement in setUnstakingDelay()
and setRewardPeriod()
of StRSR.sol may be refactored as follows:
+ modifier ratioCompatible() { + _; + require(rewardPeriod * 2 <= unstakingDelay, "unstakingDelay/rewardPeriod incompatible"); + } - function setUnstakingDelay(uint48 val) public governance { + function setUnstakingDelay(uint48 val) public ratioCompatible governance { require(val > 0 && val <= MAX_UNSTAKING_DELAY, "invalid unstakingDelay"); emit UnstakingDelaySet(unstakingDelay, val); unstakingDelay = val; - require(rewardPeriod * 2 <= unstakingDelay, "unstakingDelay/rewardPeriod incompatible"); } /// @custom:governance - function setRewardPeriod(uint48 val) public governance { + function setRewardPeriod(uint48 val) public ratioCompatible governance { require(val > 0 && val <= MAX_REWARD_PERIOD, "invalid rewardPeriod"); emit RewardPeriodSet(rewardPeriod, val); rewardPeriod = val; - require(rewardPeriod * 2 <= unstakingDelay, "unstakingDelay/rewardPeriod incompatible"); }
price()
in OracleLib.sol should have a check for negative price. The revert will also prevent an underflow in the return statement when casting a negative integer to uint256()
.
+ error NegativePrice(); function price(AggregatorV3Interface chainlinkFeed, uint48 timeout) internal view returns (uint192) { (uint80 roundId, int256 p, , uint256 updateTime, uint80 answeredInRound) = chainlinkFeed .latestRoundData(); if (updateTime == 0 || answeredInRound < roundId) { revert StalePrice(); } // Downcast is safe: uint256(-) reverts on underflow; block.timestamp assumed < 2^48 uint48 secondsSince = uint48(block.timestamp - updateTime); if (secondsSince > timeout) revert StalePrice(); + if (p < 0) revert NegativePrice(); // // {UoA/tok} return shiftl_toFix(uint256(p), -int8(chainlinkFeed.decimals())); }
The constructor in Main.sol can be rewritten as follows to silence any warning flag without the need for the commented "solhint-disable-next-line no-empty-blocks".
Note: This feature is readily incorporated in the Solidity Wizard since the UUPS vulnerability discovery. You would just need to check UPGRADEABILITY to have the following constructor code block added to the contract:
/// @custom:oz-upgrades-unsafe-allow constructor - // solhint-disable-next-line no-empty-blocks - constructor() initializer {} + constructor() { + _disableInitializers(); + }
Consider rewriting the first line of comments on freezeForever()
as follows:
Note: permanent
means lasting or intended to last or remain unchanged indefinitely. But quite the opposite, the permanent freeze can always be thawed via unfreeze()
and refrozen via freeUntil()
.
- /// Enter a permanent freeze + /// Enter an indefinite freeze // checks: // - caller has the OWNER role // - unfreezeAt != type(uint48).max // effects: unfreezeAt' = type(uint48).max function freezeForever() external onlyRole(OWNER) { freezeUntil(MAX_UNFREEZE_AT); }
Underflow due to decrementing longFreezes[_msgSender()]
in freezeLong()
of Auth.sol will never happen because _msgSender()
has already got its LONG-FREEZER
role revoked when longFreezes[_msgSender()] == 0
. This makes it impossible to reach the first line of the function logic.
function freezeLong() external onlyRole(LONG_FREEZER) { - longFreezes[_msgSender()] -= 1; // reverts on underflow + longFreezes[_msgSender()] -= 1; // No underflow would transpire. Apply unchecked { ... } if need be. // Revoke on 0 charges as a cleanup step if (longFreezes[_msgSender()] == 0) _revokeRole(LONG_FREEZER, _msgSender()); freezeUntil(uint48(block.timestamp) + longFreeze); }
longFreeze
should not only be greater than zero but also greater than MAX_SHORT_FREEZE
or shortfreeze
to be feasibly practical. Otherwise, it could likely end up having shortFreeze > longFreeze
, making freezeLong()
to readily revert (due to newUnfreezeAt < unfreezeAt
) if freezeShort()
had been invoked not too long ago.
Consider having setLongFreeze()
refactored as follows:
function setLongFreeze(uint48 longFreeze_) public onlyRole(OWNER) { - require(longFreeze_ > 0 && longFreeze_ <= MAX_LONG_FREEZE, "long freeze out of range"); + require(longFreeze_ > shortFreeze && longFreeze_ <= MAX_LONG_FREEZE, "long freeze out of range"); emit LongFreezeDurationSet(longFreeze, longFreeze_); longFreeze = longFreeze_; }
Better yet, set a reasonable threshold so that longFreeze_ > shortFreeze + threshold
to further minimize edge cases.
The following event has no parameter in it to emit anything. Although the standard global variables like block.number and block.timestamp will implicitly be tagged along, consider adding some relevant parameters to the make the best out of this emitted event for better support of off-chain logging API.
event MainInitialized();
emit MainInitialized();
In manageTokensSortedOrder()
of BackingManager.sol, erc20s
must already be in sorted ascending order before it can call ArrayLib.sortedAndAllUnique(erc20s)
.
Here is a useful and quick sortAscending()
that may be added to Array.sol on top of the off chain method the protocol might already adopt. (Note: IERC20[] memory arr
will have to be cast into uint160[] memory arr
prior to calling this function.)
function sortAscending( uint160[] memory arr, int160 left, int160 right ) internal pure { int160 i = left; int160 j = right; if (i == j) return; uint160 pivot = arr[uint160(left + (right - left) / 2)]; while (i <= j) { while (arr[uint160(i)] < pivot) i++; while (pivot < arr[uint256(j)]) j--; if (i <= j) { (arr[uint160(i)], arr[uint160(j)]) = ( arr[uint160(j)], arr[uint160(i)] ); i++; j--; } } if (left < j) _quickSort(arr, left, j); if (i < right) _quickSort(arr, i, right); }
allUnique()
in Array.sol could have a remove logic embeddedConsider having allUnique()
refactored such that it will remove one of the two identical elements, e.g. replace the would be removed element with the last element and then calling arr.pop().
Alternatively, here is another useful removePosition()
that can be invoked without affecting the order of the elements in the array:
function _removePosition(IERC20[] memory arr, uint8 position) internal returns (IERC20[] memory newArr) { uint256 length = arr.length; require(position < length); newArr = new arr[](length - 1); uint256 i; for (; i < position; ) { newArr[i] = arr[i]; unchecked { ++i; } } for (; i < length - 1; ) { unchecked { newArr[i] = arr[i + 1]; ++i; } } }
In openTrade()
of Broker.sol, CEIL
is the input RoundMode in mulDiv256()
to return result++ to req.sellAmount
.
This could still end up having req.sellAmount > GNOSIS_MAX_TOKENS
if maxQty == req.sellAmount
. For this reason, consider refactoring the affected code by replacing CEIL
with FLOOR
to avoid the incidental edge cases:
if (maxQty > GNOSIS_MAX_TOKENS) { - req.sellAmount = mulDiv256(req.sellAmount, GNOSIS_MAX_TOKENS, maxQty, CEIL); + req.sellAmount = mulDiv256(req.sellAmount, GNOSIS_MAX_TOKENS, maxQty, FLOOR); req.minBuyAmount = mulDiv256(req.minBuyAmount, GNOSIS_MAX_TOKENS, maxQty, FLOOR); }
#0 - c4-judge
2023-01-25T00:19:35Z
0xean marked the issue as grade-a
🌟 Selected for report: IllIllI
Also found by: 0xA5DF, 0xSmartContract, 0xhacksmithh, AkshaySrivastav, Awesome, Aymen0909, Bauer, Bnke0x0, Breeje, Budaghyan, Cyfrin, Madalad, NoamYakov, RHaO-sec, Rageur, RaymondFam, ReyAdmirado, Rolezn, SAAJ, SaharDevep, Sathish9098, __141345__, amshirif, arialblack14, c3phas, carlitox477, chaduke, delfin454000, descharre, nadin, oyc_109, pavankv, saneryee, shark
72.4433 USDC - $72.44
Consider having the logic of a modifier embedded through a private function to reduce contract size if need be. A private visibility that saves more gas on function calls than the internal visibility is adopted because the modifier will only be making this call inside the contract.
For instance, the modifier instance below may be refactored as follows:
+ function _notPausedOrFrozen() private view { + require(!main.pausedOrFrozen(), "paused or frozen"); + } modifier notPausedOrFrozen() { - require(!main.pausedOrFrozen(), "paused or frozen"); + _notPausedOrFrozen(); _; }
A storage pointer is cheaper since copying a state struct in memory would incur as many SLOADs and MSTOREs as there are slots. In another words, this causes all fields of the struct/array to be read from storage, incurring a Gcoldsload (2100 gas) for each field of the struct/array, and then further incurring an additional MLOAD rather than a cheap stack read. As such, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables will be much cheaper, involving only Gcoldsload for all associated field reads. Read the whole struct/array into a memory variable only when it is being returned by the function, passed into a function that requires memory, or if the array/struct is being read from another memory array/struct.
Here are some of the instances entailed:
File: RecollateralizationLib.sol
70: ComponentCache memory components = ComponentCache({ 78: TradingRules memory rules = TradingRules({ 83: Registry memory reg = components.reg.getRegistry(); [ ... ]
In price()
of OracleLib.sol, the two if blocks can be merged as follows to save gas both on contract size and function calls:
function price(AggregatorV3Interface chainlinkFeed, uint48 timeout) internal view returns (uint192) { (uint80 roundId, int256 p, , uint256 updateTime, uint80 answeredInRound) = chainlinkFeed .latestRoundData(); - if (updateTime == 0 || answeredInRound < roundId) { + if (uint48(block.timestamp - updateTime) > timeOut || answeredInRound < roundId) { revert StalePrice(); } // Downcast is safe: uint256(-) reverts on underflow; block.timestamp assumed < 2^48 - uint48 secondsSince = uint48(block.timestamp - updateTime); - if (secondsSince > timeout) revert StalePrice(); // {UoA/tok} return shiftl_toFix(uint256(p), -int8(chainlinkFeed.decimals())); }
||
costs less gas than its equivalent &&
Rule of thumb: (x && y)
is (!(!x || !y))
Even with the 10k Optimizer enabled: ||
, OR conditions cost less than their equivalent &&
, AND conditions.
As an example, the instance below may be refactored as follows:
File: RecollateralizationLib.sol#L401
- if (address(trade.sell) == address(0) && address(trade.buy) != address(0)) { + if (!(address(trade.sell) != address(0) || address(trade.buy) == address(0))) {
In melt()
of Furnace.sol, the last if block should be executed before lastPayoutBal
is updated. In the event amount == 0
due to ratio == 0
, the function could end with a return and save some gas.
function melt() external notPausedOrFrozen { if (uint48(block.timestamp) < uint64(lastPayout) + period) return; // # of whole periods that have passed since lastPayout uint48 numPeriods = uint48((block.timestamp) - lastPayout) / period; // Paying out the ratio r, N times, equals paying out the ratio (1 - (1-r)^N) 1 time. uint192 payoutRatio = FIX_ONE.minus(FIX_ONE.minus(ratio).powu(numPeriods)); uint256 amount = payoutRatio.mulu_toUint(lastPayoutBal); lastPayout += numPeriods * period; + if (amount == 0) { + return; + else { + rToken.melt(amount); + } lastPayoutBal = rToken.balanceOf(address(this)) - amount; - if (amount > 0) rToken.melt(amount); }
"Checked" math, which is default in ^0.8.0 is not free. The compiler will add some overflow checks, somehow similar to those implemented by SafeMath
. While it is reasonable to expect these checks to be less expensive than the current SafeMath
, one should keep in mind that these checks will increase the cost of "basic math operation" that were not previously covered. This particularly concerns variable increments in for loops. When no arithmetic overflow/underflow is going to happen, unchecked { ++i ;}
to use the previous wrapping behavior further saves gas just as in the for loop below as an example:
File: RecollateralizationLib.sol#L437-L454
- for (uint256 i = 0; i < len; ++i) { + for (uint256 i = 0; i < len;) { ICollateral coll = components.reg.toColl(basketERC20s[i]); [ ... ] + unchecked { + ++i; + } }
In the EVM, there is no opcode for non-strict inequalities (>=, <=) and two operations are performed (> + = or < + =).
As an example, the following >=
inequality instance may be refactored as follows:
- assert(erc20s[i].balanceOf(address(this)) >= liabilities[erc20s[i]]); + assert(erc20s[i].balanceOf(address(this)) > liabilities[erc20s[i]] - 1);
break
or continue
in for loopfor
loop entailing large array with reverting logic should incorporate break
or continue
to cater for element(s) failing to get through the iteration(s). This will tremendously save gas on instances where the loop specifically fails to execute at the end of the iterations.
Here is an instance entailed where refresh()
is associated with try catch reverting code:
File: AssetRegistry.sol#L46-L52
function refresh() public { // It's a waste of gas to require notPausedOrFrozen because assets can be updated directly uint256 length = _erc20s.length(); for (uint256 i = 0; i < length; ++i) { assets[IERC20(_erc20s.at(i))].refresh(); } }
Instead of using the &&
operator in a single require statement to check multiple conditions, using multiple require statements with 1 condition per require statement will save 3 GAS per &&
.
Here is a sample instance entailed:
require( newAuctionLength > 0 && newAuctionLength <= MAX_AUCTION_LENGTH, "invalid auctionLength" );
You can have further advantages in term of gas cost by simply using named return values as temporary local variable.
For instance, the code block below may be refactored as follows:
function deploy( string memory name, string memory symbol, string calldata mandate, address owner, DeploymentParams memory params - ) external returns (address) { + ) external returns (address rTokenAddr) { [ ... ] - return (address(components.rToken)); + rTokenAddr = address(components.rToken); }
In Auth.sol, a LONG_FREEZER role is given 6 charges whereas the SHORT_FREEZER role is only entitled to a one time use. And when they are run out of the assigned privileges, they can/will be recharged via grantRole()
. In my opinion, the logic entailed is unnecessary since these assignees are trusted partners and/or associates to the protocol. If need be, the owner can always revoke their roles via the inherited revokeRole()
.
Consider removing the following lines of codes to save gas both on contract size and function calls:
- 7: uint256 constant LONG_FREEZE_CHARGES = 6; // 6 uses - 121: // Revoke short freezer role after one use - 122: _revokeRole(SHORT_FREEZER, _msgSender()); - 138: // Revoke on 0 charges as a cleanup step - 139: if (longFreezes[_msgSender()] == 0) _revokeRole(LONG_FREEZER, _msgSender());
+=
and -=
generally cost 22 more gas than writing out the assigned equation explicitly. The amount of gas wasted can be quite sizable when repeatedly operated in a loop.
For instance, the +=
instance below may be refactored as follows:
- liabilities[IERC20(erc20s[i])] += deposits[i]; + liabilities[IERC20(erc20s[i])] = liabilities[IERC20(erc20s[i])] + deposits[i];
if ... else
Using ternary operator instead of the if else statement saves gas.
For instance, the code block below may be refactored as follows:
earliest ? refundSpan(account, queue.left, endId) : refundSpan(account, endId, queue.right);
The input parameter, val
, in setUnstakingDelay()
and setRewardPeriod()
of StRSR.sol could have been used in the second require statement to separately save 1 SLOAD on unstakingDelay
:
function setUnstakingDelay(uint48 val) public governance { require(val > 0 && val <= MAX_UNSTAKING_DELAY, "invalid unstakingDelay"); emit UnstakingDelaySet(unstakingDelay, val); unstakingDelay = val; - require(rewardPeriod * 2 <= unstakingDelay, "unstakingDelay/rewardPeriod incompatible"); + require(rewardPeriod * 2 <= val, "unstakingDelay/rewardPeriod incompatible"); } /// @custom:governance function setRewardPeriod(uint48 val) public governance { require(val > 0 && val <= MAX_REWARD_PERIOD, "invalid rewardPeriod"); emit RewardPeriodSet(rewardPeriod, val); rewardPeriod = val; - require(rewardPeriod * 2 <= unstakingDelay, "unstakingDelay/rewardPeriod incompatible"); + require(val * 2 <= unstakingDelay, "unstakingDelay/rewardPeriod incompatible"); }
Checks in a function logic should be as early as possible to minimize gas waste in the event of a revert.
For instance, the same instances in the preceding issue with the input parameter, val
, fully utilized should have been refactored as follows:
function setUnstakingDelay(uint48 val) public governance { require(val > 0 && val <= MAX_UNSTAKING_DELAY, "invalid unstakingDelay"); + require(rewardPeriod * 2 <= val, "unstakingDelay/rewardPeriod incompatible"); emit UnstakingDelaySet(unstakingDelay, val); unstakingDelay = val; - require(rewardPeriod * 2 <= unstakingDelay, "unstakingDelay/rewardPeriod incompatible"); } /// @custom:governance function setRewardPeriod(uint48 val) public governance { require(val > 0 && val <= MAX_REWARD_PERIOD, "invalid rewardPeriod"); + require(val * 2 <= unstakingDelay, "unstakingDelay/rewardPeriod incompatible"); emit RewardPeriodSet(rewardPeriod, val); rewardPeriod = val; - require(rewardPeriod * 2 <= unstakingDelay, "unstakingDelay/rewardPeriod incompatible"); }
Consider marking functions with access control as payable
. This will save 20 gas on each call by their respective permissible callers for not needing to have the compiler check for msg.value
.
For instance, the function below may be refactored as follows:
- function setTradingDelay(uint48 val) public governance { + function setTradingDelay(uint48 val) public payable governance {
The order of function will also have an impact on gas consumption. Because in smart contracts, there is a difference in the order of the functions. Each position will have an extra 22 gas. The order is dependent on method ID. So, if you rename the frequently accessed function to more early method ID, you can save gas cost. Please visit the following site for further information:
Before deploying your contract, activate the optimizer when compiling using “solc --optimize --bin sourceFile.sol”. By default, the optimizer will optimize the contract assuming it is called 200 times across its lifetime. If you want the initial contract deployment to be cheaper and the later function executions to be more expensive, set it to “ --optimize-runs=1”. Conversely, if you expect many transactions and do not care for higher deployment cost and output size, set “--optimize-runs” to a high number.
module.exports = { solidity: { version: "0.8.9", settings: { optimizer: { enabled: true, runs: 1000, }, }, }, };
Please visit the following site for further information:
https://docs.soliditylang.org/en/v0.5.4/using-the-compiler.html#using-the-commandline-compiler
Here's one example of instance on opcode comparison that delineates the gas saving mechanism:
for !=0 before optimization PUSH1 0x00 DUP2 EQ ISZERO PUSH1 [cont offset] JUMPI after optimization DUP1 PUSH1 [revert offset] JUMPI
Disclaimer: There have been several bugs with security implications related to optimizations. For this reason, Solidity compiler optimizations are disabled by default, and it is unclear how many contracts in the wild actually use them. Therefore, it is unclear how well they are being tested and exercised. High-severity security issues due to optimization bugs have occurred in the past . A high-severity bug in the emscripten -generated solc-js compiler used by Truffle and Remix persisted until late 2018. The fix for this bug was not reported in the Solidity CHANGELOG. Another high-severity optimization bug resulting in incorrect bit shift results was patched in Solidity 0.5.6. Please measure the gas savings from optimizations, and carefully weigh them against the possibility of an optimization-related bug. Also, monitor the development and adoption of Solidity compiler optimizations to assess their maturity.
Using assembly to check for address(0) saves 6 gas for each instance.
As an example, the following instance may be refactored as follows:
File: ComponentRegistry.sol#L36-L40
function _setRToken(IRToken val) private { - require(address(val) != address(0), "invalid RToken address"); + assembly { + if iszero(address(val)) { + mstore(0x00, "invalid RToken address") + revert(0x00, 0x20) + } emit RTokenSet(rToken, val); rToken = val; }
In BackingManager.sol, the second check in the if block of handoutExcessAssets()
is unneeded because basketHandler.fullyCollateralized()
has already confirmed that basketsHeldBy(address(backingManager)) >= rToken.basketsNeeded()
before handoutExcessAssets()
is privately invoked:
File: BasketHandler.sol#L273-L275
function fullyCollateralized() external view returns (bool) { return basketsHeldBy(address(backingManager)) >= rToken.basketsNeeded(); }
As such consider refactoring the following code line to save gas on contract size and function calls:
File: BackingManager.sol#L192-L206
- if (held.gt(needed)) { // gas-optimization: RToken is known to have 18 decimals, the same as FixLib uint192 totalSupply = _safeWrap(rToken.totalSupply()); // {rTok} // {BU} = {BU} - {BU} uint192 extraBUs = held.minus(needed); // {rTok} = {BU} * {rTok / BU} (if needed == 0, conv rate is 1 rTok/BU) uint192 rTok = (needed > 0) ? extraBUs.mulDiv(totalSupply, needed) : extraBUs; // gas-optimization: RToken is known to have 18 decimals, same as FixLib rToken.mint(address(this), uint256(rTok)); rToken.setBasketsNeeded(held); needed = held; - }
In quantityMulPrice()
of BasketHandler.sol, the comment before the first if block already mentioned qty will never = 0 here because of the check in _price(). For this reason, qty == 0
is always going to return false, making this check a waste of gas.
Consider refactoring the affected code line as follows:
File: BasketHandler.sol#L358-L359
// qty will never = 0 here because of the check in _price() - if (qty == 0 || p == 0) return 0; + if (p == 0) return 0;
assert()
In init()
of GnosisTrade.sol, asserting origin_ != address(0)
is unnecessary because it is the caller, _msgSender()
, that has invoked openTrade()
in Broker.sol before trade.init()
is externally called.
Consider removing it to save gas both on contract size and function calls:
- assert(origin_ != address(0));
#0 - c4-judge
2023-01-24T23:17:05Z
0xean marked the issue as grade-b