Platform: Code4rena
Start Date: 27/01/2022
Pot Size: $75,000 USDT
Total HM: 6
Participants: 29
Period: 7 days
Judge: leastwood
Total Solo HM: 6
Id: 72
League: ETH
Rank: 6/29
Findings: 2
Award: $4,394.06
🌟 Selected for report: 15
🚀 Solo Findings: 0
🌟 Selected for report: cccz
Also found by: defsec, mics, sirhashalot
mics
You use safeApprove of openZeppelin although it's deprecated. (see https://github.com/OpenZeppelin/openzeppelin-contracts/blob/566a774222707e424896c0c390a84dc3c13bdcb2/contracts/token/ERC20/utils/SafeERC20.sol#L38) You should change it to increase/decrease Allowance as OpenZeppilin says. This appears in the following locations in the code base:
Deprecated safeApprove in OpenLevV1.sol line 275: marketVars.sellToken.safeApprove(address(addressConfig.dexAggregator), maxSell); Deprecated safeApprove in XOLE.sol line 98: (IERC20(fromToken)).safeApprove(address(dexAgg), amount); Deprecated safeApprove in OpenLevV1Lib.sol line 54: IERC20(token1).safeApprove(address(pool1), uint256(- 1)); Deprecated safeApprove in OpenLevV1Lib.sol line 53: IERC20(token0).safeApprove(address(pool0), uint256(- 1)); Deprecated safeApprove in XOLE.sol line 97: (IERC20(fromToken)).safeApprove(address(dexAgg), 0); Deprecated safeApprove in OpenLevV1.sol line 455: IERC20(sellToken).safeApprove(address(dexAggregator), maxSellAmount); Deprecated safeApprove in OpenLevV1.sol line 447: IERC20(sellToken).safeApprove(address(dexAggregator), sellAmount);
#0 - ColaM12
2022-01-27T12:01:51Z
We are not using openZeppelin SafeERC20. see contracts/lib/TransferHelper.sol
#1 - 0xleastwood
2022-02-19T11:59:31Z
I think this is technically a duplicate of #87
296.6309 USDT - $296.63
mics
From solidity docs: Properly functioning code should never reach a failing assert statement; if this happens there is a bug in your contract which you should fix. With assert the user pays the gas and with require it doesn't. The ETH network gas isn't cheap and users can see it as a scam. You have reachable asserts in the following locations (which should be replaced by require / are mistakenly left from development phase):
FarmingPools.sol : reachable assert in line 77 XOLE.sol : reachable assert in line 303
#0 - ColaM12
2022-01-27T12:26:19Z
Good point, but it does not leads to any risk. Recommend severity: "0(None Critical)"
#1 - 0xleastwood
2022-02-19T12:10:48Z
I actually agree with the warden here. The usage of assert
can be pretty detrimental to the UX of the protocol.
🌟 Selected for report: mics
1627.6042 USDT - $1,627.60
mics
To improve algorithm precision instead using division in comparison use multiplication in the following scenario: Instead a < b / c use a * c < b.
In all of the big and trusted contracts this rule is maintained (for example look at AAVE codebase). OpenLevV1.sol, 125, require(borrow == 0 || deposit.mul(10000).div(tv.borrowValue) > vars.marginLimit, "MAM"); OpenLevV1Lib.sol, 166, uint hAvg = marketValue >= borrowed ? marketValue.sub(borrowed).mul(ratioVars.multiplier).div(borrowed) : 0; OpenLevV1.sol, 219, require(repayed >= closeTradeVars.borrowed.mul(closeTradeVars.closeRatio).div(1e18), "IRP"); OpenLevV1Lib.sol, 164, uint cAvg = marketValue >= borrowed ? marketValue.sub(borrowed).mul(ratioVars.multiplier).div(borrowed) : 0; OpenLevV1Lib.sol, 276, share = totalShare > 0 && reserve > 0 ? totalShare.mul(amount) / reserve : amount; OpenLevV1Lib.sol, 162, uint current = marketValue >= borrowed ? marketValue.sub(borrowed).mul(ratioVars.multiplier).div(borrowed) : 0; LPool.sol, 892, require(vars.actualRepayAmount.mul(1e18).div(vars.accountBorrows) <= 105e16, 'repay more than 5%');
🌟 Selected for report: mics
1627.6042 USDT - $1,627.60
mics
In the following functions no value is returned, due to which by default value of return will be 0. We assumed that after the update you return the latest new value. (similar issue here: https://github.com/code-423n4/2021-10-badgerdao-findings/issues/85).
OpenLevV1Lib.sol, updatePrice OpenLevV1.sol, updatePrice BscDexAggregatorV1.sol, updateV3Observation ControllerV1.sol, updatePriceAllowed EthDexAggregatorV1.sol, updateV3Observation
6.2575 USDT - $6.26
mics
Prefix increments are cheaper than postfix increments.
Further more, using unchecked {++x} is even more gas efficient, and the gas saving accumulates every iteration and can make a real change
There is no risk of overflow caused by increamenting the iteration index in for loops (the ++i
in for (uint256 i = 0; i < numIterations; ++i)
).
But increments perform overflow checks that are not necessary in this case.
These functions use not using prefix increments (++x
) or not using the unchecked keyword:
EthDexAggregatorV1.sol, i, 48 FarmingPools.sol, i, 126 FarmingPools.sol, i, 132 GovernorAlpha.sol, i, 228 OLETokenLock.sol, i, 33 ControllerV1.sol, i, 303 Airdrop.sol, i, 64 UniV3Dex.sol, i, 75 GovernorAlpha.sol, i, 202 FarmingPools.sol, i, 164 GovernorAlpha.sol, i, 307 BscDexAggregatorV1.sol, i, 46 ControllerV1.sol, i, 375 GovernorAlpha.sol, i, 280 OpenLevV1Lib.sol, i, 260 UniV2Dex.sol, i, 59 XOLE.sol, i, 362 OpenLevV1.sol, i, 52
#0 - ColaM12
2022-01-27T10:17:52Z
Good point.
10.3004 USDT - $10.30
mics
In the following files there are state variables that could be set immutable to save gas.
oleToken in FarmingPools.sol symbol in OLEToken.sol xole in GovernorAlpha.sol timelock in GovernorAlpha.sol developer in Adminable.sol name in OLEToken.sol
#0 - ColaM12
2022-01-27T10:09:10Z
Good point, but not worth to update for.
47.0985 USDT - $47.10
mics
Use bytes32 instead of string to save gas whenever possible. String is a dynamic data structure and therefore is more gas consuming then bytes32. You could use bytes32 instead of string in the following places: List format: <solidity file> <line number> <actual code line>
GovernorAlpha.sol, 13, string public constant name = "Open Leverage Governor Alpha";
mics
Caching the array length is more gas efficient. This is because access to a local variable in solidity is more efficient than query storage / calldata / memory We recommend to change from:
for (uint256 i=0; i<array.length; i++) { ... }
to:
uint len = array.length for (uint256 i=0; i<len; i++) { ... }
These functions use not using prefix increments (++x
) or not using the unchecked keyword:
OpenLevV1Lib.sol, dexs, 260 GovernorAlpha.sol, targets.proposal, 228 ControllerV1.sol, lpools, 303 FarmingPools.sol, stakeTokens, 132 OLETokenLock.sol, beneficiaries, 33 FarmingPools.sol, stakeTokens, 126 GovernorAlpha.sol, support, 307 UniV3Dex.sol, path, 75 OpenLevV1.sol, _supportDexs, 52 UniV2Dex.sol, tokens, 59 GovernorAlpha.sol, targets.proposal, 280 XOLE.sol, nonce, 362 ControllerV1.sol, marketIds, 375 BscDexAggregatorV1.sol, dexName, 46 FarmingPools.sol, stakeTokens, 164 GovernorAlpha.sol, targets.proposal, 202 EthDexAggregatorV1.sol, dexName, 48
#0 - ColaM12
2022-01-27T10:54:51Z
We find this changing may cost more gas.
#1 - 0xleastwood
2022-02-20T22:32:47Z
I think this might be optimised out by solidity's optimiser but I'll include it anyway as its typically an expected way to perform loops.
🌟 Selected for report: robee
Also found by: Dravee, mics, sirhashalot
19.0749 USDT - $19.07
mics
In Adminable.sol line 37 you can short the require message from "only pendingAdmin can accept admin" to "you're not a pendingAdmin" and then it will be in 1 slot and save gas.
#0 - ColaM12
2022-01-27T11:20:38Z
duplicate to #6
19.0749 USDT - $19.07
mics
Using newer compiler versions and the optimizer gives gas optimizations and additional safety checks are available for free. The advantages of versions 0.8.* over <0.8.0 are:
XOLEDelegator.sol Timelock.sol ControllerV1.sol
#0 - ColaM12
2022-01-27T10:37:29Z
Good suggestion.
28.2591 USDT - $28.26
mics
Using != 0 is slightly cheaper than > 0. see https://github.com/code-423n4/2021-12-maple-findings/issues/75 for similar issue.
OpenLevV1.sol, 454: change 'buyAmount > 0' to 'buyAmount != 0' OpenLevV1Lib.sol, 112: change 'marginLimit > 0' to 'marginLimit != 0' OpenLevV1.sol, 424: change 'balance > 0' to 'balance != 0' UniV2Dex.sol, 207: change 'reserveIn > 0' to 'reserveIn != 0' ControllerV1.sol, 342: change 'supplyAmount > 0' to 'supplyAmount != 0' UniV3Dex.sol, 165: change 'amount0Delta > 0' to 'amount0Delta != 0' OpenLevV1Lib.sol, 276: change 'totalShare > 0' to 'totalShare != 0' XOLE.sol, 266: change '_value > 0' to '_value != 0' ControllerV1.sol, 363: change 'borrowAmount > 0' to 'borrowAmount != 0'
#0 - ColaM12
2022-01-27T10:49:38Z
Goode point, but not a bug.
#1 - 0xleastwood
2022-02-20T22:40:13Z
Only an issue because the comparison is made in a conditional statement. Typically this will not save gas.
7.9461 USDT - $7.95
mics
Use calldata instead of memory for function parameters In some cases, having function arguments in calldata instead of memory is more optimal.
Consider the following generic example: contract C { function add(uint[] memory arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } } In the above example, the dynamic array arr has the storage location memory. When the function gets called externally, the array values are kept in calldata and copied to memory during ABI decoding (using the opcode calldataload and mstore). And during the for loop, arr[i] accesses the value in memory using a mload. However, for the above example this is inefficient. Consider the following snippet instead: contract C { function add(uint[] calldata arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } } In the above snippet, instead of going via memory, the value is directly read from calldata using calldataload. That is, there are no intermediate memory operations that carries this value. Gas savings: In the former example, the ABI decoding begins with copying value from calldata to memory in a for loop. Each iteration would cost at least 60 gas. In the latter example, this can be completely avoided. This will also reduce the number of instructions and therefore reduces the deploy time cost of the contract. In short, use calldata instead of memory if the function argument is only read. Note that in older Solidity versions, changing some function arguments from memory to calldata may cause "unimplemented feature error". This can be avoided by using a newer (0.8.*) Solidity compiler. (non-exhaustive) List of Examples: DelegatorInterface.delegateToViewImplementation (data) GovernorAlpha.propose (description) ControllerV1.initialize (_oleWethDexData) OpenLevV1.marginRatio (dexData) BscDexAggregatorV1.updateV3Observation (data)
7.9461 USDT - $7.95
mics
Reading a storage variable is gas costly (SLOAD). In cases of multiple read of a storage variable in the same scope, caching the first read (i.e saving as a local variable) can save gas and decrease the overall gas uses. see:
Adminable.sol: pendingAdmin is read twice in acceptAdmin
#0 - ColaM12
2022-01-27T09:55:18Z
Good point, but not worth to update for this.
#1 - 0xleastwood
2022-02-21T02:02:52Z
Duplicate of #137
28.2591 USDT - $28.26
mics
The following functions are used exactly once. Therefore you can inline them and save gas and improve code clearness.
UniV3Dex.sol, uniV3GetPriceCAvgPriceHAvgPrice UniV2Dex.sol, uniV2GetPriceCAvgPriceHAvgPrice UniV3Dex.sol, uniV3Buy UniV2Dex.sol, uniV2CalSellAmount UniV3Dex.sol, uniV3GetAvgPrice OpenLevV1Lib.sol, amountToShare OpenLevV1Lib.sol, shareToAmount UniV2Dex.sol, uniV2GetAvgPrice UniV2Dex.sol, uniV2UpdatePriceOracle UniV3Dex.sol, initializeUniV3 UniV3Dex.sol, uniV3SellMul UniV3Dex.sol, increaseV3Observation UniV2Dex.sol, uniV2SellMul UniV2Dex.sol, uniV2CalBuyAmount UniV2Dex.sol, uniV2GetPrice UniV2Dex.sol, uniV2Buy
#0 - ColaM12
2022-01-27T11:32:44Z
Duplicate to #24
#1 - 0xleastwood
2022-02-21T02:00:09Z
Marking as primary issue.
47.0985 USDT - $47.10
mics
Boolean variables can be checked within conditionals directly without the use of equality operators to true/false.
ControllerV1.sol, 156: if (marketExtraDistribution[marketId] == false) { ControllerV1.sol, 121: if (marketExtraDistribution[marketId] == false) {
🌟 Selected for report: mics
104.6634 USDT - $104.66
mics
There are places in the code (especially in for-each loops) that loads the same array element more than once. In such cases, only one array boundaries check should take place, and the rest are unnecessary. Therefore, this array element should be cached in a local variable and then be loaded again using this local variable, skipping the redundent second array boundaries check:
Airdrop.sol, variable name: _balances times: 2 at: claims UniV2Dex.sol, variable name: tokens times: 2 at: uniV2SellMul OLETokenLock.sol, variable name: startTimes times: 2 at: constructor
#0 - ColaM12
2022-01-27T10:12:21Z
Good point
🌟 Selected for report: mics
104.6634 USDT - $104.66
mics
The following functions could skip other steps if the amount is 0. (A similar issue: https://github.com/code-423n4/2021-10-badgerdao-findings/issues/82)
ControllerV1.sol, redeemAllowed OLEToken.sol, mint Airdrop.sol, newTranche LPool.sol, approve OLETokenLock.sol, constructor XOLE.sol, _mint LPool.sol, mintTo
🌟 Selected for report: mics
mics
Some of your contract inherent contracts but aren't use them at all. We recommend not to inherent those contracts.
ControllerV1.sol; the inherited contracts DelegateInterface not used LPool.sol; the inherited contracts DelegateInterface, LPoolInterface not used EthDexAggregatorV1.sol; the inherited contracts DelegateInterface not used BscDexAggregatorV1.sol; the inherited contracts DelegateInterface, UniV2ClassDex not used OpenLevV1.sol; the inherited contracts DelegateInterface not used
#0 - ColaM12
2022-01-27T11:19:30Z
Duplicate to #4