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: 22/73
Findings: 2
Award: $1,126.63
🌟 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
121.587 USDC - $121.59
ID | Finding | Instances |
---|---|---|
1 | Other contract addresses can only be set once | * |
2 | Precision loss with division | 1 |
ID | Finding | Instances |
---|---|---|
1 | Add custom error messages instead of an empty return | 5 |
2 | Use modifier instead of duplicate require | 2 |
3 | Use a more recent version of solidity | 1 |
4 | Useless statements | 1 |
5 | Require() or revert() statement that check input arguments should be at the top of the function | 4 |
6 | Place structs on top of the contract for overall readability | 6 |
7 | Use defined values instead of variables and computations for constants | 5 |
8 | Combine constants and variables with the exact same value | 1 |
9 | Event is missing indexed fields | 3 |
10 | Using safemath when compiler is ^0.8.0 | 2 |
11 | Not using the named return variables in a function | 2 |
12 | Miscellaneous | 1 |
Other contract addresses can only be set once in the initializer. There is no other setter method for this. This makes it so there is no room for error and can lead to redeployment.
To fix this you can make a setter method for each address that only the owner can call.
When using division with small numbers there can be a precision loss because all integer divisions round down to the nearest integer. To fix this you can make use of multiplier that needs to be accounted for when working with it in the future.
L386: uint256 stakeRSRToTake = (stakeRSR * rsrAmount + (rsrBalance - 1)) / rsrBalance; L401: uint256 draftRSRToTake = (draftRSR * rsrAmount + (rsrBalance - 1)) / rsrBalance; L417: seizedRSR += (rewards * rsrAmount + (rsrBalance - 1)) / rsrBalance;
To make error handling better use custom error messages instead of returning nothing. BackingManager.sol
L109: if (tradesOpen > 0) return; L114: if (block.timestamp < basketTimestamp + tradingDelay) return;
L310: if (endId == 0 || firstId >= endId) return; L327: if (rsrAmount == 0) return; L497: if (block.timestamp < payoutLastPaid + rewardPeriod) return;
L37: require(address(val) != address(0), "invalid RToken address"); L45: require(address(val) != address(0), "invalid StRSR address"); L53: require(address(val) != address(0), "invalid AssetRegistry address"); L61: require(address(val) != address(0), "invalid BasketHandler address"); L69: require(address(val) != address(0), "invalid BackingManager address"); L77: require(address(val) != address(0), "invalid Distributor address"); L85: require(address(val) != address(0), "invalid RSRTrader address"); L93: require(address(val) != address(0), "invalid RTokenTrader address"); L101: require(address(val) != address(0), "invalid Furnace address"); L109: require(address(val) != address(0), "invalid Broker address");
Even if they have different error messages you can still make a modifier
+ modifier checkAddress0(IRToken val, string memory errormessage){ + require(address(val) != address(0), errormessage); + _; + } L36: + function _setRToken(IRToken val) private checkAddress0(val, "invalid RToken address"){ - function _setRToken(IRToken val) private { - require(address(val) != address(0), "invalid RToken address"); emit RTokenSet(rToken, val); rToken = val; }
Same thing can be done for:
StRSRVotes.sol: can be done without the parameter errormassage because error message is the same
L77, L83, L89: require(blockNumber < block.number, "ERC20Votes: block not yet mined");
TargetIndex will 100% of the time be smaller than targetsLength because it's specified like that in the for loop.
This way no gas is wasted when the function is destined to fail.
When it's a constant value it's best to use a RToken.sol
L15: uint192 constant MIN_BLOCK_ISSUANCE_LIMIT = 10_000 * FIX_ONE; L18: uint192 constant MAX_ISSUANCE_RATE = FIX_ONE;
L39 uint192 public constant MAX_REWARD_RATIO = FIX_ONE; L65: uint192 private constant MAX_STAKE_RATE = 1e9 * FIX_ONE; L87 uint192 private constant MAX_DRAFT_RATE = 1e9 * FIX_ONE;
If two constants have the exact same value, see if it's a possibility to make it one variable
L65: uint192 private constant MAX_STAKE_RATE = 1e9 * FIX_ONE; L87 uint192 private constant MAX_DRAFT_RATE = 1e9 * FIX_ONE;
Because of this it can be hard for off-chain scripts to efficiently filter those events.
The compiler has built-in under and overflow checks so there is no need to use SafeMath
For better readability it's better to use a for loop to avoid duplicated code.
r *= 2 - z * r; r *= 2 - z * r; r *= 2 - z * r; r *= 2 - z * r; r *= 2 - z * r; r *= 2 - z * r; r *= 2 - z * r; r *= 2 - z * r;
#0 - c4-judge
2023-01-25T00:06:58Z
0xean marked the issue as grade-b
🌟 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
1005.0379 USDC - $1,005.04
ID | Finding | Instances |
---|---|---|
1 | Make for loops unchecked | * |
2 | Save storage variable to memory when it's used more than once in a function | 8 |
3 | Internal functions only called once can be inlined to save gas | 1 |
4 | Using double require instead of && consumes less gas | 11 |
5 | x = x + y is cheaper than x += y | 20 |
6 | Use calldata instead of memory for read only function parameters | 2 |
7 | Using solidity version 0.8.17 will provide an overall gas optimization | 1 |
8 | Calling msg.sender is cheaper than accessing a memory variable | 9 |
9 | Calling msg.sender directly is cheaper than the function _msgSender() | * |
10 | Combine 2 for loops | 2 |
11 | Useless checks | 2 |
12 | Use an unchecked block when operands can't underflow/overflow | 2 |
13 | Make own counter instead of using CountersUpgradeable | 1 |
14 | Ternary operation is cheaper than if else statement | 2 |
15 | Make up to 3 fields in an event indexed | 2 |
16 | Miscellaneous | 2 |
The risk of for loops getting overflowed is extremely low. Because it always increments by 1 and is limited to the arrays length. Even if the arrays are extremely long, it will take a massive amount of time and gas to let the for loop overflow.
Gas savings when you make all the for loops in RToken.sol unchecked.
Function | Normal | Gas Optimazion | Gas saved |
---|---|---|---|
cancel | 110657 | 110356 | 301 |
issue | 868581 | 868108 | 473 |
redeem | 554004 | 553728 | 276 |
vest | 522834 | 522374 | 460 |
It can also be done on every other for loop in the project. The larger the array the more gas you will save.
Example:
+ function unchecked_inc(uint256 x) private pure returns (uint256) { + unchecked { + return x + 1; + } + } L270: - for (uint256 i = 0; i < erc20s.length; ++i) { + for (uint256 i = 0; i < erc20s.length; i = unchecked_inc(i)) { IERC20Upgradeable(erc20s[i]).safeTransferFrom( issuer, address(backingManager), deposits[i] ); }
Or another way to accomplish this is
L270: + uint256 i; - for (uint256 i = 0; i < erc20s.length; ++i) { + for (; i < erc20s.length;) { IERC20Upgradeable(erc20s[i]).safeTransferFrom( issuer, address(backingManager), deposits[i] ); + unchecked{ + i++; + } }
When a storage variable is read for the second time from storage it costs 100 gas. When it's read from memory it only costs 3 gas. Make sure when you use this technique you don't use the memory when you change it's state.
swapRegistered()
avg gas saved: 164
AssetRegistry.sol#L73-L81
L73: function swapRegistered(IAsset asset) external governance returns (bool swapped) { require(_erc20s.contains(address(asset.erc20())), "no ERC20 collision"); + IBasketHandler handler =basketHandler; - uint192 quantity = basketHandler.quantity(asset.erc20()); + uint192 quantity = handler.quantity(asset.erc20()); - if (quantity > 0) basketHandler.disableBasket(); + if (quantity > 0) handler.disableBasket(); swapped = _registerIgnoringCollisions(asset); }
Same thing can be done for:
unregister()
avg gas saved: 122swapRegistered()
avg gas saved: 122manageTokens()
avg gas saved: 247; manageTokensSortedOrder()
avg gas saved: 267manageTokens()
avg gas saved: 152; manageTokensSortedOrder()
avg gas saved: 316melt()
avg gas saved: 331issue()
avg gas saved: 211unstake()
avg gas savedNot inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.
compromiseBasketsNeeded()
BackingManager.sol#L248-L251Gas saved: 8
L181: require(shortFreeze_ > 0 && shortFreeze_ <= MAX_SHORT_FREEZE, "short freeze out of range"); L188: require(longFreeze_ > 0 && longFreeze_ <= MAX_LONG_FREEZE, "long freeze out of range");
- require(shortFreeze_ > 0 && shortFreeze_ <= MAX_SHORT_FREEZE, "short freeze out of range"); + require(shortFreeze_ > 0, "short freeze out of range"); + require(shortFreeze_ <= MAX_SHORT_FREEZE, "short freeze out of range");
Saves around 20 gas per optimization. This doesn't apply for incrementing an array.
L345: low256 += quantityMulPrice(qty, lowP); L346: high256 += quantityMulPrice(qty, highP);
L165: revTotals.rTokenTotal += share.rTokenDist; L166: revTotals.rsrTotal += share.rsrDist;
L81: lastPayout += numPeriods * period; L166: revTotals.rsrTotal += share.rsrDist;
L229: stakeRSR += rsrAmount; L387: stakeRSR -= stakeRSRToTake; L396: seizedRSR += stakeRSR; L402: draftRSR -= draftRSRToTake; L403: seizedRSR += draftRSRToTake; L412: seizedRSR += draftRSR; L417: seizedRSR += (rewards * rsrAmount + (rsrBalance - 1)) / rsrBalance; L513: stakeRSR += payout; L516: payoutLastPaid += numPeriods * rewardPeriod; L549: draftRSR += rsrAmount; L699: totalStakes += amount; L721: totalStakes -= amount;
L264: assetsLow += low.mul(bal, FLOOR); L271: else assetsHigh += val;
If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory.
Deployer.sol: deploy function: avg gas saved: 395
L103: string memory name, L104: string memory symbol,
Using at least 0.8.10 will save gas due to skipped extcodesize check if there is a return value and it makes overflow checks on multiplication more efficient.
Instead of writing the msg.sender to a memory variable it's better to call it direclty. Reading from memory costs 3 gas, calling msg.sender directly is only 2 gas. Additionally you also save gas for not writing to a memory variable
The following changes save 26 gas:
L85: function openTrade(TradeRequest memory req) external notPausedOrFrozen returns (ITrade) { require(!disabled, "broker disabled"); - address caller = _msgSender(); require( - caller == address(backingManager) || - caller == address(rsrTrader) || - caller == address(rTokenTrader), + msg.sender == address(backingManager) || + msg.sender == address(rsrTrader) || + msg.sender == address(rTokenTrader), "only traders" ); L109: // == Interactions == IERC20Upgradeable(address(req.sell.erc20())).safeTransferFrom( - caller, + msg.sender, address(trade), req.sellAmount ); - trade.init(this, caller, gnosis, auctionLength, req); + trade.init(this, msg.sender, gnosis, auctionLength, req); return trade; }
Can also be done at:
Same applies for block.timestamp
If you don't want to do the optimization above you can also instead of calling the function _msgSender() call msg.sender direclty. This keeps the same readability and does exaclty the same.
- issue(_msgSender(), amtRToken); + issue(msg.sender, amtRToken);
This can be done for every _msgSender function in the code. Most of them are in RToken.sol and StRSR.sol
When 2 for loops always have the exact same length they can be combined.
In the following code making the struct is pretty useless. This optimization saves 923 gas
L105: - Transfer[] memory transfers = new Transfer[](destinations.length()); uint256 numTransfers; for (uint256 i = 0; i < destinations.length(); ++i) { L123: - transfers[numTransfers] = Transfer({ - erc20: erc20, - addrTo: addrTo, - amount: transferAmt - }); + IERC20Upgradeable(address(erc20)).safeTransferFrom(from, addrTo, transferAmt); numTransfers++; } emit RevenueDistributed(erc20, from, amount); // == Interactions == - for (uint256 i = 0; i < numTransfers; i++) { - Transfer memory t = transfers[i]; - IERC20Upgradeable(address(t.erc20)).safeTransferFrom(from, t.addrTo, t.amount); - }
Same can be applied to:
issue()
avg gas saved: 244When operands can't underflow/overflow because of a previous require() or if statement, you can use an unchecked block.
In the assert statement is checked that totalStakes + amount is smaller than max of uint224. So there is no possibility of overflowing
StRSR.sol#L699: stake()
avg gas saved: 186
assert(totalStakes + amount < type(uint224).max); + unchecked{ stakes[era][account] += amount; totalStakes += amount; + }
If stakes per account can't be underflowed, the same applies for the totalStakes
StRSR.sol#L721: unstake()
avg gas saved: 91
require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); unchecked { eraStakes[account] = accountBalance - amount; - } totalStakes -= amount; + }
StRSR.sol: permit()
avg gas saved: 30
+ mapping(address => uint256) noncesCounter; L786: function nonces(address owner) public view returns (uint256) { - return _nonces[owner].current(); + return noncesCounter[owner]; } L795: function _useNonce(address owner) internal returns (uint256 current) { - CountersUpgradeable.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); + unchecked{ + current = noncesCounter[owner]++; + } }
Really small optimization so it's also a choice to keep the if-else statement for readability
- if (uint256(assetsHigh) + val >= FIX_MAX) assetsHigh = FIX_MAX; - else assetsHigh += val; + assetsHigh = (uint256(assetsHigh) + val >= FIX_MAX ? FIX_MAX: assetsHigh + val);
Indexing all fields in an event saves gas. If the event has more than 3 fiels, only index 3.
Saves around 30 gas
For things like minus, plus, gt and other simple comparisons or calculations. It's better to not use the FixLib to save gas due to having less jumps. It keeps the same readability. For more complicated calculations you can keep using FixLib to keep the readability. Example:
- if (bal.gt(needed)) { + if (bal>needed) {
#0 - c4-judge
2023-01-24T23:45:06Z
0xean marked the issue as grade-a