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: 123/154
Findings: 1
Award: $61.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
61.2601 USDC - $61.26
SafeMath
revert()/require()
stringsstring.concat()
to be used instead of abi.encodePacked(<str>,<str>)
using for
with a list of free functionsAll contracts
The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting. If the contract is meant to be extended, the contract should be abstract and the function signatures be added without any default implementation.
(if(x){}else if(y){...}else{...} => if(!x){if(y){...}else{...}})
.receive()
/fallback()
payable functions that are not used, can be removed to save deployment gas.61 constructor() initializer {}
16 constructor( 17 address _token, 18 string memory _name, 19 string memory _symbol, 20 uint256 _tvlCap, 21 address _treasury, 22 address[] memory _strategists, 23 address[] memory _multisigRoles 24 ) ReaperVaultV2(_token, _name, _symbol, _tvlCap, _treasury, _strategists, _multisigRoles) {}
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultERC4626.sol
The constructor does nothing.
61 constructor() initializer {}
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";
All contracts
It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious owner making a sandwich attack on a user).
71 function setAddresses( 125 function setYieldingPercentage(address _collateral, uint256 _bps) external onlyOwner { 132 function setYieldingPercentageDrift(uint256 _driftBps) external onlyOwner { 138 function setYieldClaimThreshold(address _collateral, uint256 _threshold) external onlyOwner { 144 function setYieldDistributionParams(uint256 _treasurySplit, uint256 _SPSplit, uint256 _stakingSplit) external onlyOwner {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol
110 function setAddresses(
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol
61 function setAddresses
66 function setAddresses
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/LQTYStaking.sol
160 function setHarvestSteps(address[2][] calldata _newSteps) external {
258 function setWithdrawalQueue(address[] calldata _withdrawalQueue) external { 603 function setEmergencyShutdown(bool _active) external { 617 function setLockedProfitDegradation(uint256 degradation) external {
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol
261 function setAddresses(
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/StabilityPool.sol
232 function setAddresses(
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol
external
External instead of public would give more the sense of the initialize(…)
functions to behave like a constructor (only called on deployment, so should only be called externally)
From a security point of view, it might be safer so that it cannot be called internally by accident in the child contract.
It might cost a bit less gas to use external over public.
It is possible to override a function from external to public, but it is not possible to override a function from public to external
For above reasons you can change initialize(…)
to external
62 function initialize( 63 address _vault, 64 address[] memory _strategists, 65 address[] memory _multisigRoles, 66 IAToken _gWant 67 ) public initializer {
Use (e.g. 1e6) rather than decimal literals (e.g. 1000000), for better code readability.
40 uint256 public constant DEGRADATION_COEFFICIENT = 10**18; // The unit for calculating profit degradation. 125 lockedProfitDegradation = (DEGRADATION_COEFFICIENT * 46) / 10**6; // 6 hours in blocks
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol
keccak256()
, should use immutable rather than constant49 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");
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");
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol
if
statementsFor better readability and analysis it is better to avoid nested if
blocks.
118 if (emergencyExit) { 119 uint256 amountFreed = _liquidateAllPositions(); 120 if (amountFreed < debt) { 121 roi = -int256(debt - amountFreed); 122 } else if (amountFreed > debt) { 123 roi = int256(amountFreed - debt); 124 } 126 repayment = debt; 127 if (roi < 0) { 128 repayment -= uint256(-roi); 129 } 130 } else { 131 (roi, repayment) = _harvestCore(debt); 132 }
234 if (stratCurrentAllocation > stratMaxAllocation) { 235 return -int256(stratCurrentAllocation - stratMaxAllocation); 236 } else if (stratCurrentAllocation < stratMaxAllocation) { 237 uint256 vaultMaxAllocation = (totalAllocBPS * balance()) / PERCENT_DIVISOR; 238 uint256 vaultCurrentAllocation = totalAllocated; 240 if (vaultCurrentAllocation >= vaultMaxAllocation) { 241 return 0; 242 } 244 uint256 available = stratMaxAllocation - stratCurrentAllocation; 245 available = Math.min(available, vaultMaxAllocation - vaultCurrentAllocation); 246 available = Math.min(available, token.balanceOf(address(this))); 248 return int256(available); 249 } else { 250 return 0; 251 } ... 368 if (value > token.balanceOf(address(this))) { 369 uint256 totalLoss = 0; 370 uint256 queueLength = withdrawalQueue.length; 371 uint256 vaultBalance = 0; 372 for (uint256 i = 0; i < queueLength; i = i.uncheckedInc()) { 373 vaultBalance = token.balanceOf(address(this)); 374 if (value <= vaultBalance) { 375 break; 376 } 378 address stratAddr = withdrawalQueue[i]; 379 uint256 strategyBal = strategies[stratAddr].allocated; 380 if (strategyBal == 0) { 381 continue; 382 } 384 uint256 remaining = value - vaultBalance; 385 uint256 loss = IStrategy(stratAddr).withdraw(Math.min(remaining, strategyBal)); 386 uint256 actualWithdrawn = token.balanceOf(address(this)) - vaultBalance; 388 // Withdrawer incurs any losses from withdrawing as reported by strat 389 if (loss != 0) { 390 value -= loss; 391 totalLoss += loss; 392 _reportLoss(stratAddr, loss); 393 } 395 strategies[stratAddr].allocated -= actualWithdrawn; 396 totalAllocated -= actualWithdrawn; 397 } ... 438 if (totalAllocBPS != 0) { 439 // reduce strat's allocBPS proportional to loss 440 uint256 bpsChange = Math.min((loss * totalAllocBPS) / totalAllocated, stratParams.allocBPS); 442 // If the loss is too small, bpsChange will be 0 443 if (bpsChange != 0) { 444 stratParams.allocBPS -= bpsChange; 445 totalAllocBPS -= bpsChange; 446 } 447 } ... 509 if (vars.available < 0) { 510 vars.debt = uint256(-vars.available); 511 vars.debtPayment = Math.min(vars.debt, _repayment); 513 if (vars.debtPayment != 0) { 514 strategy.allocated -= vars.debtPayment; 515 totalAllocated -= vars.debtPayment; 516 vars.debt -= vars.debtPayment; // tracked for return value 517 }
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol
286 if (vars.profit != 0) { 287 // profit is ultimately (coll at hand) + (coll allocated to yield generator) - (recorded total coll Amount in pool) 288 vars.profit = IERC20(_collateral).balanceOf(address(this)).add(vars.yieldingAmount).sub(collAmount[_collateral]); 289 if (vars.profit != 0) { 290 // distribute to treasury, staking pool, and stability pool 291 vars.treasurySplit = vars.profit.mul(yieldSplitTreasury).div(10_000); 292 if (vars.treasurySplit != 0) { 293 IERC20(_collateral).safeTransfer(treasuryAddress, vars.treasurySplit); 294 } 296 vars.stakingSplit = vars.profit.mul(yieldSplitStaking).div(10_000); 297 if (vars.stakingSplit != 0) { 298 IERC20(_collateral).safeTransfer(lqtyStakingAddress, vars.stakingSplit); 299 ILQTYStaking(lqtyStakingAddress).increaseF_Collateral(_collateral, vars.stakingSplit); 300 } 302 vars.stabilityPoolSplit = vars.profit.sub(vars.treasurySplit.add(vars.stakingSplit)); 303 if (vars.stabilityPoolSplit != 0) { 304 IERC20(_collateral).safeTransfer(stabilityPoolAddress, vars.stabilityPoolSplit); 305 IStabilityPool(stabilityPoolAddress).updateRewardSum(_collateral, vars.stabilityPoolSplit); 306 } 307 } 308 }
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol
615 if (!vars.backToNormalMode) { 616 // Break the loop if ICR is greater than MCR and Stability Pool is empty 617 if (vars.ICR >= vars.collMCR && vars.remainingLUSDInStabPool == 0) { break; } ... ... 814 if (!vars.backToNormalMode) { 816 // Skip this trove if ICR is greater than MCR and Stability Pool is empty 817 if (vars.ICR >= vars.collMCR && vars.remainingLUSDInStabPool == 0) { continue; } ...
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol
595 if (_isRecoveryMode) { 596 _requireNoCollWithdrawal(_collWithdrawal); 597 if (_isDebtIncrease) { 598 _requireICRisAboveCCR(_vars.newICR, _vars.collCCR); 599 _requireNewICRisAboveOldICR(_vars.newICR, _vars.oldICR); 600 } ...
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol
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.
https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
Generaly contracts in scope do not have NatSpec or are not covered fully. There are a lot of comments for major functions, which is good, but good NatSpec should be applied.
Protocol has enabled optional compiler optimizations in Solidity.
There have been several optimization bugs with security implications. Moreover, optimizations are actively being developed. Solidity compiler optimizations are disabled by default, and it is unclear how many contracts in the wild actually use them.
Therefore, it is unclear how well they are being tested and exercised.
High-severity security issues due to optimization bugs have occurred in the past. A high-severity bug in the emscripten-generated solc-js compiler used by Truffle and Remix persisted until late 2018. The fix for this bug was not reported in the Solidity CHANGELOG.
Another high-severity optimization bug resulting in incorrect bit shift results was patched in Solidity 0.5.6. More recently, another bug due to the incorrect caching of keccak256 was reported. A compiler audit of Solidity from November 2018 concluded that the optional optimizations may not be safe.
It is likely that there are latent bugs related to optimization and that new bugs will be introduced due to future optimizations.
Exploit Scenario
A latent or future bug in Solidity compiler optimizations—or in the Emscripten transpilation to solc-js—causes a security vulnerability in the contracts.
Recommended Mitigation Steps Short term, measure the gas savings from optimizations and carefully weigh them against the possibility of an optimization-related bug.
Long term, monitor the development and adoption of Solidity compiler optimizations to assess their maturity.
Function writing
that does not comply with the Solidity Style Guide
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:
It is best practice to set visibility for state variables.
28 address stabilityPoolAddress; 30 address gasPoolAddress; 32 ICollSurplusPool collSurplusPool;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/BorrowerOperations.sol
31 address gasPoolAddress; 33 ICollSurplusPool collSurplusPool;
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol
__gap[50]
storage variable to allow for new storage variables in later versionsSee the following link for a description of this storage variable. While some contracts may not currently be sub-classed, adding the variable now protects against forgetting to add it in the future.
_safeMint()
should be used rather than _mint()
wherever possible_mint()
is discouraged in favor of _safeMint()
which ensures that the recipient is either an EOA or implements IERC721Receiver
. Both OpenZeppelin and solmate have versions of this function so that NFTs aren’t lost if they’re minted to contracts that cannot transfer them back out.
188 _mint(_account, _amount);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LUSDToken.sol
336 _mint(_receiver, shares); 467 _mint(treasury, shares);
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol
Typically, the contract’s owner is the account that deploys the contract. As a result, the owner is able to perform certain privileged activities.
The Openzeppelin’s Ownable used in this project contract implements renounceOwnership. This can represent a certain risk if the ownership is renounced for any other reason than by design. Renouncing ownership will leave the contract without an owner, thereby removing any functionality that is only available to the owner.
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/ActivePool.sol#L26
Mitigation
We recommend to either reimplement the function to disable it or to clearly specify if it is part of the contract design.
Project is using Openzeppelin version 3.3.0, which has known vulnerabilities.
It is recommended to switch to the latest stable version of Openzeppelin contracts.
#0 - c4-judge
2023-03-09T09:05:25Z
trust1995 marked the issue as grade-b
#1 - c4-sponsor
2023-03-17T23:58:57Z
0xBebis marked the issue as sponsor confirmed