Ethos Reserve contest - SleepingBugs's results

A CDP-backed stablecoin platform designed to generate yield on underlying assets to establish a sustainable DeFi stable interest rate.

General Information

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

Ethos Reserve

Findings Distribution

Researcher Performance

Rank: 32/154

Findings: 1

Award: $455.47

🌟 Selected for report: 0

🚀 Solo Findings: 0

Low

New tvlCap can be less than current value locked

Add a check to ensure new tvlCap is not less than current value locked.

Use of asserts()

Impact

From solidity docs: Properly functioning code should never reach a failing assert statement; if this happens there is a bug in your contract which you should fix.

With assert the user pays the gas and with require it doesn't. The ETH network gas isn't cheap. You have reachable asserts in the following locations (which should be replaced by require / are mistakenly left from development phase):

Details

The Solidity assert() function is meant to assert invariants. Properly functioning code should never reach a failing assert statement. A reachable assertion can mean one of two things:

A bug exists in the contract that allows it to enter an invalid state; The assert statement is used incorrectly, e.g. to validate inputs.

References

SWC ID: 110

Code Snippet

Recommendation

Substitute asserts with require/revert.

Upgradeable contract is missing a __gap[50] storage variable to allow for new storage variables in later versions

Impact

For upgradeable contracts, there must be storage gap to "allow developers to freely add new state variables in the future without compromising the storage compatibility with existing deployments" (quote OpenZeppelin). Otherwise it may be very difficult to write new implementation code. Without storage gap, the variable in child contract might be overwritten by the upgraded base contract if new variables are added to the base contract. This could have unintended and very serious consequences to the child contracts, potentially causing loss of user fund or cause the contract to malfunction completely. See.

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.

Vulnerability Details

In the following context of the upgradeable contracts they are expected to use gaps for avoiding collision:

  • UUPS

However, none of these contracts contain storage gap. The storage gap is essential for upgradeable contract because "It allows us to freely add new state variables in the future without compromising the storage compatibility with existing deployments". Refer to the bottom part of this article.

If a contract inheriting from a base contract contains additional variable, then the base contract cannot be upgraded to include any additional variable, because it would overwrite the variable declared in its child contract. This greatly limits contract upgradeability.

Code Snippet

    UUPSUpgradeable,
    AccessControlEnumerableUpgradeable

Tools Used

Manual analysis

Recommendation

Recommend adding appropriate storage gap at the end of upgradeable contracts such as the below.

uint256[50] private __gap;

Reference OpenZeppelin upgradeable contract templates.

Missing important events emissions affects off-chain monitoring

Add event emissions for important variables changes and actions.

Critical address change should use two-step procedure

The critical procedures should be two step process.

Recommendation

Lack of two-step procedure for critical operations leaves them error-prone. Consider adding two step procedure on the critical functions and / or a timelock

updateCollateralRatios is not safe without a two-step procedure

As notice at: " !!!PLEASE USE EXTREME CARE AND CAUTION. THIS IS IRREVERSIBLE!!! // // You probably don't want to do this unless a specific asset has proved itself through tough times. // Doing this irresponsibly can permanently harm the protocol."

This functions is critical. Therefore, using a two steps procedure would be optimal.

Consider adding a time before this change is completely done so it can be canceled, or do it in a 2 steps manner so it can be reviewed before setting it up.

_safeTransfer() should be use rather than _transfer

distributionPeriod can be changed without anybody noticing and removing rewardPerSecond until changed back

updateDistributionPeriod doesn't use any kind of event, so if by error the owner sets the value to a high one, rewards per seconds at fund will not be distributed because of rounding down at rewardPerSecond = amount.div(distributionPeriod);

So this human fail can lead to an a not noticed change, due to missing the event, that will make rewards to be 0 until notice.

        function fund(uint amount) external onlyOwner {
        require(amount != 0, "cannot fund 0");
        OathToken.transferFrom(msg.sender, address(this), amount);
        // roll any unissued OATH into new distribution
        if (lastIssuanceTimestamp < lastDistributionTime) {
            uint timeLeft = lastDistributionTime.sub(lastIssuanceTimestamp);
            uint notIssued = timeLeft.mul(rewardPerSecond);
            amount = amount.add(notIssued);
        }

        rewardPerSecond = amount.div(distributionPeriod); //@audit div by high value
        lastDistributionTime = block.timestamp.add(distributionPeriod);
        lastIssuanceTimestamp = block.timestamp;

        emit LogRewardPerSecond(rewardPerSecond);
    }

    // Owner-only function to update the distribution period
    function updateDistributionPeriod(uint256 _newDistributionPeriod) external onlyOwner {
        distributionPeriod = _newDistributionPeriod;
    }

_approve can be frontrunned

Consider to require to set allowance to 0 first as suggested at,

Wrong comment

Collateral is not checked to already exist, this leads to unexpected behaviors

Missing emit keyword

Single-step process for critical ownership transfer/renounce is risky

Impact

Given that some contracts are derived from Ownable, the ownership management of this contract defaults to Ownable ’s transferOwnership() and renounceOwnership() methods which are not overridden here.

Such critical address transfer/renouncing in one-step is very risky because it is irrecoverable from any mistakes

Scenario: If an incorrect address, e.g. for which the private key is not known, is used accidentally then it prevents the use of all the onlyOwner() functions forever, which includes the changing of various critical addresses and parameters. This use of incorrect address may not even be immediately apparent given that these functions are probably not used immediately.

When noticed, due to a failing onlyOwner() function call, it will force the redeployment of these contracts and require appropriate changes and notifications for switching from the old to new address. This will diminish trust in the protocol and incur a significant reputational damage.

Code Snippet

Recommendation

  • Recommend using Ownable2Step from OpenZeppelin or Owned from Solmate

Informational

Wrong place of underscore can lead to confusions

Underscore is a valid symbol used for clarity while reading numbers, this symbol is used in general for separating hundreds (i.e: 4_000) however, it is being used in a not typical position 40_00.

  • ActivePool.sol#L52 uint256 public yieldSplitTreasury = 20_00; // amount of yield to direct to treasury in BPS
  • ActivePool.sol#L53 uint256 public yieldSplitSP = 40_00; // amount of yield to direct to stability pool in BPS
  • ActivePool.sol#L54 uint256 public yieldSplitStaking = 40_00; // amount of yield to direct to OATH Stakers in BPS

Constants should be defined rather than using magic numbers

  • ActivePool.sol#L127 require(_bps <= 10_000, "Invalid BPS value");

  • ActivePool.sol#L133 require(_driftBps <= 500, "Exceeds max allowed value of 500 BPS");

  • ActivePool.sol#L145 require(_treasurySplit + _SPSplit + _stakingSplit == 10_000, "Splits must add up to 10000 BPS");

  • ActivePool.sol#L258 vars.percentOfFinalBal = vars.finalBalance == 0 ? uint256(-1) : vars.currentAllocated.mul(10_000).div(vars.finalBalance);

  • ActivePool.sol#L262 vars.finalYieldingAmount = vars.finalBalance.mul(vars.yieldingPercentage).div(10_000);

  • ActivePool.sol#L291 vars.treasurySplit = vars.profit.mul(yieldSplitTreasury).div(10_000);

  • ActivePool.sol#L296 vars.stakingSplit = vars.profit.mul(yieldSplitStaking).div(10_000);

  • StabilityPool.sol#L768 if (compoundedDeposit < initialDeposit.div(1e9)) {return 0;}

  • ActivePool.sol#L127 require(_bps <= 10_000, "Invalid BPS value");

  • ActivePool.sol#L145 require(_treasurySplit + _SPSplit + _stakingSplit == 10_000, "Splits must add up to 10000 BPS");

Use scientific notation (e.g. 1e18) rather than exponentiation (e.g. 10 ** 18)

  • ReaperVaultV2.sol#L40 uint256 public constant DEGRADATION_COEFFICIENT = 10**18; // The unit for calculating profit degradation.
  • ReaperVaultV2.sol#L125 lockedProfitDegradation = (DEGRADATION_COEFFICIENT * 46) / 10**6; // 6 hours in blocks

Recommendation

Consider replacing 10 ** value notation to scientific notation

Mixed use of type(uint).max and uint(-1) affects consistency

Avoid using mixed ways of accessing max value as this affects consistency.

Inconsistent spacing in comments

Some lines use // x and some use //x. Following the common style comment should be as follows:

-uint256 public genericDeclaration; //generic comment without space`
+uint256 public genericDeclaration; // generic comment with space`
  • TroveManager.sol#L1413 //assert(newBaseRate <= DECIMAL_PRECISION); // This is already enforced in the line above

Non-assembly method available

Non assembly methods are available for this values

  • assembly{ id := chainid() } => uint256 id = block.chainid,

  • LUSDToken.sol#L300 chainID := chainid()

Wrong value of days slightly affects precision

Formula uses a hardcoded value of 365 (days) which would be wrong applied in a leap year (366 days).

Recommendation

  • Consider that that each year is 365.2422 days for maximum precision(NASA value)

Missing indexed event parameters

NOTE: None of these findings where found by 4naly3er output - NC

Index event fields make the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Each event should use three indexed fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed.

Recommendation

Consider which event parameters could be particularly useful to off-chain tools and should be indexed.

Different versions of pragma

Some of the contracts include an unlocked pragma, e.g., pragma solidity >=0.8.0.

Locking the pragma helps ensure that contracts are not accidentally deployed using an old compiler version with unfixed bugs.

Recommendation

Lock pragmas to a specific Solidity version.

Use of uint and uint256 for the same type affects consistency

All over the code uint and uint256, what are the same type, are used with no consistency even in the same contract.

Consider using only uint or uint256 for clarity and consistency.

Missing error messages in require statements

NOTE: None of these findings where found by 4naly3er output - NC

require/revert statements should include error messages in order to help at monitoring the system.

Mixed styles as mapping( / mapping (, for ( / for( are used without consistency, affecting searchability

All over the code there is no consistency when writting spacings and (. Not only as mentioned in the title, but ( variable and (variable are used, etc. This really affects how easy is to maintain the code as it is hard to search at it.

Better to follow Solidity guidelines as it would be following for(variable rather than for ( variable or other mixes of style.

As example:

Deprecated use of now rather than block.timestamp

Signature malleability of EVM's ecrecover()

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).

Missing idempotent check

Add a not same value check like this:

if (_newCoolAddress == newCoolAddress) revert ADDRESS_SAME();

Use of _renounceOwnership can lead to unexpected behaviors

Renouncing to the ownership of a contract can lead to unexpected behaviors. Method setAddresses can only be called once therefore, due to the call from the method to _renounceOwnership. Consider using a method to submit a proposal of new addresses, then another one to accept the proposal where _renounceOwnership is called at the end.

Names can be more specific

Unused dependency can be removed

// import "./Dependencies/Ownable.sol";

Strategy is not checked to be valid and therefore may not exist

strategy is not validated (could not exist) and this function would return 0 on that case.

Maximum line length exceeded

Usually lines in source code are limited to 80 characters. Today’s screens are much larger so it’s reasonable to stretch this in some cases. Solidity newer guidelines suggest 120 characters. Reference: Long lines should be wrapped to conform with Solidity Style guidelines

Following lines with more than 120:

#0 - c4-judge

2023-03-08T12:25:33Z

trust1995 marked the issue as grade-a

#1 - c4-sponsor

2023-03-16T18:19:57Z

0xBebis marked the issue as sponsor confirmed

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter