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: 23/154
Findings: 2
Award: $764.26
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: GalloDaSballo
Also found by: 0x3b, 0xAgro, 0xSmartContract, 0xTheC0der, 0xackermann, 0xnev, 0xsomeone, ABA, BRONZEDISC, Bjorn_bug, Bnke0x0, Breeje, Co0nan, CodeFoxInc, CodingNameKiki, DadeKuma, DeFiHackLabs, IceBear, Josiah, Kaysoft, Lavishq, MohammedRizwan, PaludoX0, PawelK, Phantasmagoria, Raiders, RaymondFam, Rickard, Rolezn, Sathish9098, SleepingBugs, SuperRayss, UdarTeam, Udsen, Viktor_Cortess, arialblack14, ast3ros, bin2chen, brgltd, btk, catellatech, ch0bu, chaduke, chrisdior4, codeislight, cryptonue, delfin454000, descharre, dontonka, emmac002, fs0c, hacker-dom, hansfriese, imare, lukris02, luxartvinsec, martin, matrix_0wl, peanuts, rbserver, shark, tnevler, trustindistrust, tsvetanovv, vagrant, yongskiws, zzzitron
455.4712 USDC - $455.47
Number | Issues Details | Context |
---|---|---|
[L-01] | Use ERC-5143: Slippage Protection for Tokenized Vault | 1 |
[L-02] | Head Overflow Bug in Calldata Tuple ABI-Reencoding | 1 |
[L-03] | uint type argument was use instead uint256 for permit function , this will cause the function selector to be miscalculated | 1 |
[L-04] | First ERC4626 deposit exploit can break share calculation | 1 |
[L-05] | initialize() function can be called by anybody | 1 |
[L-06] | Insufficient coverage | All Contracts |
[L-07] | Missing Event for initialize | 1 |
[L-08] | Project Upgrade and Stop Scenario should be | 1 |
[L-09] | Loss of precision due to rounding in amount / uint256(rewardsPerSecond) | 1 |
[L-10] | Use Fuzzing Test for complicated code bases | |
[L-11] | Update codes to avoid Compile Errors | 11 |
[L-12] | Add to Event-Emit for critical functions | 3 |
[L-13] | Use uint256 instead uint | 477 |
[L-14] | Signature Malleability of EVM's ecrecover() | 1 |
[L-15] | sendCollateral event is missing parameters | 1 |
[L-16] | Lack of Input Validation | 1 |
[L-17] | Prevent division by 0 | 1 |
[L-18] | Project has NPM Dependency which uses a vulnerable version : @openzeppelin | 1 |
[L-19] | Keccak Constant values should used to immutable rather than constant | 8 |
[L-20] | Due to the novelty of the ERC4626 standard, it is safer to use as upgradeable | |
[L-21] | In the setAddresses function, there is no return of incorrect address identification | 1 |
Total 21 issues
Number | Issues Details | Context |
---|---|---|
[N-01] | Omissions in Events | 2 |
[N-02] | NatSpec comments should be increased in contracts | All Contracts |
[N-03] | Function writing that does not comply with the Solidity Style Guide | All Contracts |
[N-04] | Include return parameters in NatSpec comments | All Constracs |
[N-05] | Tokens accidentally sent to the contract cannot be recovered | 1 |
[N-06] | Repeated validation logic can be converted into a function modifier | 13 |
[N-07] | Use a more recent version of Solidity | 14 |
[N-08] | For functions, follow Solidity standard naming conventions (internal function style rule) | 4 |
[N-09] | Floating pragma | 4 |
[N-10] | Use descriptive names for Contracts and Libraries | All Contracts |
[N-11] | Use SMTChecker | All Contracts |
[N-12] | Add NatSpec Mapping comment | 13 |
[N-13] | Showing the actual values of numbers in NatSpec comments makes checking and reading code easier | 2 |
[N-14] | Lines are too long | 16 |
[N-15] | Constants on the left are better | 36 |
[N-16] | constants should be defined rather than using magic numbers | 3 |
[N-17] | Missing timelock for critical parameter change | 1 |
[N-18] | Use the delete keyword instead of assigning a value of 0 | 17 |
[N-19] | Not using the named return variables anywhere in the function is confusing | 2 |
[N-20] | Function Calls in Loop Could Lead to Denial of Service due to Array length not being checked | 2 |
[N-21] | According to the syntax rules, use mapping () instead of mapping() using spaces as keyword | 2 |
[N-22] | For modern and more readable code; update import usages | 110 |
[N-23] | Use require instead of assert | 20 |
[N-24] | Implement some type of version counter that will be incremented automatically for contract upgrades | 1 |
[N-25] | Use a single file for all system-wide constants | 18 |
[N-26] | Assembly Codes Specific – Should Have Comments | 1 |
[N-27] | Highest risk must be specified in NatSpec comments and documentation | 1 |
[N-28] | Include proxy contracts to Audit |
Total 28 issues
Number | Suggestion Details |
---|---|
[S-01] | Generate perfect code headers every time |
Total 2 suggestions
Project use ERC-4626 standart
EIP-4626 is vulnerable to the so-called inflation attacks. This attack results from the possibility to manipulate the exchange rate and front run a victim’s deposit when the vault has low liquidity volume.
Proposed mitigation
The following standard extends the EIP-4626 Tokenized Vault standard with functions dedicated to the safe interaction between EOAs and the vault when price is subject to slippage.
https://eips.ethereum.org/EIPS/eip-5143
On July 5, 2022, Chance Hudson (@vimwitch) from the Ethereum Foundation discovered a bug in the Solidity code generator. The earliest affected version of the compiler is 0.5.8, which introduced ABI-reencoding of calldata arrays and structs. Solidity version 0.8.16, released on August 08, 2022, provides a fix.
 Fix data corruption that affected ABI-encoding of calldata values represented by tuples: structs at any nesting level; argument lists of external functions, events and errors; return value lists of external functions. The 32 leading bytes of the first dynamically-encoded value in the tuple would get zeroed when the last component contained a statically-encoded array.
Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol: 159 */ 160: function setHarvestSteps(address[2][] calldata _newSteps) external { 161: _atLeastRole(ADMIN); 162: delete steps; 163: 164: uint256 numSteps = _newSteps.length; 165: for (uint256 i = 0; i < numSteps; i = i.uncheckedInc()) { 166: address[2] memory step = _newSteps[i]; 167: require(step[0] != address(0)); 168: require(step[1] != address(0)); 169: steps.push(step); 170: } 171: }
https://blog.soliditylang.org/2022/08/08/calldata-tuple-reencoding-head-overflow-bug/
Use solidity of version min. 0.8.16
uint
type argument was use instead uint256
for permit
function , this will cause the function selector to be miscalculatedLUSDToken.permit()
function is important external function and use for external account interaction, when external contract accounts interact with the contract, they trigger the permit
function with interface or low level call
However, the best practice of the permit
function is to use uint256
type arguments, in the project it cannot interact with the permit
function of the contract because it is used with uint
type, the operation will be reverted
Ethos-Core/contracts/LUSDToken.sol: 261 262: function permit 263: ( 264: address owner, 265: address spender, 266: uint amount, 267: uint deadline, 268: uint8 v, 269: bytes32 r, 270: bytes32 s 271: ) 272: external 273: override 274: {
contract CheckFunctionSelector { // 0x97a6e84a function functionSelector_uint() external pure returns (bytes4 fourbyte) { return bytes4(keccak256(bytes("permit(address,address,uint,uint,uint8,bytes32,bytes32)"))); } // 0xd505accf function functionSelector_uint256() external pure returns (bytes4 fourbyte) { return bytes4(keccak256(bytes("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"))); } }
Ethos-Core/contracts/LUSDToken.sol: 261 262: function permit 263: ( 264: address owner, 265: address spender, - 266: uint amount, + 266: uint256 amount, - 267: uint deadline, + 267: uint256 deadline, 268: uint8 v, 269: bytes32 r, 270: bytes32 s 271: ) 272: external 273: override 274: {
A well known attack vector for almost all shares based liquidity pool contracts, where an early user can manipulate the price per share and profit from late users' deposits because of the precision loss caused by the rather large value of price per share.
Ethos-Vault/contracts/ReaperVaultERC4626.sol: 50 // the “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and from. 51: function convertToShares(uint256 assets) public view override returns (uint256 shares) { 52: if (totalSupply() == 0 || _freeFunds() == 0) return assets; 53: return (assets * totalSupply()) / _freeFunds(); 54: }
1 - A malicious early user can deposit()
 with 1 wei of asset token as the first depositor of the LToken, and get 1 wei of shares
2 - Then the attacker can send 10000e18 - 1 of asset tokens and inflate the price per share from 1.0000 to an extreme value of 1.0000e22 ( from (1 + 10000e18 - 1) / 1)
3 - As a result, the future user who deposits 19999e18 will only receive 1 wei (from 19999e18 * 1 / 10000e18) of shares token
4 - They will immediately lose 9999e18 or half of their deposits if they redeem()
 right after the deposit()
The attacker can profit from future users' deposits. While the late users will lose part of their funds to the attacker.
Consider sending the first 1000 shares to the address 0, a mitigation used in Uniswap V2
initialize()
function can be called anybody when the contract is not initialized.
More importantly, if someone else runs this function, they will have full authority because of the __ReaperBaseStrategy_init()
function and
__ReaperBaseStrategy_init()
function sets DEFAULT_ADMIN_ROLE , so this is very important risk
Here is a definition of initialize()
function.
Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol: 61 */ 62: function initialize( 63: address _vault, 64: address[] memory _strategists, 65: address[] memory _multisigRoles, 66: IAToken _gWant 67: ) public initializer { 68: gWant = _gWant; 69: want = _gWant.UNDERLYING_ASSET_ADDRESS(); 70: __ReaperBaseStrategy_init(_vault, want, _strategists, _multisigRoles); 71: rewardClaimingTokens = [address(_gWant)]; 72: }
Add a control that makes initialize()
only call the Deployer Contract or EOA;
if (msg.sender != DEPLOYER_ADDRESS) { revert NotDeployer(); }
Description: The test coverage rate of the project is ~93%. Testing all functions is best practice in terms of security criteria. Due to its capacity, test coverage is expected to be 100%.
1 result - 1 file README.md: 91: - What is the overall line coverage percentage provided by your tests?: 93
Context:
Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol: 61 */ 62: function initialize( 63: address _vault, 64: address[] memory _strategists, 65: address[] memory _multisigRoles, 66: IAToken _gWant 67: ) public initializer { 68: gWant = _gWant; 69: want = _gWant.UNDERLYING_ASSET_ADDRESS(); 70: __ReaperBaseStrategy_init(_vault, want, _strategists, _multisigRoles); 71: rewardClaimingTokens = [address(_gWant)]; 72: }
Description: Events help non-contract tools to track changes, and events prevent users from being surprised by changes Issuing event-emit during initialization is a detail that many projects skip
Recommendation: Add Event-Emit
At the start of the project, the system may need to be stopped or upgraded, I suggest you have a script beforehand and add it to the documentation. This can also be called an " EMERGENCY STOP (CIRCUIT BREAKER) PATTERN ".
https://github.com/maxwoe/solidity_patterns/blob/master/security/EmergencyStop.sol
amount / uint256(rewardsPerSecond)
If totalSupply is too low then the rounding problem is big
Add scalar so roundings are negligible
Ethos-Vault/contracts/ReaperVaultV2.sol: 365: value = (_freeFunds() * _shares) / totalSupply();
I recommend fuzzing testing in complex code structures, especially Ethos Reserve, where there is an innovative code base and a lot of computation.
Recommendation:
Use should fuzzing test like Echidna.
As Alberto Cuesta Canada said:
Fuzzing is not easy, the tools are rough, and the math is hard, but it is worth it. Fuzzing gives me a level of confidence in my smart contracts that I didn’t have before. Relying just on unit testing anymore and poking around in a testnet seems reckless now.
https://medium.com/coinmonks/smart-contract-fuzzing-d9b88e0b0a05
contracts/StabilityPool.sol:413:1: Warning: Unused local variable. uint LUSDLoss = initialDeposit.sub(compoundedLUSDDeposit); // Needed only for event log ^-----------^ contracts/StabilityPool.sol:502:1: Warning: Unused local variable. uint LUSDLoss = initialDeposit.sub(compoundedLUSDDeposit); // Needed only for event log ^-----------^ contracts/ActivePool.sol:26:1: Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries. contract ActivePool is Ownable, CheckContract, IActivePool { ^ (Relevant source part starts here and spans across multiple lines). contracts/BorrowerOperations.sol:18:1: Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries. contract BorrowerOperations is LiquityBase, Ownable, CheckContract, IBorrowerOperations { ^ (Relevant source part starts here and spans across multiple lines). contracts/LQTY/LQTYStaking.sol:18:1: Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries. contract LQTYStaking is ILQTYStaking, Ownable, CheckContract, BaseMath { ^ (Relevant source part starts here and spans across multiple lines). contracts/TroveManager.sol:18:1: Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries. contract TroveManager is LiquityBase, /*Ownable,*/ CheckContract, ITroveManager { ^ (Relevant source part starts here and spans across multiple lines). contracts/SortedTroves.sol:46:1: Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries. contract SortedTroves is Ownable, CheckContract, ISortedTroves { ^ (Relevant source part starts here and spans across multiple lines). contracts/PriceFeed.sol:24:1: Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries. contract PriceFeed is Ownable, CheckContract, BaseMath, IPriceFeed { ^ (Relevant source part starts here and spans across multiple lines). contracts/Proxy/BorrowerWrappersScript.sol:21:1: Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries. contract BorrowerWrappersScript is BorrowerOperationsScript, ERC20TransferScript, LQTYStakingScript { ^ (Relevant source part starts here and spans across multiple lines). contracts/RedemptionHelper.sol:16:1: Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries. contract RedemptionHelper is LiquityBase, Ownable, IRedemptionHelper { ^ (Relevant source part starts here and spans across multiple lines). contracts/StabilityPool.sol:146:1: Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries. contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool { ^ (Relevant source part starts here and spans across multiple lines).
3 functions Ethos-Core/contracts/LUSDToken.sol: 132 133: function pauseMinting() external { 134: require( 135: msg.sender == guardianAddress || msg.sender == governanceAddress, 136: "LUSD: Caller is not guardian or governance" 137: ); 138: mintingPaused = true; 139: } 140: 141: function unpauseMinting() external { 142: _requireCallerIsGovernance(); 143: mintingPaused = false; 144: } 145 Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: 166 */ 167: function initiateUpgradeCooldown() external { 168: _atLeastRole(STRATEGIST); 169: upgradeProposalTime = block.timestamp; 170: }
uint256
instead uint
Project use uint and uint256
Number of uses: uint = 477 results – 8 files uint256 = 281 results - 11 files
Some developers prefer to use uint256
 because it is consistent with other uint data types, which also specify their size, and also because making the size of the data explicit reminds the developer and the reader how much data they've got to play with, which may help prevent or detect bugs.
For example if doing bytes4(keccak('transfer(address, uint)’))
, you'll get a different method sig ID than bytes4(keccak('transfer(address, uint256)’))
 and smart contracts will only understand the latter when comparing method sig IDs
Context:
Ethos-Core/contracts/LUSDToken.sol: 261 262: function permit // ... 283: bytes32 digest = keccak256(abi.encodePacked('\x19\x01', 284: domainSeparator(), keccak256(abi.encode( 285: _PERMIT_TYPEHASH, owner, spender, amount, 286: _nonces[owner]++, deadline)))); 287: address recoveredAddress = ecrecover(digest, v, r, s);
Description: Description: The function calls the Solidity ecrecover() function directly to verify the given signatures. However, the ecrecover() EVM opcode allows malleable (non-unique) signatures and thus is susceptible to replay attacks.
Although a replay attack seems not possible for this contract, I recommend using the battle-tested OpenZeppelin's ECDSA library.
Recommendation:* Use the ecrecover function from OpenZeppelin's ECDSA library for signature verification. (Ensure using a version > 4.7.3 for there was a critical bug >= 4.1.0 < 4.7.3).
sendCollateral
event is missing parametersThe ActivePool.sol
contract has very important function; sendCollateral
However, only amounts are published in emits, whereas msg.sender must be specified in every transaction, a contract or web page listening to events cannot react to users, emit does not serve the purpose. Basically, this event cannot be used
Ethos-Core/contracts/ActivePool.sol: 170 171: function sendCollateral(address _collateral, address _account, uint _amount) external override { 172: _requireValidCollateralAddress(_collateral); 173: _requireCallerIsBOorTroveMorSP(); 174: _rebalance(_collateral, _amount); 175: collAmount[_collateral] = collAmount[_collateral].sub(_amount); 176: emit ActivePoolCollateralBalanceUpdated(_collateral, collAmount[_collateral]); - 177: emit CollateralSent(_collateral, _account, _amount); + 177: emit CollateralSent(msg.sender_collateral, _account, _amount);
add msg.sender
parameter in event-emit
For defence-in-depth purpose, it is recommended to perform additional validation against the amount that the user is attempting to deposit, mint, withdraw and redeem to ensure that the submitted amount is valid.
OpenZeppelinTokenizedVault.sol#L9
Ethos-Vault/contracts/ReaperVaultV2.sol: 318 // shares to {_receiver}. Returns the number of shares that were minted. 319: function _deposit(uint256 _amount, address _receiver) internal nonReentrant returns (uint256 shares) { 320: _atLeastRole(DEPOSITOR); 321: require(!emergencyShutdown, "Cannot deposit during emergency shutdown"); 322: require(_amount != 0, "Invalid amount"); + require(shares <= maxMint(receiver), "mint more than max"); 323: uint256 pool = balance(); 324: require(pool + _amount <= tvlCap, "Vault is full"); 325: 326: uint256 freeFunds = _freeFunds(); 327: uint256 balBefore = token.balanceOf(address(this)); 328: token.safeTransferFrom(msg.sender, address(this), _amount); 329: uint256 balAfter = token.balanceOf(address(this)); 330: _amount = balAfter - balBefore; 331: if (totalSupply() == 0) { 332: shares = _amount; 333: } else { 334: shares = (_amount * totalSupply()) / freeFunds; // use "freeFunds" instead of "pool" 335: } 336: _mint(_receiver, shares); 337: emit Deposit(msg.sender, _receiver, _amount, shares); 338: }
0
On several locations in the code precautions are not being taken for not dividing by 0, this will revert the code. These functions can be calledd with 0 value in the input, this value is not checked for being bigger than 0, that means in some scenarios this can potentially trigger a division by zero.
Ethos-Vault/contracts/ReaperVaultV2.sol: 358 // and return corresponding assets to {_receiver}. Returns the number of assets that were returned. 359: function _withdraw( 360: uint256 _shares, 361: address _receiver, 362: address _owner 363: ) internal nonReentrant returns (uint256 value) { 364: require(_shares != 0, "Invalid amount"); - 365: value = (_freeFunds() * _shares) / totalSupply(); + if (totalSupply() == 0) { + revert Unauthorized("total supply is zero"); + } else { + value = (_freeFunds * _shares) / totalSupply(); + } 366: _burn(_owner, _shares);
@openzeppelin
Ethos-Vault/package.json: 39 }, 40: "dependencies": { 41: "@openzeppelin/contracts": "^4.7.3", 42: "@openzeppelin/contracts-upgradeable": "^4.7.3",
https://security.snyk.io/package/npm/@openzeppelin%2Fcontracts/
Upgrade OZ to version 4.8.0 or higher
There is a difference between constant variables and immutable variables, and they should each be used in their appropriate contexts.
While it doesn't save any gas because the compiler knows that developers often make this mistake, it's still best to use the right tool for the task at hand.
8 results - 2 files Ethos-Vault/contracts/ReaperVaultV2.sol: 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"); Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: 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");
ERC-4626 is a standard to optimize and unify the technical parameters of yield-bearing vaults. It provides a standard API for tokenized yield-bearing vaults that represent shares of a single underlying ERC-20 token. ERC-4626 also outlines an optional extension for tokenized vaults utilizing ERC-20, offering basic functionality for depositing, withdrawing tokens and reading balances.
ERC4626 Tokenized Vauldt was created 2021-12-22 so this standart is safer to use as upgrade
setAddresses
function, there is no return of incorrect address identificationIn case of incorrect address definition in the setAddresses
function, there is no way to fix it because of the owner = address(0)
at the end of the function
It is recommended to fix the architecture
1- Address definitions can be defined in the constructor
2- Because of owner = address(0)
at the end of the function, there is no way to fix it, so the owner's authority can be maintained.
Ethos-Core/contracts/TroveManager.sol: 231 232: function setAddresses( 233: address _borrowerOperationsAddress, 234: address _collateralConfigAddress, 235: address _activePoolAddress, 236: address _defaultPoolAddress, 237: address _stabilityPoolAddress, 238: address _gasPoolAddress, 239: address _collSurplusPoolAddress, 240: address _priceFeedAddress, 241: address _lusdTokenAddress, 242: address _sortedTrovesAddress, 243: address _lqtyTokenAddress, 244: address _lqtyStakingAddress, 245: address _redemptionHelperAddress 246: ) 247: external 248: override 249: { 250: require(msg.sender == owner); 251: 252: checkContract(_borrowerOperationsAddress); 253: checkContract(_collateralConfigAddress); 254: checkContract(_activePoolAddress); 255: checkContract(_defaultPoolAddress); 256: checkContract(_stabilityPoolAddress); 257: checkContract(_gasPoolAddress); 258: checkContract(_collSurplusPoolAddress); 259: checkContract(_priceFeedAddress); 260: checkContract(_lusdTokenAddress); 261: checkContract(_sortedTrovesAddress); 262: checkContract(_lqtyTokenAddress); 263: checkContract(_lqtyStakingAddress); 264: checkContract(_redemptionHelperAddress); 265: 266: borrowerOperationsAddress = _borrowerOperationsAddress; 267: collateralConfig = ICollateralConfig(_collateralConfigAddress); 268: activePool = IActivePool(_activePoolAddress); 269: defaultPool = IDefaultPool(_defaultPoolAddress); 270: stabilityPool = IStabilityPool(_stabilityPoolAddress); 271: gasPoolAddress = _gasPoolAddress; 272: collSurplusPool = ICollSurplusPool(_collSurplusPoolAddress); 273: priceFeed = IPriceFeed(_priceFeedAddress); 274: lusdToken = ILUSDToken(_lusdTokenAddress); 275: sortedTroves = ISortedTroves(_sortedTrovesAddress); 276: lqtyToken = IERC20(_lqtyTokenAddress); 277: lqtyStaking = ILQTYStaking(_lqtyStakingAddress); 278: redemptionHelper = IRedemptionHelper(_redemptionHelperAddress); 279: 280: emit BorrowerOperationsAddressChanged(_borrowerOperationsAddress); 281: emit CollateralConfigAddressChanged(_collateralConfigAddress); 282: emit ActivePoolAddressChanged(_activePoolAddress); 283: emit DefaultPoolAddressChanged(_defaultPoolAddress); 284: emit StabilityPoolAddressChanged(_stabilityPoolAddress); 285: emit GasPoolAddressChanged(_gasPoolAddress); 286: emit CollSurplusPoolAddressChanged(_collSurplusPoolAddress); 287: emit PriceFeedAddressChanged(_priceFeedAddress); 288: emit LUSDTokenAddressChanged(_lusdTokenAddress); 289: emit SortedTrovesAddressChanged(_sortedTrovesAddress); 290: emit LQTYTokenAddressChanged(_lqtyTokenAddress); 291: emit LQTYStakingAddressChanged(_lqtyStakingAddress); 292: emit RedemptionHelperAddressChanged(_redemptionHelperAddress); 293: 294: owner = address(0); 295: } 296
Throughout the codebase, events are generally emitted when sensitive changes are made to the contracts. However, some events are missing important parameters
The events should include the new value and old value where possible:
Ethos-Core/contracts/LQTY/CommunityIssuance.sol: 60 61: function setAddresses 62: ( 63: address _oathTokenAddress, 64: address _stabilityPoolAddress 65: ) 66: external 67: onlyOwner 68: override 69: { 70: require(!initialized, "issuance has been initialized"); 71: checkContract(_oathTokenAddress); 72: checkContract(_stabilityPoolAddress); 73: 74: OathToken = IERC20(_oathTokenAddress); 75: stabilityPoolAddress = _stabilityPoolAddress; 76: 77: initialized = true; 78: - 79: emit OathTokenAddressSet(_oathTokenAddress); - 80: emit StabilityPoolAddressSet(_stabilityPoolAddress); + 79: emit OathTokenAddressSet(old_oathTokenAddress , _oathTokenAddress); + 80: emit StabilityPoolAddressSet(old_ stabilityPoolAddress, _stabilityPoolAddress); 81: } Ethos-Vault/contracts/ReaperVaultV2.sol: 257 */ 258: function setWithdrawalQueue(address[] calldata _withdrawalQueue) external { 259: _atLeastRole(ADMIN); 260: uint256 queueLength = _withdrawalQueue.length; 261: require(queueLength != 0, "Queue must not be empty"); 262: 263: delete withdrawalQueue; 264: for (uint256 i = 0; i < queueLength; i = i.uncheckedInc()) { 265: address strategy = _withdrawalQueue[i]; 266: StrategyParams storage params = strategies[strategy]; 267: require(params.activation != 0, "Invalid strategy address"); 268: withdrawalQueue.push(strategy); 269: } - 270: emit UpdateWithdrawalQueue(_withdrawalQueue); + 270: emit UpdateWithdrawalQueue(oldWithdrawalQueue, _withdrawalQueue); 271: } Ethos-Core/contracts/ActivePool.sol: 137 138: function setYieldClaimThreshold(address _collateral, uint256 _threshold) external onlyOwner { 139: _requireValidCollateralAddress(_collateral); 140: yieldClaimThreshold[_collateral] = _threshold; - 141: emit YieldClaimThresholdUpdated(_collateral, _threshold); + 141: emit YieldClaimThresholdUpdated(oldCollateral, _collateral, _threshold); 142 }
Context: All project have 226 functions (without interfaces) but they have only 95 NatSpec’s
Description: It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability. https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
Recommendation: NatSpec comments should be increased in contracts
Function writing
that does not comply with the Solidity Style Guide
Context: All Contracts
Description: Order of Functions; ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html
Functions should be grouped according to their visibility and ordered:
constructor receive function (if exists) fallback function (if exists) external public internal private within a grouping, place the view and pure functions last
Context: All project have 118 returns variable (without interfaces) but they have only 20 @return NatSpec’s
Description: It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
Recommendation: Include return parameters in NatSpec comments
Recommendation Code Style: (from Uniswap3)
/// @notice Adds liquidity for the given recipient/tickLower/tickUpper position /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends /// on tickLower, tickUpper, the amount of liquidity, and the current price. /// @param recipient The address for which the liquidity will be created /// @param tickLower The lower tick of the position in which to add liquidity /// @param tickUpper The upper tick of the position in which to add liquidity /// @param amount The amount of liquidity to mint /// @param data Any data that should be passed through to the callback /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback function mint( address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external returns (uint256 amount0, uint256 amount1);
Using balanceOf for reward distribution means accidentally sent reward tokens to the contract will be distributed to all users who are eligible for rewards.
Examine and consider the trade-offs of using internal accounting instead of balanceOf(address(this) . Implement functions that allow to withdraw the accidentally sent tokens from the contracts.
Contex Ethos-Vault/contracts/ReaperVaultERC4626.sol
It can't be recovered if the tokens accidentally arrive at the contract address, which has happened to many popular projects, so I recommend adding a recovery code to your critical contracts.
Add this code:
/** * @notice Sends ERC20 tokens trapped in contract to external address * @dev Onlyowner is allowed to make this function call * @param account is the receiving address * @param externalToken is the token being sent * @param amount is the quantity being sent * @return boolean value indicating whether the operation succeeded. * */ function rescueERC20(address account, address externalToken, uint256 amount) public onlyOwner returns (bool) { IERC20(externalToken).transfer(account, amount); return true; } }
If a query or logic is repeated over many lines, using a modifier improves the readability and reusability of the code
13 results - 5 files Ethos-Core/contracts/ActivePool.sol: 92 checkContract(_collSurplusPoolAddress); 93: require(_treasuryAddress != address(0), "Treasury cannot be 0 address"); Ethos-Core/contracts/LUSDToken.sol: 311 function _transfer(address sender, address recipient, uint256 amount) internal { 312: assert(sender != address(0)); 313: assert(recipient != address(0)); 314 320 function _mint(address account, uint256 amount) internal { 321: assert(account != address(0)); 322 328 function _burn(address account, uint256 amount) internal { 329: assert(account != address(0)); 330 336 function _approve(address owner, address spender, uint256 amount) internal { 337: assert(owner != address(0)); 338: assert(spender != address(0)); 339 347 require( 348: _recipient != address(0) && Ethos-Core/contracts/TroveManager.sol: 293 294: owner = address(0); 295 } Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol: 167: require(step[0] != address(0)); 168: require(step[1] != address(0)); Ethos-Vault/contracts/ReaperVaultV2.sol: 151: require(_strategy != address(0), "Invalid strategy address"); 629: require(newTreasury != address(0), "Invalid address");
Context:
14 results - 12 files Ethos-Core/contracts/ActivePool.sol: 2 3: pragma solidity 0.6.11; 4 Ethos-Core/contracts/BorrowerOperations.sol: 2 3: pragma solidity 0.6.11; 4 Ethos-Core/contracts/CollateralConfig.sol: 2 3: pragma solidity 0.6.11; 4 Ethos-Core/contracts/LUSDToken.sol: 2 3: pragma solidity 0.6.11; 4 Ethos-Core/contracts/StabilityPool.sol: 2 3: pragma solidity 0.6.11; 4 Ethos-Core/contracts/TroveManager.sol: 2 3: pragma solidity 0.6.11; 4 Ethos-Core/contracts/LQTY/CommunityIssuance.sol: 2 3: pragma solidity 0.6.11; 4 Ethos-Core/contracts/LQTY/LQTYStaking.sol: 2 3: pragma solidity 0.6.11; 4 Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol: 2 3: pragma solidity ^0.8.0; 4 Ethos-Vault/contracts/ReaperVaultERC4626.sol: 2 3: pragma solidity ^0.8.0; 4 Ethos-Vault/contracts/ReaperVaultV2.sol: 2 3: pragma solidity ^0.8.0; 4 Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: 2 3: pragma solidity ^0.8.0; 4
Description: For security, it is best practice to use the latest Solidity version. For the security fix list in the versions; https://github.com/ethereum/solidity/blob/develop/Changelog.md
Recommendation:
Old version of Solidity is used (0.8.0 / 0.6.11)
, newer version can be used (0.8.17)
Context:
4 results - 2 files Ethos-Core/contracts/ActivePool.sol: 41: mapping (address => uint256) internal collAmount; // collateral => amount tracker 42: mapping (address => uint256) internal LUSDDebt; // collateral => corresponding debt tracker Ethos-Core/contracts/StabilityPool.sol: 167: mapping (address => uint256) internal collAmounts; // deposited collateral tracker 170: uint256 internal totalLUSDDeposits;
Description: The above codes don't follow Solidity's standard naming convention,
internal and private functions : the mixedCase format starting with an underscore (_mixedCase starting with an underscore)
Description: Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively. https://swcregistry.io/docs/SWC-103
Floating Pragma List:
4 results Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol: 2 3: pragma solidity ^0.8.0; 4 Ethos-Vault/contracts/ReaperVaultERC4626.sol: 2 3: pragma solidity ^0.8.0; 4 Ethos-Vault/contracts/ReaperVaultV2.sol: 2 3: pragma solidity ^0.8.0; 4 Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: 2 3: pragma solidity ^0.8.0; 4
Recommendation: Lock the pragma version and also consider known bugs (https://github.com/ethereum/solidity/releases) for the compiler version that is chosen.
This codebase will be difficult to navigate, as there are no descriptive naming conventions that specify which files should contain meaningful logic.
Prefixes should be added like this by filing:
Interface I_ absctract contracts Abs_ Libraries Lib_
We recommend that you implement this or a similar agreement.
The highest tier of smart contract behavior assurance is formal mathematical verification. All assertions that are made are guaranteed to be true across all inputs → The quality of your asserts is the quality of your verification.
https://twitter.com/0xOwenThurm/status/1614359896350425088?t=dbG9gHFigBX85Rv29lOjIQ&s=19
Description: Add NatSpec comments describing mapping keys and values
Context:
13 results - 4 files Ethos-Core/contracts/CollateralConfig.sol: 35: mapping (address => Config) public collateralConfig; Ethos-Core/contracts/LUSDToken.sol: 54: mapping (address => uint256) private _nonces; 57: mapping (address => uint256) private _balances; 58: mapping (address => mapping (address => uint256)) private _allowances; 62: mapping (address => bool) public troveManagers; 63: mapping (address => bool) public stabilityPools; 64: mapping (address => bool) public borrowerOperations; Ethos-Core/contracts/StabilityPool.sol: 167: mapping (address => uint256) internal collAmounts; // deposited collateral tracker 179: mapping (address => uint) S; 223: mapping (uint128 => mapping(uint128 => uint)) public epochToScaleToG; 228: mapping (address => uint) public lastCollateralError_Offset; Ethos-Core/contracts/LQTY/LQTYStaking.sol: 35: mapping (address => uint) F_Collateral_Snapshot;
Recommendation:
/// @dev address(user) -> address(ERC0 Contract Address) -> uint256(allowance amount from user) mapping(address => mapping(address => uint256)) public allowance;
2 results - 2 files Ethos-Core/contracts/LQTY/CommunityIssuance.sol: 57 constructor() public { - 58: distributionPeriod = 14 days; + 58: distributionPeriod = 14 days; // 1_209_600 (14*24*60*60) 59 } Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: - 24 uint256 public constant UPGRADE_TIMELOCK = 48 hours; // minimum 48 hours for RF + //minimum 48 hours for RF + 24 uint256 public constant UPGRADE_TIMELOCK = 48 hours; // 172_800 (48*60*60) - 25: uint256 public constant FUTURE_NEXT_PROPOSAL_TIME = 365 days * 100; // 3_153_600_000 (365*24*60*60*100)
Description: It is generally recommended that lines in the source code should not exceed 80-120 characters. Today's screens are much larger, so in some cases it makes sense to expand that.The lines above should be split when they reach that length, as the files will most likely be on GitHub and GitHub always uses a scrollbar when the length is more than 164 characters.
(why-is-80-characters-the-standard-limit-for-code-width)[https://softwareengineering.stackexchange.com/questions/148677/why-is-80-characters-the-standard-limit-for-code-width]
Recommendation: Multiline output parameters and return statements should follow the same style recommended for wrapping long lines found in the Maximum Line Length section.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html#introduction
thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3 );
If you use the constant first you support structures that veil programming errors. And one should only produce code either to add functionality, fix an programming error or trying to support structures to avoid programming errors (like design patterns).
https://www.moserware.com/2008/01/constants-on-left-are-better-but-this.html
36 results - 6 files Ethos-Core/contracts/BorrowerOperations.sol: 534: require(_collTopUp == 0 || _collWithdrawal == 0, "BorrowerOperations: Cannot withdraw and add coll"); 568: require(_collWithdrawal == 0, "BorrowerOps: Collateral withdrawal not permitted Recovery Mode"); Ethos-Core/contracts/StabilityPool.sol: 428: if (totalLUSD == 0 || _LQTYIssuance == 0) {return;} 469: if (totalLUSD == 0 || _debtToOffset == 0) { return; } 489: if (totalLUSD == 0) { return; } 575: if (newProductFactor == 0) { 685: if (initialDeposit == 0) {return 0;} 720: if (initialDeposit == 0) { return 0; } 751: if (scaleDiff == 0) { 784: if (_amount == 0) {return;} 795: if (LUSDWithdrawal == 0) {return;} 809: if (_newValue == 0) { Ethos-Core/contracts/TroveManager.sol: 616: if (vars.ICR >= vars.collMCR && vars.remainingLUSDInStabPool == 0) { break; } 817: if (vars.ICR >= vars.collMCR && vars.remainingLUSDInStabPool == 0) { continue; } 1127: if ( rewardPerUnitStaked == 0 || Troves[_borrower][_collateral].status != Status.active) { return 0; } 1142: if ( rewardPerUnitStaked == 0 || Troves[_borrower][_collateral].status != Status.active) { return 0; } 1215: if (totalCollateralSnapshot[_collateral] == 0) { 1238: if (_debt == 0) { return; } Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol: 121: if (amount == 0) { Ethos-Vault/contracts/ReaperVaultERC4626.sol: 52: if (totalSupply() == 0 || _freeFunds() == 0) return assets; 67: if (totalSupply() == 0) return shares; 140: if (totalSupply() == 0) return shares; 184: if (totalSupply() == 0 || _freeFunds() == 0) return 0; Ethos-Vault/contracts/ReaperVaultV2.sol: 152: require(strategies[_strategy].activation == 0, "Strategy already added"); 210: if (strategies[_strategy].allocBPS == 0) { 227: if (totalAllocBPS == 0 || emergencyShutdown) { 296: return totalSupply() == 0 ? 10**decimals() : (_freeFunds() * 10**decimals()) / totalSupply(); 331: if (totalSupply() == 0) { 380: if (strategyBal == 0) { 466: uint256 shares = supply == 0 ? performanceFee : (performanceFee * supply) / _freeFunds(); 555: if (strategy.allocBPS == 0 || emergencyShutdown) {
constants
should be defined rather than using magic numbersA magic number is a numeric literal that is used in the code without any explanation of its meaning. The use of magic numbers makes programs less readable and hence more difficult to maintain and update. Even assembly can benefit from using readable constants instead of hex/numeric literals
3 result Ethos-Core/contracts/TroveManager.sol: 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% Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: 25: uint256 public constant FUTURE_NEXT_PROPOSAL_TIME = 365 days * 100;
1 result - 1 file Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: 155 */ 156: function setEmergencyExit() external { 157: _atLeastRole(GUARDIAN); 158: emergencyExit = true; 159: IVault(vault).revokeStrategy(address(this)); 160: }
delete
keyword instead of assigning a value of 0
Using the 'delete' keyword instead of assigning a '0' value is a detailed optimization that increases code readability and audit quality, and clearly indicates the intent.
Other hand, if use delete
instead 0
value assign , it will be gas saved
17 results - 4 files Ethos-Core/contracts/ActivePool.sol: 253: vars.profit = 0; Ethos-Core/contracts/StabilityPool.sol: 529: lastLUSDLossError_Offset = 0; 578: currentScale = 0; 756: compoundedDeposit = 0; Ethos-Core/contracts/TroveManager.sol: 387: singleLiquidation.debtToOffset = 0; 388: singleLiquidation.collToSendToSP = 0; 468: debtToOffset = 0; 469: collToSendToSP = 0; 505: singleLiquidation.debtToRedistribute = 0; 506: singleLiquidation.collToRedistribute = 0; 1192: Troves[_borrower][_collateral].stake = 0; 1285: Troves[_borrower][_collateral].coll = 0; 1286: Troves[_borrower][_collateral].debt = 0; 1288: rewardSnapshots[_borrower][_collateral].collAmount = 0; 1289: rewardSnapshots[_borrower][_collateral].LUSDDebt = 0; Ethos-Vault/contracts/ReaperVaultV2.sol: 215: strategies[_strategy].allocBPS = 0; 537: lockedProfit = 0;
2 results Ethos-Core/contracts/StabilityPool.sol: 626: function getDepositorCollateralGain(address _depositor) public view override returns (address[] memory assets, uint[] memory amounts) { 627: uint initialDeposit = deposits[_depositor].initialValue; 628: 629: if (initialDeposit != 0) { 630: Snapshots storage snapshots = depositSnapshots[_depositor]; 631: 632: return _getCollateralGainFromSnapshots(initialDeposit, snapshots); 633: } Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol: 103: 104: function _liquidateAllPositions() internal override returns (uint256 amountFreed) { 105: _withdrawUnderlying(balanceOfPool()); 106: return balanceOfWant(); 107: }
Recommendation: Consider adopting a consistent approach to return values throughout the codebase by removing all named return variables, explicitly declaring them as local variables, and adding the necessary return statements where appropriate. This would improve both the explicitness and readability of the code, and it may also help reduce regressions during future code refactors.
Ethos-Core/contracts/TroveManager.sol: 715: function batchLiquidateTroves(address _collateral, address[] memory _troveArray) public override { + require(memory _troveArray.length< max troveArrayLengt, "max length"); 716: require(_troveArray.length != 0); 717: 718: IActivePool activePoolCached = activePool; 719: IDefaultPool defaultPoolCached = defaultPool; 720: IStabilityPool stabilityPoolCached = stabilityPool; 721: 722: LocalVariables_OuterLiquidationFunction memory vars; 723: LiquidationTotals memory totals;
Recommendation: Function calls made in unbounded loop are error-prone with potential resource exhaustion as it can trap the contract due to gas limitations or failed transactions. Consider bounding the loop length if the array is expected to be growing and/or handling a huge list of elements to avoid unnecessary gas wastage and denial of service.
mapping ()
instead of mapping()
using spaces as keyword2 results - 2 files Ethos-Core/contracts/LQTY/LQTYStaking.sol: - 25: mapping( address => uint) public stakes; + 25: mapping ( address => uint) public stakes; Ethos-Vault/contracts/ReaperVaultV2.sol: - 35: mapping(address => StrategyParams) public strategies; + 35: mapping (address => StrategyParams) public strategies;
Context:
110 results - 12 files Ethos-Core/contracts/ActivePool.sol: 4 5: import './Interfaces/IActivePool.sol'; 6: import "./Interfaces/ICollateralConfig.sol"; 7: import './Interfaces/IDefaultPool.sol'; 8: import "./Interfaces/ICollSurplusPool.sol"; 9: import "./Interfaces/ILQTYStaking.sol"; 10: import "./Interfaces/IStabilityPool.sol"; 11: import "./Interfaces/ITroveManager.sol"; 12: import "./Dependencies/SafeMath.sol"; 13: import "./Dependencies/Ownable.sol"; 14: import "./Dependencies/CheckContract.sol"; 15: import "./Dependencies/console.sol"; 16: import "./Dependencies/SafeERC20.sol"; 17: import "./Dependencies/IERC4626.sol"; 18 Ethos-Core/contracts/BorrowerOperations.sol: 4 5: import "./Interfaces/IBorrowerOperations.sol"; 6: import "./Interfaces/ICollateralConfig.sol"; 7: import "./Interfaces/ITroveManager.sol"; 8: import "./Interfaces/ILUSDToken.sol"; 9: import "./Interfaces/ICollSurplusPool.sol"; 10: import "./Interfaces/ISortedTroves.sol"; 11: import "./Interfaces/ILQTYStaking.sol"; 12: import "./Dependencies/LiquityBase.sol"; 13: import "./Dependencies/Ownable.sol"; 14: import "./Dependencies/CheckContract.sol"; 15: import "./Dependencies/console.sol"; 16: import "./Dependencies/SafeERC20.sol"; 17 Ethos-Core/contracts/CollateralConfig.sol: 4 5: import "./Dependencies/CheckContract.sol"; 6: import "./Dependencies/Ownable.sol"; 7: import "./Dependencies/SafeERC20.sol"; 8: import "./Interfaces/ICollateralConfig.sol"; 9 Ethos-Core/contracts/LUSDToken.sol: 4 5: import "./Interfaces/ILUSDToken.sol"; 6: import "./Interfaces/ITroveManager.sol"; 7: import "./Dependencies/SafeMath.sol"; 8: import "./Dependencies/CheckContract.sol"; 9: import "./Dependencies/console.sol"; Ethos-Core/contracts/StabilityPool.sol: 4 5: import './Interfaces/IBorrowerOperations.sol'; 6: import "./Interfaces/ICollateralConfig.sol"; 7: import './Interfaces/IStabilityPool.sol'; 8: import './Interfaces/IBorrowerOperations.sol'; 9: import './Interfaces/ITroveManager.sol'; 10: import './Interfaces/ILUSDToken.sol'; 11: import './Interfaces/ISortedTroves.sol'; 12: import "./Interfaces/ICommunityIssuance.sol"; 13: import "./Dependencies/LiquityBase.sol"; 14: import "./Dependencies/SafeMath.sol"; 15: import "./Dependencies/LiquitySafeMath128.sol"; 16: import "./Dependencies/Ownable.sol"; 17: import "./Dependencies/CheckContract.sol"; 18: import "./Dependencies/console.sol"; 19: import "./Dependencies/SafeERC20.sol"; 20 Ethos-Core/contracts/TroveManager.sol: 4 5: import "./Interfaces/ICollateralConfig.sol"; 6: import "./Interfaces/ITroveManager.sol"; 7: import "./Interfaces/IStabilityPool.sol"; 8: import "./Interfaces/ICollSurplusPool.sol"; 9: import "./Interfaces/ILUSDToken.sol"; 10: import "./Interfaces/ISortedTroves.sol"; 11: import "./Interfaces/ILQTYStaking.sol"; 12: import "./Interfaces/IRedemptionHelper.sol"; 13: import "./Dependencies/LiquityBase.sol"; 14: // import "./Dependencies/Ownable.sol"; 15: import "./Dependencies/CheckContract.sol"; 16: import "./Dependencies/IERC20.sol"; 17 Ethos-Core/contracts/LQTY/CommunityIssuance.sol: 4 5: import "../Dependencies/IERC20.sol"; 6: import "../Interfaces/ICommunityIssuance.sol"; 7: import "../Dependencies/BaseMath.sol"; 8: import "../Dependencies/LiquityMath.sol"; 9: import "../Dependencies/Ownable.sol"; 10: import "../Dependencies/CheckContract.sol"; 11: import "../Dependencies/SafeMath.sol"; 12 Ethos-Core/contracts/LQTY/LQTYStaking.sol: 4 5: import "../Dependencies/BaseMath.sol"; 6: import "../Dependencies/SafeMath.sol"; 7: import "../Dependencies/Ownable.sol"; 8: import "../Dependencies/CheckContract.sol"; 9: import "../Dependencies/console.sol"; 10: import "../Dependencies/IERC20.sol"; 11: import "../Interfaces/ICollateralConfig.sol"; 12: import "../Interfaces/ILQTYStaking.sol"; 13: import "../Interfaces/ITroveManager.sol"; 14: import "../Dependencies/LiquityMath.sol"; 15: import "../Interfaces/ILUSDToken.sol"; 16: import "../Dependencies/SafeERC20.sol"; 17 Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol: 4 5: import "./abstract/ReaperBaseStrategyv4.sol"; 6: import "./interfaces/IAToken.sol"; 7: import "./interfaces/IAaveProtocolDataProvider.sol"; 8: import "./interfaces/ILendingPool.sol"; 9: import "./interfaces/ILendingPoolAddressesProvider.sol"; 10: import "./interfaces/IRewardsController.sol"; 11: import "./libraries/ReaperMathUtils.sol"; 12: import "./mixins/VeloSolidMixin.sol"; 13: import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 14: import "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; 15 Ethos-Vault/contracts/ReaperVaultERC4626.sol: 4 5: import "./ReaperVaultV2.sol"; 6: import "./interfaces/IERC4626Functions.sol"; 7 Ethos-Vault/contracts/ReaperVaultV2.sol: 4 5: import "./interfaces/IERC4626Events.sol"; 6: import "./interfaces/IStrategy.sol"; 7: import "./libraries/ReaperMathUtils.sol"; 8: import "./mixins/ReaperAccessControl.sol"; 9: import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; 10: import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 11: import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 12: import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 13: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 14: import "@openzeppelin/contracts/utils/math/Math.sol"; 15 Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: 4 5: import "../interfaces/IStrategy.sol"; 6: import "../interfaces/IVault.sol"; 7: import "../libraries/ReaperMathUtils.sol"; 8: import "../mixins/ReaperAccessControl.sol"; 9: import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; 10: import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 11: import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 12: import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 13
Description:
Solidity code is also cleaner in another way that might not be noticeable: the struct Point. We were importing it previously with global import but not using it. The Point struct polluted the source code
with an unnecessary object we were not using because we did not need it.
This was breaking the rule of modularity and modular programming: only import what you need
Specific imports with curly braces allow us to apply this rule better.
Recommendation:
import {contract1 , contract2} from "filename.sol";
A good example from the ArtGobblers project;
import {Owned} from "solmate/auth/Owned.sol"; import {ERC721} from "solmate/tokens/ERC721.sol"; import {LibString} from "solmate/utils/LibString.sol"; import {MerkleProofLib} from "solmate/utils/MerkleProofLib.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {ERC1155, ERC1155TokenReceiver} from "solmate/tokens/ERC1155.sol"; import {toWadUnsafe, toDaysWadUnsafe} from "solmate/utils/SignedWadMath.sol";
require
instead of assert
20 results - 4 files Ethos-Core/contracts/BorrowerOperations.sol: 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); Ethos-Core/contracts/LUSDToken.sol: 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)); Ethos-Core/contracts/StabilityPool.sol: 526: assert(_debtToOffset <= _totalLUSDDeposits); 551: assert(_LUSDLossPerUnitStaked <= DECIMAL_PRECISION); 591: assert(newP > 0); Ethos-Core/contracts/TroveManager.sol: 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
Description:
Assert should not be used except for tests, require
should be used
Prior to Solidity 0.8.0, pressing a confirm consumes the remainder of the process's available gas instead of returning it, as request()/revert() did.
assert() and ruqire();
The big difference between the two is that the assert()
function when false, uses up all the remaining gas and reverts all the changes made.
Meanwhile, a require()
function when false, also reverts back all the changes made to the contract but does refund all the remaining gas fees we offered to pay.
This is the most common Solidity function used by developers for debugging and error handling.
Assertion() should be avoided even after solidity version 0.8.0, because its documentation states "The Assert function generates an error of type Panic(uint256). Code that works properly should never Panic, even on invalid external input. If this happens, you need to fix it in your contract. there's a mistake".
As part of the upgradeability of Proxies , the contract can be upgraded multiple times, where it is a systematic approach to record the version of each upgrade.
Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: 189: function _authorizeUpgrade(address) internal override { 190: _atLeastRole(DEFAULT_ADMIN_ROLE); 191: require( 192: upgradeProposalTime + UPGRADE_TIMELOCK < block.timestamp, 193: "Upgrade cooldown not initiated or still ongoing" 194: ); 195: clearUpgradeCooldown(); 196: }
I suggest implementing some kind of version counter that will be incremented automatically when you upgrade the contract.
Recommendation:
Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: + uint256 public authorizeUpgradeCounter; 189: function _authorizeUpgrade(address) internal override { 190: _atLeastRole(DEFAULT_ADMIN_ROLE); 191: require( 192: upgradeProposalTime + UPGRADE_TIMELOCK < block.timestamp, 193: "Upgrade cooldown not initiated or still ongoing" 194: ); + authorizeUpgradeCounter+=1; 195: clearUpgradeCooldown(); 196: }
There are many addresses and constants used in the system. It is recommended to put the most used ones in one file (for example constants.sol, use inheritance to access these values)
This will help with readability and easier maintenance for future changes. This also helps with any issues, as some of these hard-coded values are admin addresses.
constants.sol Use and import this file in contracts that require access to these values. This is just a suggestion, in some use cases this may result in higher gas usage in the distribution.
18 results - 4 files Ethos-Core/contracts/StabilityPool.sol: 197: uint public constant SCALE_FACTOR = 1e9; Ethos-Vault/contracts/ReaperVaultV2.sol: 40: uint256 public constant DEGRADATION_COEFFICIENT = 10**18; // The unit for calculating profit degradation. 41: uint256 public constant PERCENT_DIVISOR = 10000; 42 uint256 public tvlCap; 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"); 77 Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: 22 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");
Since this is a low level language that is more difficult to parse by readers, include extensive documentation, comments on the rationale behind its use, clearly explaining what each assembly instruction does
This will make it easier for users to trust the code, for reviewers to validate the code, and for developers to build on or update the code.
Note that using Aseembly removes several important security features of Solidity, which can make the code more insecure and more error-prone.
Ethos-Core/contracts/LUSDToken.sol: 297 298: function _chainID() private pure returns (uint256 chainID) { 299: assembly { 300: chainID := chainid() 301: } 302: }
Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol: 189: function _authorizeUpgrade(address) internal override { 190: _atLeastRole(DEFAULT_ADMIN_ROLE); 191: require( 192: upgradeProposalTime + UPGRADE_TIMELOCK < block.timestamp, 193: "Upgrade cooldown not initiated or still ongoing" 194: ); 195: clearUpgradeCooldown(); 196: }
Description: Although the UUPS model has many advantages and the proposed proxy model is currently the UUPS model, there are some caveats we should be aware of before applying it to a real-world project.
One of the main attention: Since upgrades are done through the implementation contract with the help of the
_authorizeUpgrade
method, there is a high risk of new implementations such as excluding the _authorizeUpgrade
method, which can permanently kill the smart contract's ability to upgrade. Also, this pattern is somewhat complicated to implement compared to other proxy patterns.
Recommendation: Therefore, this highest risk must be found in NatSpec comments and documents.
The Proxy contract could not be seen in the checklist because it is an upgradable contract, only the implementation contracts are visible, I recommend including the Proxy contract in the audit for integrity in the audit.
Description: I recommend using header for Solidity code layout and readability
https://github.com/transmissions11/headers
/*////////////////////////////////////////////////////////////// TESTING 123 //////////////////////////////////////////////////////////////*/
#0 - c4-judge
2023-03-09T18:09:25Z
trust1995 marked the issue as grade-a
#1 - trust1995
2023-03-10T17:42:49Z
Excellent report, top 3 QA
#2 - c4-sponsor
2023-03-28T21:14:01Z
0xBebis marked the issue as sponsor confirmed
🌟 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
308.7866 USDC - $308.79
Number | Optimization Details | Context |
---|---|---|
[G-01] | Remove the initializer modifier | 2 |
[G-02] | Use hardcode address instead address(this) | 36 |
[G-03] | Structs can be packed into fewer storage slots | 2 |
[G-04] | Pack state variables | 4 |
[G-05] | Remove or replace unused state variables | 1 |
[G-06] | Deposit struct can be removed | 1 |
[G-07] | Using delete instead of setting mapping/state variable 0 saves gas | 9 |
[G-08] | Using delete instead of setting address(0) saves gas | 1 |
[G-09] | Using delete to set bool values to their default values saves gas | 3 |
[G-10] | Using storage instead of memory for structs/arrays saves gas | 7 |
[G-11] | Multiple address /ID mappings can be combined into a single mapping of an address /ID to a struct , where appropriate | 7 |
[G-12] | Multiple accesses of a mapping/array should use a local variable cache | 30 |
[G-13] | The result of function calls should be cached rather than re-calling the function | 4 |
[G-14] | Avoid using state variable in emit | 1 |
[G-15] | Superfluous event fields | 1 |
[G-16] | Avoid contract existence checks by using low level calls | 97 |
[G-17] | bytes constants are more eficient than string constans | 8 |
[G-18] | Change public function visibility to external | 3 |
[G-19] | Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead | 6 |
[G-20] | internal functions only called once can be inlined to save gas | 45 |
[G-21] | Do not calculate constants variables | 11 |
[G-22] | Use assembly to write address storage values | 18 |
[G-23] | Setting the constructor to payable | 6 |
[G-24] | Add unchecked {} for subtractions where the operands cannot underflow because of a previous require or if statement | 10 |
[G-25] | ++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-loops | 13 |
[G-26] | x += y (x -= y) costs more gas than x = x + y (x = x - y) for state variables | 9 |
[G-27] | Use require instead of assert | 20 |
[G-28] | Use nested if and, avoid multiple check combinations | 3 |
[G-29] | Use double require instead of using && | 4 |
[G-30] | Sort Solidity operations using short-circuit mode | 14 |
[G-31] | Optimize names to save gas | All contracts |
[G-32] | Use a more recent version of solidity | 12 |
[G-33] | >= costs less gas than > | 1 |
[G-34] | Upgrade Solidity's optimizer |
Total 34 issues
initializer
modifierif we can just ensure that the initialize()
function could only be called from within the constructor, we shouldn't need to worry about it getting called again.
2 results - 2 files:
Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol: 67: ) public initializer {
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: 61: constructor() initializer {}
In the EVM, the constructor's job is actually to return the bytecode that will live at the contract's address. So, while inside a constructor, your address (address(this))
will be the deployment address, but there will be no bytecode at that address! So if we check address(this).code.length
before the constructor has finished, even from within a delegatecall, we will get 0. So now let's update our initialize()
function to only run if we are inside a constructor:
Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol: - 67: ) public initializer { + require(address(this).code.length == 0, 'not in constructor’);
Now the Proxy contract's constructor can still delegatecall initialize(), but if anyone attempts to call it again (after deployment) through the Proxy instance, or tries to call it directly on the above instance, it will revert because address(this).code.length will be nonzero.
Also, because we no longer need to write to any state to track whether initialize() has been called, we can avoid the 20k storage gas cost. In fact, the cost for checking our own code size is only 2 gas, which means we have a 10,000x gas savings over the standard version. Pretty neat!
address(this)
Instead of address(this)
, it is more gas-efficient to pre-calculate and use the address with a hardcode. Foundry's script.sol
and solmate````LibRlp.sol`` contracts can do this.
Reference: https://book.getfoundry.sh/reference/forge-std/compute-create-address https://twitter.com/transmissions11/status/1518507047943245824
36 results - 9 files:
Ethos-Core\contracts\ActivePool.sol: 210: IERC20(_collateral).safeTransferFrom(msg.sender, address(this), _amount); 247: vars.ownedShares = vars.yieldGenerator.balanceOf(address(this)); 280: IERC4626(yieldGenerator[_collateral]).deposit(uint256(vars.netAssetMovement), address(this)); 282: IERC4626(yieldGenerator[_collateral]).withdraw(uint256(-vars.netAssetMovement), address(this), address(this)); 288: vars.profit = IERC20(_collateral).balanceOf(address(this)).add(vars.yieldingAmount).sub(collAmount[_collateral]);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L210
Ethos-Core\contracts\BorrowerOperations.sol: 231: IERC20(_collateral).safeTransferFrom(msg.sender, address(this), _collAmount); 497: IERC20(_collateral).safeTransferFrom(msg.sender, address(this), _collChange); 530: require(IERC20(_collateral).allowance(_user, address(this)) >= _collAmount, "BorrowerOperations: Insufficient collateral allowance");
Ethos-Core\contracts\LUSDToken.sol: 305: return keccak256(abi.encode(typeHash, name, version, _chainID(), address(this))); 349: _recipient != address(this),
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L305
Ethos-Core\contracts\StabilityPool.sol: 605: lusdToken.burn(address(this), _debtToOffset); 606 607: activePoolCached.sendCollateral(_collateral, address(this), _collToAdd); 777: lusdToken.sendToPool(_address, address(this), _amount); 797: lusdToken.returnFromPool(address(this), _depositor, LUSDWithdrawal);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L605
Ethos-Core\contracts\LQTY\CommunityIssuance.sol: 103: OathToken.transferFrom(msg.sender, address(this), amount);
Ethos-Core\contracts\LQTY\LQTYStaking.sol: 128: lqtyToken.safeTransferFrom(msg.sender, address(this), _LQTYamount);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/LQTYStaking.sol#L128
Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol: 120: uint256 amount = startToken.balanceOf(address(this)); 127: uint256 allocated = IVault(vault).strategies(address(this)).allocated; 180: ILendingPool(lendingPoolAddress).deposit(want, toReinvest, address(this), LENDER_REFERRAL_CODE_NONE); 200: ILendingPool(ADDRESSES_PROVIDER.getLendingPool()).withdraw(address(want), _withdrawAmount, address(this)); 227: return IERC20Upgradeable(want).balanceOf(address(this)); 233: address(this)
Ethos-Vault\contracts\ReaperVaultV2.sol: 153: require(address(this) == IStrategy(_strategy).vault(), "Strategy's vault does not match"); 246: available = Math.min(available, token.balanceOf(address(this))); 279: return token.balanceOf(address(this)) + totalAllocated; 327: uint256 balBefore = token.balanceOf(address(this)); 328: token.safeTransferFrom(msg.sender, address(this), _amount); 329: uint256 balAfter = token.balanceOf(address(this)); 368: if (value > token.balanceOf(address(this))) { 373: vaultBalance = token.balanceOf(address(this)); 386: uint256 actualWithdrawn = token.balanceOf(address(this)) - vaultBalance; 399: vaultBalance = token.balanceOf(address(this)); 528: token.safeTransferFrom(vars.stratAddr, address(this), vars.freeWantInStrat - vars.credit); 641: uint256 amount = IERC20Metadata(_token).balanceOf(address(this));
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L153
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: 159: IVault(vault).revokeStrategy(address(this));
uint64
is suggested for uint256 activation
and```uint256 lastReport`` considering that it will be valid for the next 532 years.1 slot win 1 * 2k = 2k gas saved
Ethos-Vault\contracts\ReaperVaultV2.sol: 25: struct StrategyParams { 26: uint256 activation; // Activation block.timestamp 27: uint256 feeBPS; // Performance fee taken from profit, in BPS 28: uint256 allocBPS; // Allocation in BPS of vault's total assets 29: uint256 allocated; // Amount of capital allocated to this strategy 30: uint256 gains; // Total returns that Strategy has realized for Vault 31: uint256 losses; // Total losses that Strategy has realized for Vault 32: uint256 lastReport; // block.timestamp of the last time a report occured 33: }
Ethos-Vault\contracts\ReaperVaultV2.sol: 25: struct StrategyParams { - 26: uint256 activation; // Activation block.timestamp + 26: uint64 activation; // Activation block.timestamp + 32: uint64 lastReport; // block.timestamp of the last time a report occured 27: uint256 feeBPS; // Performance fee taken from profit, in BPS 28: uint256 allocBPS; // Allocation in BPS of vault's total assets 29: uint256 allocated; // Amount of capital allocated to this strategy 30: uint256 gains; // Total returns that Strategy has realized for Vault 31: uint256 losses; // Total losses that Strategy has realized for Vault - 32: uint256 lastReport; // block.timestamp of the last time a report occured 33: }
1 slot win 1 * 2k = 2k gas saved
Ethos-Core\contracts\CollateralConfig.sol: 27: struct Config { 28: bool allowed; 29: uint256 decimals; 30: uint256 MCR; 31: uint256 CCR; 32: }
Ethos-Core\contracts\CollateralConfig.sol: 27: struct Config { 28: bool allowed; - 29: uint256 decimals; + 29: uint128 decimals; 30: uint256 MCR; 31: uint256 CCR; 32: }
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: 31: uint256 public lastHarvestTimestamp; 33: uint256 public upgradeProposalTime;
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: - 31: uint256 public lastHarvestTimestamp; + 31: uint64 public lastHarvestTimestamp; - 33: uint256 public upgradeProposalTime; + 33: uint64 public upgradeProposalTime;
Ethos-Core\contracts\ActivePool.sol: 49: uint256 public yieldingPercentageDrift = 100; // rebalance iff % is off by more than 100 BPS 50: 51: // Yield distribution params, must add up to 10k 52: uint256 public yieldSplitTreasury = 20_00; // amount of yield to direct to treasury in BPS 53: uint256 public yieldSplitSP = 40_00; // amount of yield to direct to stability pool in BPS 54: uint256 public yieldSplitStaking = 40_00; // amount of yield to direct to OATH Stakers in BPS
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L49-L54
The state variable yieldingPercentageDrift
can be updated to a maximum value of 500 as seen in function setYieldingPercentageDrift
.
Ethos-Core\contracts\ActivePool.sol: 132 function setYieldingPercentageDrift(uint256 _driftBps) external onlyOwner { 133: require(_driftBps <= 500, "Exceeds max allowed value of 500 BPS"); 134: yieldingPercentageDrift = _driftBps;
On the other hand, `yieldSplitTreasury,
yieldSplitSPand The
yieldSplitStaking`` state variables can total up to 10k.
51: // Yield distribution params, must add up to 10k
Given this information, these state variables can be tightly packed into 1 slot instead of 4 if updated as shown below.
Ethos-Core\contracts\ActivePool.sol: - 49: uint256 public yieldingPercentageDrift = 100; // rebalance iff % is off by more than 100 BPS + 49: uint64 public yieldingPercentageDrift = 100; // rebalance iff % is off by more than 100 BPS 51: // Yield distribution params, must add up to 10k - 52: uint256 public yieldSplitTreasury = 20_00; // amount of yield to direct to treasury in BPS + 52: uint64 public yieldSplitTreasury = 20_00; // amount of yield to direct to treasury in BPS - 53: uint256 public yieldSplitSP = 40_00; // amount of yield to direct to stability pool in BPS + 53: uint64 public yieldSplitSP = 40_00; // amount of yield to direct to stability pool in BPS - 54: uint256 public yieldSplitStaking = 40_00; // amount of yield to direct to OATH Stakers in BPS + 54: uint64 public yieldSplitStaking = 40_00; // amount of yield to direct to OATH Stakers in BPS
Ethos-Vault\contracts\ReaperVaultV2.sol: 35: mapping(address => StrategyParams) public strategies; 38: address[] public withdrawalQueue; 42: uint256 public tvlCap; 43: 44: uint256 public totalAllocBPS; // Sum of allocBPS across all strategies (in BPS, <= 10k) 45: uint256 public totalAllocated; // Amount of tokens that have been allocated to all strategies 46: uint256 public lastReport; // block.timestamp of last report from any strategy 49: bool public emergencyShutdown; 54: // Max slippage(loss) allowed when withdrawing, in BPS (0.01%) 55: uint256 public withdrawMaxLoss = 1; 56: uint256 public lockedProfitDegradation; // rate per block of degradation. DEGRADATION_COEFFICIENT is 100% per block 57: uint256 public lockedProfit; // how much profit is locked and cant be withdrawn 78: address public treasury; // address to whom performance fee is remitted in the form of vault shares
Ethos-Vault\contracts\ReaperVaultV2.sol: 35: mapping(address => StrategyParams) public strategies; 38: address[] public withdrawalQueue; 42: uint256 public tvlCap; + 49: bool public emergencyShutdown; - 44: uint256 public totalAllocBPS; // Sum of allocBPS across all strategies (in BPS, <= 10k) + 44: uint192 public totalAllocBPS; // Sum of allocBPS across all strategies (in BPS, <= 10k) 45: uint256 public totalAllocated; // Amount of tokens that have been allocated to all strategies - 46: uint256 public lastReport; // block.timestamp of last report from any strategy + 46: uint64 public lastReport; // block.timestamp of last report from any strategy - 49: bool public emergencyShutdown; - 55: uint256 public withdrawMaxLoss = 1; + 55: uint64 public withdrawMaxLoss = 1; - 56: uint256 public lockedProfitDegradation; // rate per block of degradation. DEGRADATION_COEFFICIENT is 100% per block + 56: uint128 public lockedProfitDegradation; // rate per block of degradation. DEGRADATION_COEFFICIENT is 100% per block 57: uint256 public lockedProfit; // how much profit is locked and cant be withdrawn 78: address public treasury; // address to whom performance fee is remitted in the form of vault shares
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: 28: address public want; 30: bool public emergencyExit; 31: uint256 public lastHarvestTimestamp; 33: uint256 public upgradeProposalTime;
- 28: address public want; 30: bool public emergencyExit; - 31: uint256 public lastHarvestTimestamp; + 31: uint64 public lastHarvestTimestamp; - 33: uint256 public upgradeProposalTime; + 33: uint64 public upgradeProposalTime; + 28: address public want;
Saves a storage slot. If the variable is assigned a non-zero value, saves Gsset (20000 gas). If it's assigned a zero value, saves Gsreset (2900 gas). If the variable remains unassigned, there is no gas savings unless the variable is public, in which case the compiler-generated non-payable getter deployment cost is saved. If the state variable is overriding an interface's public function, mark the variable as constant or immutable so that it does not use a storage slot
1 result - 1 file:
Ethos-Core\contracts\StabilityPool.sol: 160: address public lqtyTokenAddress;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L160
Deposit
struct can be removedEthos-Core\contracts\StabilityPool.sol: 174: struct Deposit { 175: uint initialValue; 176: } 186: mapping (address => Deposit) public deposits; // depositor address -> deposit struct
Using a direct type when accessing mapping values is more gas-optimized than using a single-element structure. Because the first requires a storage slot to be read or written. The second requires reading or writing two storage slots. (one for the struct and one for the value inside the struct)
Here, removing struct Deposit
and accessing directly from mapping is gas-optimized.
Ethos-Core\contracts\StabilityPool.sol: - 174: struct Deposit { - 175: uint initialValue; - 176: } - 186: mapping (address => Deposit) public deposits; // depositor address -> deposit struct // address depositor -> uint256 initialValue + 186: mapping (address => uint256) public deposits;
delete
instead of setting mapping/state variable 0
saves gas9 results - 3 files:
Ethos-Core\contracts\StabilityPool.sol: 529: lastLUSDLossError_Offset = 0; 578: currentScale = 0;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L529
Ethos-Core\contracts\TroveManager.sol: 1192: Troves[_borrower][_collateral].stake = 0; 1285: Troves[_borrower][_collateral].coll = 0; 1286: Troves[_borrower][_collateral].debt = 0; 1288: rewardSnapshots[_borrower][_collateral].collAmount = 0; 1289: rewardSnapshots[_borrower][_collateral].LUSDDebt = 0;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1192
Ethos-Vault\contracts\ReaperVaultV2.sol: 215: strategies[_strategy].allocBPS = 0; 537: lockedProfit = 0;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L215
Recommendation code:
Ethos-Vault\contracts\ReaperVaultV2.sol: - 215: strategies[_strategy].allocBPS = 0; + 215: delete strategies[_strategy].allocBPS;
delete
instead of setting address(0)
saves gas1 result - 1 file:
Ethos-Core\contracts\TroveManager.sol: 294: owner = address(0);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L294
Proof Of Concepts:
contract Example { address public myAddress = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4; // use for empty value Slot: 23450 gas // use for non empty value Slot: 21450 gas function reset() public { delete myAddress; } // use for empty value Slot: 23497 gas // use for non empty value Slot: 23497 gas function setToZero() public { myAddress = address(0); } }
delete
to set bool
values to their default values saves gas3 result - 2 files:
Ethos-Core\contracts\LUSDToken.sol: 143: mintingPaused = false;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L143
Ethos-Core\contracts\TroveManager.sol: 602: vars.backToNormalMode = false; 804: vars.backToNormalMode = false;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L602 https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L804
storage
instead of memory
for structs/arrays
saves gasWhen fetching data from a storage location, assigning the data to a memory
variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory
keyword, declaring the variable with the storage
keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory
variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory
, or if the array/struct is being read from another memory
array/struct
7 results - 3 files:
Ethos-Core\contracts\BorrowerOperations.sol: 175: ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, lusdToken); 283: ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, lusdToken);
Ethos-Core\contracts\StabilityPool.sol: 687: Snapshots memory snapshots = depositSnapshots[_depositor]; 722: Snapshots memory snapshots = depositSnapshots[_depositor]; 806: address[] memory collaterals = collateralConfig.getAllowedCollaterals(); 857: address[] memory collaterals = collateralConfig.getAllowedCollaterals();
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L687
Ethos-Core\contracts\LQTY\LQTYStaking.sol: 226: address[] memory collaterals = collateralConfig.getAllowedCollaterals();
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/LQTYStaking.sol#L226
address
/ID mappings can be combined into a single mapping
of an address
/ID to a struct
, where appropriateSaves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.
7 results 2 files:
Ethos-Core\contracts\LUSDToken.sol: 62: mapping (address => bool) public troveManagers; 63: mapping (address => bool) public stabilityPools; 64: mapping (address => bool) public borrowerOperations;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L62-L64
Ethos-Core\contracts\TroveManager.sol: 104: mapping (address => uint) public L_Collateral; 105: mapping (address => uint) public L_LUSDDebt; 119: mapping (address => uint) public lastCollateralError_Redistribution; 120: mapping (address => uint) public lastLUSDDebtError_Redistribution;
The instances below point to the second+ access of a value inside a mapping/array, within a function. Caching a mapping's value in a local storage or calldata variable when the value is accessed multiple times, saves ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations. Caching an array's struct avoids recalculating the array offsets into memory/calldata.
30 results - 4 files:
Ethos-Core\contracts\ActivePool.sol: // @audit LUSDDebt[_collateral] 193: LUSDDebt[_collateral] = LUSDDebt[_collateral].add(_amount); 194: ActivePoolLUSDDebtUpdated(_collateral, LUSDDebt[_collateral]); 200: LUSDDebt[_collateral] = LUSDDebt[_collateral].sub(_amount); 201: ActivePoolLUSDDebtUpdated(_collateral, LUSDDebt[_collateral]); // @audit yieldGenerator[_collateral] 245: vars.yieldGenerator = IERC4626(yieldGenerator[_collateral]); 278: IERC20(_collateral).safeIncreaseAllowance(yieldGenerator[_collateral], uint256(vars.netAssetMovement)); 279: IERC4626(yieldGenerator[_collateral]).deposit(uint256(vars.netAssetMovement), address(this)); 281: IERC4626(yieldGenerator[_collateral]).withdraw(uint256(-vars.netAssetMovement), address(this), address(this));
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L193
Ethos-Core\contracts\StabilityPool.sol: // @audit depositSnapshots[_depositor] 825: depositSnapshots[_depositor].P = currentP; 826: depositSnapshots[_depositor].G = currentG; 827: depositSnapshots[_depositor].scale = currentScaleCached; 828: depositSnapshots[_depositor].epoch = currentEpochCached;
Ethos-Core\contracts\TroveManager.sol: // @audit Troves[_borrower][_collateral] 969: Troves[_borrower][_collateral].debt = _newDebt; 970: Troves[_borrower][_collateral].coll = _newColl; 977: Troves[_borrower][_collateral].stake, //@audit Troves[_borrower][_collateral] 1091: Troves[_borrower][_collateral].coll = Troves[_borrower][_collateral].coll.add(pendingCollateralReward); 1092: Troves[_borrower][_collateral].debt = Troves[_borrower][_collateral].debt.add(pendingLUSDDebtReward); 1102: Troves[_borrower][_collateral].debt, 1103: Troves[_borrower][_collateral].coll, 1104: Troves[_borrower][_collateral].stake, // @audit Troves[_borrower][_collateral] 1284: Troves[_borrower][_collateral].status = closedStatus; 1285: Troves[_borrower][_collateral].coll = 0; 1286: Troves[_borrower][_collateral].debt = 0; // @audit rewardSnapshots[_borrower][_collateral] 1288: rewardSnapshots[_borrower][_collateral].collAmount = 0; 1289: rewardSnapshots[_borrower][_collateral].LUSDDebt = 0;
Ethos-Vault\contracts\ReaperVaultV2.sol: // @audit strategies[_strategy] 180: require(strategies[_strategy].activation != 0, "Invalid strategy address"); 182: strategies[_strategy].feeBPS = _feeBPS; 183: emit StrategyFeeBPSUpdated(_strategy, _feeBPS); // @audit strategies[_strategy] 210: if (strategies[_strategy].allocBPS == 0) { 214: totalAllocBPS -= strategies[_strategy].allocBPS;
4 result - 1 file:
Ethos-Vault\contracts\ReaperVaultV2.sol: //@audit token.balanceOf() 368: if (value > token.balanceOf(address(this))) { //@audit token.balanceOf() 373: vaultBalance = token.balanceOf(address(this)); //@audit token.balanceOf() 386: uint256 actualWithdrawn = token.balanceOf(address(this)) - vaultBalance; //@audit token.balanceOf() 399: vaultBalance = token.balanceOf(address(this));
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L368
state variable
in emitUsing the current memory value here will be more gas-optimized.
1 result - 1 file:
Ethos-Vault\contracts\ReaperVaultV2.sol: 578: function updateTvlCap(uint256 _newTvlCap) public { 579: _atLeastRole(ADMIN); 580: tvlCap = _newTvlCap; 581: emit TvlCapUpdated(tvlCap); 582: }
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L581
Ethos-Vault\contracts\ReaperVaultV2.sol: 578: function updateTvlCap(uint256 _newTvlCap) public { 579: _atLeastRole(ADMIN); 580: tvlCap = _newTvlCap; - 581: emit TvlCapUpdated(tvlCap); + 581: emit TvlCapUpdated(_newTvlCap); 582: }
block.timestamp
and block.number
are added to event information by default so adding them manually wastes gas.
1 result - 1 file:
Ethos-Core\contracts\TroveManager.sol: 1500 function _updateLastFeeOpTime() internal { 1501 uint timePassed = block.timestamp.sub(lastFeeOperationTime); 1502 1503 if (timePassed >= SECONDS_IN_ONE_MINUTE) { 1504 lastFeeOperationTime = block.timestamp; 1505: emit LastFeeOpTimeUpdated(block.timestamp); 1506 } 1507 }
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1505
Ethos-Core\contracts\TroveManager.sol: 1500 function _updateLastFeeOpTime() internal { 1501 uint timePassed = block.timestamp.sub(lastFeeOperationTime); 1502 1503 if (timePassed >= SECONDS_IN_ONE_MINUTE) { 1504 lastFeeOperationTime = block.timestamp; -1505: emit LastFeeOpTimeUpdated(block.timestamp); + 1505: emit LastFeeOpTimeUpdated(lastFeeOperationTime); 1506 } 1507 }
Prior to 0.8.10 the compiler inserted extra code, including EXTCODESIZE
(100 gas), to check for contract existence for external function calls. In more recent solidity versions, the compiler will not insert these checks if the external call has a return value. Similar behavior can be achieved in earlier versions by using low-level calls, since low level calls never check for contract existence.
97 results - 9 files:
Ethos-Core\contracts\BorrowerOperations.sol: //@audit getCollateralCCR() 178: vars.collCCR = collateralConfig.getCollateralCCR(_collateral); 286: vars.collCCR = collateralConfig.getCollateralCCR(_collateral); //@audit getCollateralMCR() 179: vars.collMCR = collateralConfig.getCollateralMCR(_collateral); 287: vars.collMCR = collateralConfig.getCollateralMCR(_collateral); //@audit getCollateralDecimals() 180: vars.collDecimals = collateralConfig.getCollateralDecimals(_collateral); 288: vars.collDecimals = collateralConfig.getCollateralDecimals(_collateral); 382: uint256 collDecimals = collateralConfig.getCollateralDecimals(_collateral); //@audit getTroveColl() 388: uint coll = troveManagerCached.getTroveColl(msg.sender, _collateral); //@audit getTroveDebt() 389: uint debt = troveManagerCached.getTroveDebt(msg.sender, _collateral); //@audit getBorrowingFee() 421: uint LUSDFee = _troveManager.getBorrowingFee(_LUSDAmount); //@audit increaseTroveColl(), decreaseTroveColl() 468: uint newColl = (_isCollIncrease) ? _troveManager.increaseTroveColl(_borrower, _collateral, _collChange) 469: : _troveManager.decreaseTroveColl(_borrower, _collateral, _collChange); 470: uint newDebt = (_isDebtIncrease) ? _troveManager.increaseTroveDebt(_borrower, _collateral, _debtChange) 471: : _troveManager.decreaseTroveDebt(_borrower, _collateral, _debtChange); //@audit isCollateralAllowed() 525: require(collateralConfig.isCollateralAllowed(_collateral), "BorrowerOps: Invalid collateral address"); //@audit balanceOf() 529: require(IERC20(_collateral).balanceOf(_user) >= _collAmount, "BorrowerOperations: Insufficient user collateral balance"); 645: require(_lusdToken.balanceOf(_borrower) >= _debtRepayment, "BorrowerOps: Caller doesnt have enough LUSD to make repayment"); //@audit allowance() 530: require(IERC20(_collateral).allowance(_user, address(this)) >= _collAmount, "BorrowerOperations: Insufficient collateral allowance"); //@audit getTroveStatus() 542: uint status = _troveManager.getTroveStatus(_borrower, _collateral); 547: uint status = _troveManager.getTroveStatus(_borrower, _collateral);
Ethos-Core\contracts\TroveManager.sol: //@audit getCollateralDecimals() 420: uint collDecimals = collateralConfig.getCollateralDecimals(_collateral); 522: vars.collDecimals = collateralConfig.getCollateralDecimals(_collateral); 596: vars.collDecimals = collateralConfig.getCollateralDecimals(_collateral); 725: vars.collDecimals = collateralConfig.getCollateralDecimals(_collateral); 798: vars.collDecimals = collateralConfig.getCollateralDecimals(_collateral); 1048: uint256 collDecimals = collateralConfig.getCollateralDecimals(_collateral); 1061: uint256 collDecimals = collateralConfig.getCollateralDecimals(_collateral); 1131: uint256 collDecimals = collateralConfig.getCollateralDecimals(_collateral); 1146: uint256 collDecimals = collateralConfig.getCollateralDecimals(_collateral); 1362: uint256 collDecimals = collateralConfig.getCollateralDecimals(_collateral); 1368: uint256 collDecimals = collateralConfig.getCollateralDecimals(_collateral); //@audit getCollateralMCR() 523: vars.collMCR = collateralConfig.getCollateralMCR(_collateral); 598: vars.collMCR = collateralConfig.getCollateralMCR(_collateral); 727: vars.collMCR = collateralConfig.getCollateralMCR(_collateral); 800: vars.collMCR = collateralConfig.getCollateralMCR(_collateral); //@audit fetchPrice() 524: vars.price = priceFeed.fetchPrice(_collateral); 728: vars.price = priceFeed.fetchPrice(_collateral); //@audit getCollateralCCR() 521: vars.collCCR = collateralConfig.getCollateralCCR(_collateral); 597: vars.collCCR = collateralConfig.getCollateralCCR(_collateral); 726: vars.collCCR = collateralConfig.getCollateralCCR(_collateral); 799: vars.collCCR = collateralConfig.getCollateralCCR(_collateral); //@audit getLast() 606: vars.user = _sortedTroves.getLast(_collateral); 691: vars.user = sortedTrovesCached.getLast(_collateral); //@audit getFirst() 607: address firstUser = _sortedTroves.getFirst(_collateral); //@audit getPrev() 610: address nextUser = _sortedTroves.getPrev(_collateral, vars.user); //@audit getTotalLUSDDeposits() 729: vars.LUSDInStabPool = stabilityPoolCached.getTotalLUSDDeposits(); //@audit getCollateral() 1308: uint activeColl = _activePool.getCollateral(_collateral); 1309: uint liquidatedColl = defaultPool.getCollateral(_collateral); //@audit getSize() 1539: require (TroveOwnersArrayLength > 1 && sortedTroves.getSize(_collateral) > 1);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L420
Ethos-Core\contracts\ActivePool.sol: //@audit getAllowedCollaterals() 105: address[] memory collaterals = ICollateralConfig(collateralConfigAddress).getAllowedCollaterals(); //@audit asset() 111: require(IERC4626(vault).asset() == collateral, "Vault asset must be collateral"); //@audit deposit() 280: IERC4626(yieldGenerator[_collateral]).deposit(uint256(vars.netAssetMovement), address(this)); //@audit withdraw() 282: IERC4626(yieldGenerator[_collateral]).withdraw(uint256(-vars.netAssetMovement), address(this), address(this)); //@audit balanceOf() 288: vars.profit = IERC20(_collateral).balanceOf(address(this)).add(vars.yieldingAmount).sub(collAmount[_collateral]); //@audit isCollateralAllowed() 315: ICollateralConfig(collateralConfigAddress).isCollateralAllowed(_collateral), //@audit redemptionHelper() 328: address redemptionHelper = address(ITroveManager(troveManagerAddress).redemptionHelper());
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L105
Ethos-Core\contracts\StabilityPool.sol: //@audit redemptionHelper() 328: address redemptionHelper = address(ITroveManager(troveManagerAddress).redemptionHelper()); //@audit issueOath() 417: uint LQTYIssuance = _communityIssuance.issueOath(); //@audit getAllowedCollaterals() 638: assets = collateralConfig.getAllowedCollaterals(); 806: address[] memory collaterals = collateralConfig.getAllowedCollaterals(); 857: address[] memory collaterals = collateralConfig.getAllowedCollaterals(); //@audit getCollateralDecimals() 664: vars.collDecimals = collateralConfig.getCollateralDecimals(_collateral); //@audit fetchPrice() 861: uint price = priceFeed.fetchPrice(collateral); //@audit getLast() 862: address lowestTrove = sortedTroves.getLast(collateral); //@audit getCollateralMCR() 863: uint256 collMCR = collateralConfig.getCollateralMCR(collateral); //@audit getCurrentICR() 864: uint ICR = troveManager.getCurrentICR(lowestTrove, collateral, price);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L328
Ethos-Core\contracts\LQTY\CommunityIssuance.sol: //@audit transferFrom() 103: OathToken.transferFrom(msg.sender, address(this), amount); //@audit transfer() 127: OathToken.transfer(_account, _OathAmount);
Ethos-Core\contracts\LQTY\LQTYStaking.sol: //@audit transfer() 135: lusdToken.transfer(msg.sender, LUSDGain); 171: lusdToken.transfer(msg.sender, LUSDGain); //@audit getAllowedCollaterals() 204: assets = collateralConfig.getAllowedCollaterals(); 226: address[] memory collaterals = collateralConfig.getAllowedCollaterals(); //@audit redemptionHelper() 252: address redemptionHelper = address(ITroveManager(troveManagerAddress).redemptionHelper());
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/LQTYStaking.sol#L135
Ethos-Vault\contracts\ReaperVaultV2.sol: //@audit vault() 153: require(address(this) == IStrategy(_strategy).vault(), "Strategy's vault does not match"); //@audit want() 154: require(address(token) == IStrategy(_strategy).want(), "Strategy's want does not match"); //@audit balanceOf() 246: available = Math.min(available, token.balanceOf(address(this))); 279: return token.balanceOf(address(this)) + totalAllocated; 303: _deposit(token.balanceOf(msg.sender), msg.sender); 327: uint256 balBefore = token.balanceOf(address(this)); 329: uint256 balAfter = token.balanceOf(address(this)); 368: if (value > token.balanceOf(address(this))) { 373: vaultBalance = token.balanceOf(address(this)); 386: uint256 actualWithdrawn = token.balanceOf(address(this)) - vaultBalance; 556: return IStrategy(vars.stratAddr).balanceOf(); 641: uint256 amount = IERC20Metadata(_token).balanceOf(address(this)); //@audit withdraw() 385: uint256 loss = IStrategy(stratAddr).withdraw(Math.min(remaining, strategyBal)); //@audit decimals() 651: return token.decimals();
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L153
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: //@audit availableCapital() 111: int256 availableCapital = IVault(vault).availableCapital(); //@audit report() 134: debt = IVault(vault).report(roi, repayment); Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol: //@audit UNDERLYING_ASSET_ADDRESS() 69: want = _gWant.UNDERLYING_ASSET_ADDRESS(); //@audit balanceOf() 120: uint256 amount = startToken.balanceOf(address(this)); 227: return IERC20Upgradeable(want).balanceOf(address(this)); //@audit strategies() 127: uint256 allocated = IVault(vault).strategies(address(this)).allocated; //@audit getLendingPool() 178: address lendingPoolAddress = ADDRESSES_PROVIDER.getLendingPool(); 200: ILendingPool(ADDRESSES_PROVIDER.getLendingPool()).withdraw(address(want), _withdrawAmount, address(this)); //@audit claimAllRewardsToSelf() 207: IRewardsController(REWARDER).claimAllRewardsToSelf(rewardClaimingTokens); //@audit getUserReserveData( 231: (uint256 supply, , , , , , , , ) = IAaveProtocolDataProvider(DATA_PROVIDER).getUserReserveData(
bytes
constants are more eficient than string
constansIf the data can fit in 32 bytes, the bytes32 data type can be used instead of bytes or strings, as it is less robust in terms of robustness.
8 results - 6 files:
Ethos-Core\contracts\ActivePool.sol: 30: string constant public NAME = "ActivePool";
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L30
Ethos-Core\contracts\BorrowerOperations.sol: 21: string constant public NAME = "BorrowerOperations";
Ethos-Core\contracts\LUSDToken.sol: 32: string constant internal _NAME = "LUSD Stablecoin"; 33: string constant internal _SYMBOL = "LUSD"; 34: string constant internal _VERSION = "1";
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L32-L34
Ethos-Core\contracts\StabilityPool.sol: 150: string constant public NAME = "StabilityPool";
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L150
Ethos-Core\contracts\LQTY\CommunityIssuance.sol: 19: string constant public NAME = "CommunityIssuance";
Ethos-Core\contracts\LQTY\LQTYStaking.sol: 23: string constant public NAME = "LQTYStaking";
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/LQTYStaking.sol#L23
public
function visibility to external
Since the following public functions are not called from within the contract, their visibility can be made external for gas optimization.
3 results - 2 files:
Ethos-Core\contracts\TroveManager.sol: 1045: function getNominalICR(address _borrower, address _collateral) public view override returns (uint) { 1440: function getRedemptionFee(uint _collateralDrawn) public view override returns (uint) {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1045
Ethos-Vault\contracts\ReaperVaultV2.sol: 295: function getPricePerFullShare() public view returns (uint256) {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L295
When using elements that are smaller than 32 bytes, your contracts 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.
6 results 3 files:
Ethos-Vault\contracts\ReaperVaultV2.sol: // @audit uint8 decimals() 651: return token.decimals();
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L651
Ethos-Core\contracts\TroveManager.sol: // @audit uint128 index 1329: index = uint128(TroveOwners[_collateral].length.sub(1)); 1332: return index;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1329
Ethos-Core\contracts\StabilityPool.sol: // @audit uint128 currentScaleCached 585: currentScale = currentScaleCached.add(1); // @audit uint128 currentEpochCached 576: currentEpoch = currentEpochCached.add(1); // @audit uint128 scaleSnapshot 745: uint128 scaleDiff = currentScale.sub(scaleSnapshot);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L585
internal
functions only called once can be inlined to save gasNot inlining costs 20 to 40 gas because of two extra JUMP
instructions and additional stack operations needed for function calls.
45 results - 9 files:
Ethos-Core\contracts\ActivePool.sol: 320: function _requireCallerIsBorrowerOperationsOrDefaultPool() internal view {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L320
Ethos-Core\contracts\BorrowerOperations.sol: 524: function _requireValidCollateralAddress(address _collateral) internal view { 533: function _requireSingularCollChange(uint _collTopUp, uint _collWithdrawal) internal pure { 537: function _requireNonZeroAdjustment(uint _collTopUp, uint _collWithdrawal, uint _LUSDChange) internal pure { 546: function _requireTroveisNotActive(ITroveManager _troveManager, address _borrower, address _collateral) internal view { 551: function _requireNonZeroDebtChange(uint _LUSDChange) internal pure { 555 function _requireNotInRecoveryMode( 556 address _collateral, 557 uint _price, 558 uint256 _CCR, 559 uint256 _collateralDecimals 560: ) internal view { 567: function _requireNoCollWithdrawal(uint _collWithdrawal) internal pure { 571 function _requireValidAdjustmentInCurrentMode 572 ( 573 bool _isRecoveryMode, 574 address _collateral, 575 uint _collWithdrawal, 576 bool _isDebtIncrease, 577 LocalVariables_adjustTrove memory _vars 578 ) 579: internal 624: function _requireNewICRisAboveOldICR(uint _newICR, uint _oldICR) internal pure { 636: function _requireValidLUSDRepayment(uint _currentDebt, uint _debtRepayment) internal pure { 640: function _requireCallerIsStabilityPool() internal view {
Ethos-Core\contracts\LUSDToken.sol: 320: function _mint(address account, uint256 amount) internal { 328: function _burn(address account, uint256 amount) internal { 360: function _requireCallerIsBorrowerOperations() internal view { 365: function _requireCallerIsBOorTroveMorSP() internal view { 375: function _requireCallerIsStabilityPool() internal view { 380: function _requireCallerIsTroveMorSP() internal view { 393: function _requireMintingNotPaused() internal view {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L320
Ethos-Core\contracts\StabilityPool.sol: 421: function _updateG(uint _LQTYIssuance) internal { 439: function _computeLQTYPerUnitStaked(uint _LQTYIssuance, uint _totalLUSDDeposits) internal returns (uint) { 597: function _moveOffsetCollAndDebt(address _collateral, uint _collToAdd, uint _debtToOffset) internal { 637: function _getCollateralGainFromSnapshots(uint initialDeposit, Snapshots storage snapshots) internal view returns (address[] memory assets, uint[] memory amounts) { 693: function _getLQTYGainFromSnapshots(uint initialDeposit, Snapshots memory snapshots) internal view returns (uint) { 776: function _sendLUSDtoStabilityPool(address _address, uint _amount) internal { 794: function _sendLUSDToDepositor(address _depositor, uint LUSDWithdrawal) internal { 848: function _requireCallerIsActivePool() internal view { 852: function _requireCallerIsTroveManager() internal view { 856: function _requireNoUnderCollateralizedTroves() internal { 869: function _requireUserHasDeposit(uint _initialDeposit) internal pure { 873: function _requireNonZeroAmount(uint _amount) internal pure {
Ethos-Core\contracts\TroveManager.sol: 1082: function _applyPendingRewards(IActivePool _activePool, IDefaultPool _defaultPool, address _borrower, address _collateral) internal { 1213: function _computeNewStake(address _collateral, uint _coll) internal view returns (uint) { 1321: function _addTroveOwnerToArray(address _borrower, address _collateral) internal returns (uint128 index) { 1339: function _removeTroveOwner(address _borrower, address _collateral, uint TroveOwnersArrayLength) internal { 1516: function _minutesPassedSinceLastFeeOp() internal view returns (uint) { 1538: function _requireMoreThanOneTroveInSystem(uint TroveOwnersArrayLength, address _collateral) internal view {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1082
Ethos-Core\contracts\LQTY\LQTYStaking.sol: 251: function _requireCallerIsTroveManagerOrActivePool() internal view { 261: function _requireCallerIsBorrowerOperations() internal view { 265: function _requireUserHasStake(uint currentStake) internal pure { 269: function _requireNonZeroAmount(uint _amount) internal pure {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/LQTYStaking.sol#L251
Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol: 176: function _deposit(uint256 toReinvest) internal { 206: function _claimRewards() internal {
Ethos-Vault\contracts\ReaperVaultV2.sol: 462: function _chargeFees(address strategy, uint256 gain) internal returns (uint256) { Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: 250: function _liquidateAllPositions() internal virtual returns (uint256 amountFreed);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L462
Due to how constant variables are implemented (replacements at compile-time), an expression assigned to a constant variable is recomputed each time that the variable is used, which wastes some gas.
11 results - 3 files:
Ethos-Core\contracts\TroveManager.sol: 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%
Ethos-Vault\contracts\ReaperVaultV2.sol: 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");
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: 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");
Recommendation Code:
Ethos-Core\contracts\TroveManager.sol: - 54: uint constant public override REDEMPTION_FEE_FLOOR = DECIMAL_PRECISION / 1000 * 5; // 0.5% + // REDEMPTION_FEE_FLOOR = DECIMAL_PRECISION / 1000 * 5; // 0.5% + 54: uint constant public override REDEMPTION_FEE_FLOOR = 5e15; Ethos-Vault\contracts\ReaperVaultV2.sol: - 73: bytes32 public constant DEPOSITOR = keccak256("DEPOSITOR"); + 73: bytes32 public constant DEPOSITOR = 0xe16b3d8fc79140c62874442c8b523e98592b429e73c0db67686a5b378b29f336;
assembly
to write address storage values18 results - 6 files:
Ethos-Core\contracts\LUSDToken.sol: 84 constructor 102: troveManagerAddress = _troveManagerAddress; 106: stabilityPoolAddress = _stabilityPoolAddress; 110: borrowerOperationsAddress = _borrowerOperationsAddress; 114: governanceAddress = _governanceAddress; 117: guardianAddress = _guardianAddress; 146 function updateGovernance(address _newGovernanceAddress) external { 149: governanceAddress = _newGovernanceAddress; 153 function updateGuardian(address _newGuardianAddress) external { 156: guardianAddress = _newGuardianAddress; 160 function upgradeProtocol( 170: troveManagerAddress = _newTroveManagerAddress; 174: stabilityPoolAddress = _newStabilityPoolAddress; 178: borrowerOperationsAddress = _newBorrowerOperationsAddress;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L102
Ethos-Core\contracts\TroveManager.sol: 232 function setAddresses( 266: borrowerOperationsAddress = _borrowerOperationsAddress;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L266
Ethos-Core\contracts\LQTY\CommunityIssuance.sol: 61 function setAddresses 75: stabilityPoolAddress = _stabilityPoolAddress;
Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol: 62: function initialize( 68: gWant = _gWant;
Ethos-Vault\contracts\ReaperVaultV2.sol: 111 constructor( 123: tvlCap = _tvlCap; 124: treasury = _treasury; 627 function updateTreasury(address newTreasury) external { 630: treasury = newTreasury;
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: 63 function __ReaperBaseStrategy_init( 72: vault = _vault; 73: want = _want;
Recommendation Code:
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: 63 function __ReaperBaseStrategy_init( - 72: vault = _vault; + assembly { + sstore(vault.slot, _vault ) + }
payable
You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0
 and saves 13 gas
on deployment with no security risks.
6 results - 6 files:
Ethos-Core\contracts\LUSDToken.sol: 84: constructor
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L84
Ethos-Core\contracts\TroveManager.sol: 225: constructor() public {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L225
Ethos-Core\contracts\LQTY\CommunityIssuance.sol: 57: constructor() public {
Ethos-Vault\contracts\ReaperVaultERC4626.sol: 16: constructor(
Ethos-Vault\contracts\ReaperVaultV2.sol: 111: constructor(
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L111
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: 61: constructor() initializer {}
Recommendation:
Set the constructor to payable
unchecked {}
for subtractions where the operands cannot underflow because of a previous require
or if
statementrequire(a <= b); x = b - a => require(a <= b); unchecked { x = b - a } if(a <= b); x = b - a => if(a <= b); unchecked { x = b - a }
This will stop the check for overflow and underflow so it will save gas.
10 results - 2 files:
Ethos-Vault\contracts\ReaperVaultV2.sol: 144 function addStrategy( 156 require(_allocBPS + totalAllocBPS <= PERCENT_DIVISOR, "Invalid allocBPS value"); 168: totalAllocBPS += _allocBPS;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L156
Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol: 99 if (_amountNeeded > liquidatedAmount) { 100: loss = _amountNeeded - liquidatedAmount; 131 if (totalAssets > allocated) { 132: uint256 profit = totalAssets - allocated; 135 } else if (totalAssets < allocated) { 136: roi = -int256(allocated - totalAssets); 137 } 225 function availableCapital() public view returns (int256) { 234 if (stratCurrentAllocation > stratMaxAllocation) { 235: return -int256(stratCurrentAllocation - stratMaxAllocation); 236: } else if (stratCurrentAllocation < stratMaxAllocation) { 244: uint256 available = stratMaxAllocation - stratCurrentAllocation; 359 function _withdraw( 374 if (value <= vaultBalance) { 384: uint256 remaining = value - vaultBalance; 493 function report(int256 _roi, uint256 _repayment) external returns (uint256) { 534 if (vars.lockedProfitBeforeLoss > vars.loss) { 535: lockedProfit = vars.lockedProfitBeforeLoss - vars.loss; 109 function harvest() external override returns (int256 roi) { 120 if (amountFreed < debt) { 121: roi = -int256(debt - amountFreed); 122 } else if (amountFreed > debt) { 123: roi = int256(amountFreed - debt); 124 }
In the use of for loops, this structure, which will reduce gas costs. This saves 30-40 gas per loop.
13 results - 3 files:
Ethos-Core\contracts\StabilityPool.sol: 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++) {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L351
Ethos-Core\contracts\TroveManager.sol: 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++) {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L608
Ethos-Core\contracts\LQTY\LQTYStaking.sol: 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++) {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/LQTYStaking.sol#L206
Recommendation Code:
- 206: for (uint i = 0; i < assets.length; i++) { + 206: for (uint i = 0; i < assets.length; i = unchecked_inc(i)) { + function unchecked_inc(uint i) internal pure returns (uint) { + unchecked { + return i + 1; + } + }
x += y (x -= y)
costs more gas than x = x + y (x = x - y)
for state variables9 results - 1 file:
Ethos-Vault\contracts\ReaperVaultV2.sol: 168: totalAllocBPS += _allocBPS; 194: totalAllocBPS -= strategies[_strategy].allocBPS; 196: totalAllocBPS += _allocBPS; 214: totalAllocBPS -= strategies[_strategy].allocBPS; 396: totalAllocated -= actualWithdrawn; 445: totalAllocBPS -= bpsChange; 452: totalAllocated -= loss; 515: totalAllocated -= vars.debtPayment; 521: totalAllocated += vars.credit;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L168
x += y (x -= y)
costs more gas than x = x + y (x = x - y)
for state variables.
require
instead of assert
The assert() and require() functions are a part of the error handling aspect in Solidity. Solidity makes use of state-reverting error handling exceptions. This means all changes made to the contract on that call or any sub-calls are undone if an error is thrown. It also flags an error.
They are quite similar as both check for conditions and if they are not met, would throw an error.
The big difference between the two is that the assert() function when false, uses up all the remaining gas and reverts all the changes made.
20 results - 4 files:
Ethos-Core\contracts\BorrowerOperations.sol: 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);
Ethos-Core\contracts\LUSDToken.sol: 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));
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L312
Ethos-Core\contracts\StabilityPool.sol: 526: assert(_debtToOffset <= _totalLUSDDeposits); 551: assert(_LUSDLossPerUnitStaked <= DECIMAL_PRECISION); 591: assert(newP > 0);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L526
Ethos-Core\contracts\TroveManager.sol: 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
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L417
Using nested is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.
3 results - 2 files:
Ethos-Core\contracts\ActivePool.sol: 264: if (vars.percentOfFinalBal > vars.yieldingPercentage && vars.percentOfFinalBal.sub(vars.yieldingPercentage) > yieldingPercentageDrift) {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L264
Ethos-Core\contracts\BorrowerOperations.sol: 311: if (_isDebtIncrease && !isRecoveryMode) { 337: if (!_isDebtIncrease && _LUSDChange > 0) {
Recomendation Code:
Ethos-Core\contracts\BorrowerOperations.sol: - 311: if (_isDebtIncrease && !isRecoveryMode) { + if (_isDebtIncrease) { + if (!isRecoveryMode) { + } + }
double require
instead of using &&
Using double require instead of operator && can save more gas When having a require statement with 2 or more expressions needed, place the expression that cost less gas first.
4 results - 3 files:
Ethos-Core\contracts\BorrowerOperations.sol: 653: require(_maxFeePercentage >= BORROWING_FEE_FLOOR && _maxFeePercentage <= DECIMAL_PRECISION,
Ethos-Core\contracts\LUSDToken.sol: 347: require( 348: _recipient != address(0) && 349: _recipient != address(this), 350: "LUSD: Cannot transfer tokens directly to the LUSD token contract or the zero address" 351: ); 352: require( 353: !stabilityPools[_recipient] && 354: !troveManagers[_recipient] && 355: !borrowerOperations[_recipient], 356: "LUSD: Cannot transfer tokens directly to the StabilityPool, TroveManager or BorrowerOps" 357: );
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L347
Ethos-Core\contracts\TroveManager.sol: 1539: require (TroveOwnersArrayLength > 1 && sortedTroves.getSize(_collateral) > 1);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L1539
Recommendation Code:
Ethos-Core\contracts\TroveManager.sol: - 1539: require (TroveOwnersArrayLength > 1 && sortedTroves.getSize(_collateral) > 1); + require (TroveOwnersArrayLength > 1); + require (sortedTroves.getSize(_collateral) > 1);
Short-circuiting is a solidity contract development model that uses OR/AND
logic to sequence different cost operations. It puts low gas cost operations in the front and high gas cost operations in the back, so that if the front is low If the cost operation is feasible, you can skip (short-circuit) the subsequent high-cost Ethereum virtual machine operation.
//f(x) is a low gas cost operation //g(y) is a high gas cost operation //Sort operations with different gas costs as follows f(x) || g(y) f(x) && g(y)
14 results - 5 files:
Ethos-Core\contracts\TroveManager.sol: 616: if (vars.ICR >= vars.collMCR && vars.remainingLUSDInStabPool == 0) { break; } 817: if (vars.ICR >= vars.collMCR && vars.remainingLUSDInStabPool == 0) { continue; } 1539: require (TroveOwnersArrayLength > 1 && sortedTroves.getSize(_collateral) > 1); 1531: require(msg.sender == borrowerOperationsAddress || msg.sender == address(redemptionHelper));
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L616
Ethos-Vault\contracts\ReaperVaultERC4626.sol: 52: if (totalSupply() == 0 || _freeFunds() == 0) return assets; 80: if (emergencyShutdown || balance() >= tvlCap) return 0; 123: if (emergencyShutdown || balance() >= tvlCap) return 0; 184: if (totalSupply() == 0 || _freeFunds() == 0) return 0;
Ethos-Vault\contracts\ReaperVaultV2.sol: 227: if (totalAllocBPS == 0 || emergencyShutdown) { 555: if (strategy.allocBPS == 0 || emergencyShutdown) {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L227
Ethos-Core\contracts\BorrowerOperations.sol: 311: if (_isDebtIncrease && !isRecoveryMode) { 337: if (!_isDebtIncrease && _LUSDChange > 0) { 653: require(_maxFeePercentage >= BORROWING_FEE_FLOOR && _maxFeePercentage <= DECIMAL_PRECISION,
Ethos-Core\contracts\LUSDToken.sol: 347: require( 348 _recipient != address(0) && 349 _recipient != address(this),
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L347
Contracts most called functions could simply save gas by function ordering via Method ID
. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas
are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.Â
Context:Â All Contracts
Recommendation:Â
Find a lower method ID
name for the most called functions for example Call() vs. Call1() is cheaper by 22 gas
For example, the function IDs in the ActivePool.sol
contract will be the most used; A lower method ID may be given.
Proof of Consept: https://medium.com/joyso/solidity-how-does-function-name-affect-gas-consumption-in-smart-contract-47d270d8ac92
ActivePool.sol function names can be named and sorted according to METHOD ID
Sighash | Function Signature ======================== 31585161 => manualRebalance(address,uint256) c707f625 => setAddresses(address,address,address,address,address,address,address,address,address[]) 8fca46f4 => setYieldingPercentage(address,uint256) 6dc85da8 => setYieldingPercentageDrift(uint256) 2bc75d35 => setYieldClaimThreshold(address,uint256) 46a80441 => setYieldDistributionParams(uint256,uint256,uint256) 9b56d6c9 => getCollateral(address) 4b14fa87 => getLUSDDebt(address) 715aa61b => sendCollateral(address,address,uint256) 5fc7b5e8 => increaseLUSDDebt(address,uint256) 3aacc815 => decreaseLUSDDebt(address,uint256) eb13cd10 => pullCollateralFromBorrowerOperationsOrDefaultPool(address,uint256) e07a32fa => _rebalance(address,uint256) a388c48f => _requireValidCollateralAddress(address) cd0a39b5 => _requireCallerIsBorrowerOperationsOrDefaultPool() 9cfa3bca => _requireCallerIsBOorTroveMorSP() 8daa3501 => _requireCallerIsBOorTroveM()
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 simple 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 In 0.8.15 the conditions necessary for inlining are relaxed. Benchmarks show that the change significantly decreases the bytecode size (which impacts the deployment cost) while the effect on the runtime gas usage is smaller.
In 0.8.17 prevent the incorrect removal of storage writes before calls to Yul functions that conditionally terminate the external EVM call; Simplify the starting offset of zero-length operations to zero. More efficient overflow checks for multiplication.
12 results - 12 files:
Ethos-Core\contracts\ActivePool.sol: 3: pragma solidity 0.6.11;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L3
Ethos-Core\contracts\BorrowerOperations.sol: 3: pragma solidity 0.6.11;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol#L3
Ethos-Core\contracts\CollateralConfig.sol: 3: pragma solidity 0.6.11;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/CollateralConfig.sol#L3
Ethos-Core\contracts\LUSDToken.sol: 3: pragma solidity 0.6.11;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol#L3
Ethos-Core\contracts\StabilityPool.sol: 3: pragma solidity 0.6.11;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol#L3
Ethos-Core\contracts\TroveManager.sol: 3: pragma solidity 0.6.11;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L3
Ethos-Core\contracts\LQTY\CommunityIssuance.sol: 3: pragma solidity 0.6.11;
Ethos-Core\contracts\LQTY\LQTYStaking.sol: 3: pragma solidity 0.6.11;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/LQTYStaking.sol#L3
Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol: 3: pragma solidity ^0.8.0;
Ethos-Vault\contracts\ReaperVaultERC4626.sol: 3: pragma solidity ^0.8.0;
Ethos-Vault\contracts\ReaperVaultV2.sol: 3: pragma solidity ^0.8.0;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L3
Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol: 3: pragma solidity ^0.8.0;
>=
costs less gas than >
The compiler uses opcodes GT and ISZERO for solidity code that uses >, but only requires LT for >=, which saves 3 gas
1 result - 1 file:
Ethos-Core\contracts\LQTY\CommunityIssuance.sol: 87: uint256 endTimestamp = block.timestamp > lastDistributionTime ? lastDistributionTime : block.timestamp;
Make sure Solidity’s optimizer is enabled. It reduces gas costs. If you want to gas optimize for contract deployment (costs less to deploy a contract) then set the Solidity optimizer at a low number. If you want to optimize for run-time gas costs (when functions are called on a contract) then set the optimizer to a high number.
Ethos-Core\hardhat.config.js: 38: solidity: { 39: compilers: [ 40: { 41: version: "0.4.23", 42: settings: { 43: optimizer: { 44: enabled: true, 45: runs: 100 46: } 47: } 48: }, 49: { 50: version: "0.5.17", 51: settings: { 52: optimizer: { 53: enabled: true, 54: runs: 100 55: } 56: } 57: }, 58: { 59: version: "0.6.11", 60: settings: { 61: optimizer: { 62: enabled: true, 63: runs: 100 64: } 65: } 66: }, 67: ] 68: },
Ethos-Vault\hardhat.config.js: 10: module.exports = { 11: solidity: { 12: compilers: [ 13: { 14: version: "0.8.11", 15: settings: { 16: optimizer: { 17: enabled: true, 18: runs: 200, 19: }, 20: }, 21: }, 22: ], 23: }, 24: networks: { 25: mainnet: { 26: url: `https://rpc.ftm.tools`, 27: chainId: 250, 28: accounts: [`0x${PRIVATE_KEY}`], 29: }, 30: testnet: { 31: url: `https://rpcapi-tracing.testnet.fantom.network`, 32: chainId: 4002, 33: accounts: [`0x${PRIVATE_KEY}`], 34: }, 35: }, 36: etherscan: { 37: apiKey: FTMSCAN_KEY, 38: }, 39: mocha: { 40: timeout: 1200000, 41: }, 42: gasReporter: { 43: enabled: true 44: } 45: };
#0 - c4-judge
2023-03-08T15:26:12Z
trust1995 marked the issue as grade-a