xanoutas/solidity-security-findings huggingface dataset:
CDPVault.calDecrease() calculates ```newCumulativeIndex``` wrongly, as a result, either the borrower/the protocol will lose funds. 
Audit: 2024-07-loopfi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L703-L709


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
```CDPVault.calDecrease()``` performs some calculation during debt repayment, in particular, ```newCumulativeIndex``` needs to be calculated when all/part of the cumulativeInterest has been paid. However, the way to calculate ```newCumulativeIndex``` is wrong, leading to the loss of funds for the user or the  protocol. Sometimes we even get ```newCumulativeIndex > cumulatveIndexNow```, effectively giving an interest credit to the user, loss of funds for the protocol.  

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

First of all, ```CDPVault.calDecrease()``` performs some calculation during debt repayment. In particular, when ```amountToRepay < interestAccrued```, ```newCumulativeIndex``` is calculated as follows: 

[https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L703-L709](https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/CDPVault.sol#L703-L709)

However, this is wrong. To show this, we understand that make sure that the ```accruedInterest``` from ```cumulativeIndexLastUpdate``` to  ```newCumulativeIndex``` should be equal to ```amountToRepay```.

Second, based on creditLogi.calcAccruedInterest(), we should have: 

```amountToRepay = (debt * newCumulativeIndex) / cumulativeIndexLastUpdate - debt```



Therefore, we have:

```
newCumulativeIndex  = (amountToRepay + debt) *  cumulativeIndexLastUpdate / debt
                    = cumulativeIndexLastUpdate + amountToRepay * cumulativeIndexLastUpdate / debt; 
```

Therefore, the way to calculate ```newCumulativeIndex``` in ```CDPVault.calDecrease()``` is wrong. 


In the following, we show that:
1. The test contract creates a position 100 ether collateral and 80 ether debt.
2. After 365 days, it accrues ``` cumulativeQuotaInterest``` in the amount of 80000000000000000 and ```accruedInterst``` in the amount of ```8001505882352941176``` and thus a total interest of 8081505882352941176.
3. The test contract repays in the amount of 8002505882352941176, which is 0.001 ether over ```accruedInterst```. As a result, all of ``` cumulativeQuotaInterest```  will be paid off, and part of the ```cumulativeQuotaInterest``` will be paid. 
4, However, due the wrong calculation of ```newCumulativeIndex```, we get into a situation in which ```newCumulativeIndex > cumulativeIndexNow```, means that effectively, the contract cancels some of the debt for the user ahead of time, loss of funds for the protocol. 
5. Due to ```newCumulativeIndex > cumulativeIndexNow```, the function ```virtualDebt()``` will revert due to underflow.


run ```forge test --match-test test_repay5 -vv```.

```javasc
Upgraded Q -> 2 from #1023 [1686214249453]
Audit: 2023-04-rubicon-findings
Severity: HIGH

Judge has assessed an item in Issue #1023 as 2 risk. The relevant finding follows:

 The function should have an additional check to ensure recipient and owner are not both address(0), as in such case the pay_gem would be frozen in the market: users can neither cancel nor buy it (both reverting because cannot transfer to address(0))
Upgraded Q -> 2 from #931 [1686211383200]
Audit: 2023-04-rubicon-findings
Severity: HIGH

Judge has assessed an item in Issue #931 as 2 risk. The relevant finding follows:

 In SimpleMarket.buy function, _offer.recipient recieves the spend amount of tokens from the taker of the offer. But the _offer.recipient is never checked for address(0). So in the worst case the buy_gem tokens could be sent to the address(0) and be burnt.
Rewards can be claimed from Comptroller without justification
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/BathHouseV2.sol#L120
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/compound-v2-fork/Comptroller.sol#L1740


# Vulnerability details

## Impact

**This seems to be out of scope since it affects the Comptroller and BathToken contracts. However, I still wanted to report this in order to assure the safety of the Rubicon v2 protocol. Furthermore, unjustified claiming of reward tokens affects the solvency of the protocol. **  

Owners of BathTokens can claim rewards from Comptroller trough [claimRewards](https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/BathHouseV2.sol#L115) of the BathHouseV2 contract. This can be done multiple times in a row (distinct transactions but no waiting time in between) while receiving new rewards every time.  

The call to [distributeSupplierComp](https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/compound-v2-fork/Comptroller.sol#L1740) in the [claimComp](https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/compound-v2-fork/Comptroller.sol#L1717) method the `Comptroller` contract adds seemingly unjustified rewards to the `compAccrued[holders[j]]` storage mapping on every new transaction. The accrured reward is subsequently paid to the BathToken holder, i.e. the user. 

## Proof of Concept

The following PoC demonstrates the problem outlined above, please apply the diff to your `bath-buddy.ts` test and run it.
```diff
diff --git a/test/hardhat-tests/bath-buddy.ts b/test/hardhat-tests/bath-buddy.ts
index 8616871..fbebc7d 100644
--- a/test/hardhat-tests/bath-buddy.ts
+++ b/test/hardhat-tests/bath-buddy.ts
@@ -13,6 +13,7 @@ describe("RubiconV2 rewards system", function () {
     "function balanceOf(address) external view returns(uint256)",
     "function admin() external view returns(address)",
     "function mint(uint256) external returns(uint256)",
+    "function transfer(address, uint256) external returns (bool)",
   ];
   const BUDDY_ABI = [
     "function setRewardsDuration(uint256,address) external",
@@ -22,7 +23,7 @@ describe("RubiconV2 rewards system", function () {

   async function buddySetupFixture() {
     // Contracts are deployed using the first signer/account by default
-    const [owner, otherAccount] = await ethers.getSigners();
+    const [owner, otherAccount, userAlice] = await ethers.getSigners();

     // Use Compound Fork to spawn some cTokens...
     // Note: In prod COMP uses Unitroller for Comptroller storage and upgradeability*
@@ -143,6 +144,7 @@ describe("RubiconV2 rewards system", function () {
       testRewardCoin,
       buddy,
       bathHouse,
+      userAlice,
     };
   }

@@ -157,30 +159,50 @@ describe("RubiconV2 rewards system", function () {
         buddy,
         b
The borrowing loop mechanism for leverage makes all traders using leverage subject to liquidation
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/utilities/poolsUtility/Position.sol#L309


# Vulnerability details

### Impact

[docs.compound.finance/v2/ctokens/#liquidate-borrow](https://docs.compound.finance/v2/ctokens/#liquidate-borrow)

Leveraged position are compounded using a loop by using the max borrow capacity at each time. meaning that if the quote token the user is trading just drop a little in value a liquidator will be allow to liquidate his position until the user close his trade.

### Proof of Concept

The Position contract create leveraged position by borrowing multiple time the full available liquidity ([Position.sol#L309](https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/utilities/poolsUtility/Position.sol#L309) - [Position.sol#L267](https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/utilities/poolsUtility/Position.sol#L267))

This technique allow to high leveraged position (up to x3) but don’t let any margin in case the supplied token loss value against the quote token (which, at some point, will be the case for long and short positions). 

But when the supplied token will decrease a little in value a liquidator will be allow to seize the trader position until the trade end by calling the native Compound function `liquidateBorrow()` [CErc20.sol#L115-L118](https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/compound-v2-fork/CErc20.sol#L115-L118)

### Recommended Mitigation Steps

The borrowing loop mechanism must allow room for price changes. Instead of borrowing all the available liquidity at every loop this is needed to adjust the max borrowing amount depending of the leverage choose by the trader to let the trader position organically end.
Automatic matching allows front running offers forcing the maker to pay fees instead of earning them 
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L824-L852


# Vulnerability details

If maker fees are enabled and high enough and given the orderbook depth is low enough above the offer,  front running by an  opposite offer at the same price becomes profitable by forcing the maker to pay protocol fees, the maker fee to the attacker and preventing him from receiving the intended maker fee. Issue is less severe in markets with bigger spreads, but markets with higher volume tend to have lower spreads, hence it's greater for the more important markets.

## Impact
1. Loss of funds by the maker
2. If the maker is a strategists, loss of funds by the LPs 
3. Chaotic price action, 
4. in the long run, issues with liquidity caused by thin order books around the market price.

## Proof of Concept
Take a stable coin pair A, B. Let:
market price = 1 
protocolFee = 10pbs 
makerFee = 10bps
orderbookDepth = 3000 A

Maker sends a tx with offer to buy B at a price below below the orderbookDepth
pay_gem = A
pay_amt =  10000
buy_gem = B
buy _amt =  10001


Attacker front runs with an offer
pay_gem = B
pay_amt = 13001
buy_gem = A
buy _amt =  13000

Front runner fills the 3000 worth of orders paying protocol + maker fees 
attackPayed = 3000*20bps = 6
Original offer fills front runners offer for 10000, paying 
attackEarned = 10000 * 10pbs = 10 

Yielding attacker  with
attackNet = attackEarned - attackPayed = 4

Overall losses of the Maker are
makerLoss = 10000 * (makerFee + protocolFee + lostMakerFee) = 30


## Tools Used
VS Code, Pen and paper
## Recommended Mitigation Steps
Allow the maker to chose if they want the offer to be matched. It can be done in many ways eg.
1. add a `bool doMatch` parameter to offer
2. never match offers from strategists, let `offer()` verify if `msg.sender` is part of some kind of strategist registry
[H-01] Anyone can create a malicious offer which permanently blocks the market contract's functionality
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L532
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L375


# Vulnerability details

## Impact

An attacker can create a malicious offer by passing `address(0)` to the `owner` and `recipient` parameters in the `offer() public` function at line https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L511, since there is no zero address guard. Executing this malicious offer will be reverted when transferring tokens from `msg.sender` to the `recipient` at line https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L375, due to the zero address guard in the `transfer()`/`transferFrom()` functions of affected tokens such as `WBTC`, `DAI`, `USDC`, `USDT` and `OP`.

Creating this offer with the best price ratio automatically sets it as the best offer, which means it will be executed first when users attempt to buy tokens, e.g., via the `buyAllAmount() external` function. Since there is no way to delete the malicious offer, the contract's buying functionality will be permanently locked.

## Proof of Concept

To test the issue, I used a fork of the Optimism mainnet. First, create a Foundry test file `./test/foundry/RubiconM1.t.sol` with the source code below. Don't forget to change the `"optimism"` string to your own Optimism RPC node address, e.g., `https://opt-mainnet.g.alchemy.com/v2/{YOUR_ALCHEMY_KEY}`. Additionally, create your own `./test/foundry/Cheatcodes.sol` with the `interface CheatCodes` from https://book.getfoundry.sh/cheatcodes/.

In the `setUp()` initialization function of the Foundry tests, I performed the following actions:

- Moved to a fork of the Optimism mainnet at block 89938794, which is my current test block.
- Read the proxy admin address, impersonated it, and upgraded the old v1 `RubiconMarket` implementation to v2 via `TransparentUpgradeableProxy`, as the admins would do.
- Deployed Compound's contracts, `bathOP` and `bathUSDC` tokens related to the underlying `OP` and `USDC`, respectively.
- Impersonated the `RubiconMarket` owner and set the necessary fees.

To demonstrate the vulnerability, I used a pair of `USDC`/`OP` tokens. This vulnerability exists with most `ERC20` tokens. I created two test functions:

1. `test_buyAllAmount()` performs a typical token purchase from the market by calling `_buyAllAmountUSDCForOP()`. It attempts to buy as many `USDC` tokens as possible for 100 `OP` tokens. The test executes successfully with the following output:
```
[PASS] test_buyAllAmount() (gas: 469449)
Logs:
  >>>>> Going into buyAllAmount() >>>>>
  <<<<< Exited from buyAllAmount() <<<<<
  Bought 196 USDC for 100 OP
```

2. `test_buyAllAmountAlwaysReverted()` first creates a malicious offer with zero-unchecked address parameters and the best price ratio, which moves it to the top position in the order list. Then, attempting to execute the previo
Attacker can get more rewards from BathBuddy than expected
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/6ba583bf7a7c873eb4236b4b708e75724f200c99/contracts/periphery/BathBuddy.sol#L147


# Vulnerability details

## Impact

BathBuddy is a Vesting Wallet that payout withdrawers any bonus token they may have accrued while staking in the BathToken (cToken) 

This vesting occurs linearly over Unix time.

An attacker could steal almost all the bonusTokens in the BathBuddy Vesting Wallet.

## Proof of Concept

The root cause of this issue is that the amount of bonus token that a user is entitled to is based on their relative share of the pool’s total vested bonus token at the point of the withdraw call. It is calculated based on the user’s “spot” share in the pool.

Thus, it is possible for an attacker to deposit large amount of tokens into a BathToken Pool to gain significant share of the pool (e.g. 95%), and then withdraw the all the shares immediately.

Or even frontrun `bathHouse.claimRewards()` function call and increase his shares before it

```
  describe("Testing the rewards", async function () {
    it("should get rewards from both Comptroller and BathBuddy", async function () {
      const {
        owner,
        testCoin,
        testStableCoin,
        testRewardCoin,
        bathTEST,
        buddy,
        bathHouse,
        comptroller,
      } = await loadFixture(buddySetupFixture);

      expect(await comptroller.getCompAddress()).to.be.equal(
        testRewardCoin.address
      );

      await testCoin.approve(bathTEST.address, parseUnits("1000"));
      await bathTEST.mint(parseUnits("1000"));

      const earned = await buddy.earned(owner.address, testStableCoin.address);

      const earnedComp = await testRewardCoin.balanceOf(owner.address);
      expect(earnedComp).to.be.equal(0);

      // skip a year
      await time.increaseTo((await time.latest()) + 365 * 24 * 60 * 60);

      + // deposit more tokens before withdraw (maybe frontruns transaction)
      + await testCoin.approve(bathTEST.address, parseUnits("500"));
      + await bathTEST.mint(parseUnits("500"));
      bathHouse.claimRewards([buddy.address], [testStableCoin.address]);

      const earned2 = await testStableCoin.balanceOf(owner.address);
      const earnedComp2 = await testRewardCoin.balanceOf(owner.address);
      + // REAL VALUE
      + expect(earnedComp2).to.be.equal(parseUnits("9.12"));
      + // EXPECTED VALUE
      + // expect(earnedComp2).to.be.equal(parseUnits("4.56"));

      expect(earned).to.be.lt(earned2);
      expect(earnedComp).to.be.lt(earnedComp2);
    });
  });

```

## Tools Used

vs code

## Recommended Mitigation Steps

Save invested balance while calculating vesting. Do not use spot shares (`balanceOf()`)
BathBuddy reward accounting wobbly
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/periphery/BathBuddy.sol#L139-L155


# Vulnerability details

## Impact
BathBuddy's reward calculation relies on the total supply of `myBathTokenBuddy` and the balance of individual users. `myBathTokenBuddy` is the paired bathToken. However, the total supply of `bathToken` and the balance of each user may change without notifying `BathBuddy`, resulting in incoherent and manipulable reward distribution.

## Proof of Concept
`rewardPerToken` calculates the global amount of reward per bath token held by the user:
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/periphery/BathBuddy.sol#L121-L135

The modifier `updateReward()` is called on `notifyRewardAmount`:

```solidity
    function notifyRewardAmount(
        uint256 reward,
        IERC20 rewardsToken
    ) external onlyOwner updateReward(address(0), address(rewardsToken)) {
```

This computes the reward per token and stores it in the mapping `rewardsPerTokensStored`:
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/periphery/BathBuddy.sol#L247-L257

This `rewardsPerTokensStored` mapping is later used to determine the next reward reward per token in `rewardPerToken`:
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/periphery/BathBuddy.sol#L121-L135

The earned token for a user finally uses `rewardPerToken(token)` to compute the payout for an individual user:
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/periphery/BathBuddy.sol#L139-L155

Any user can wait for reward to be accrued, (by other users calling `getReward()` for example), and get a lot of `bathToken`, call `getReward()` and sell / refund the `bathToken` in the same transaction to profit from high reward.

## Tools Used

Manual review

## Recommended Mitigation Steps

Rethink reward distribution outside of ctoken contract
if repay failed to excute , then interest will still grow ,user will pay more interest 
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/BathHouseV2.sol#L115-L128
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/utilities/poolsUtility/Position.sol#L210-L223


# Vulnerability details

## Impact
if close position didn't work cuz of repay failed to excuted, the interest will grow more , user had to pay more interest even tho they wanna close position

## Proof of Concept
require(
            CErc20Interface(_bathTokenQuote).repayBorrow(_amountToRepay) == 0,
            "_repay: ERROR"
        );
if this check didnt pass , rapay will fail to excute , then interest  will grow with the block.number , even tho user want to close position at certain block.number and they have to pay more interest than block.number they wanted to close 

## Tools Used
manual review

## Recommended Mitigation Steps
`RubiconMarket._buys` will not work for V1 offers due to the reversion in `cancel` method.
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L1201-L1202


# Vulnerability details


## Impact
`RubiconMarket._buys` tries to cancel dust remaining offers, but it fails for V1 offers. So it blocks `RubiconMarket._buys`.


## Proof of Concept

In `RubiconMarket._buys`, it tries to cancel an offer with dust pay token amount after fulfilling. So it sets `dustId` so it can pass the `RubiconMarket.can_cancel` modifier.

```solidity
        if (
            isActive(id) &&
            offers[id].pay_amt < _dust[address(offers[id].pay_gem)]
        ) {
            dustId = id; //enable current msg.sender to call cancel(id)
            cancel(id);
        }
```

But when we move into `cancel` method, there is another validation. `RubiconMarket.cancel` will call `SimpleMarket.cancel`, and `SimpleMarket.cancel` tries to return dust pay token amount to owner.

```solidity
        _offer.owner == address(0) && msg.sender == _offer.recipient
            ? require(_offer.pay_gem.transfer(_offer.recipient, _offer.pay_amt))
            : require(_offer.pay_gem.transfer(_offer.owner, _offer.pay_amt));
```

In this case, `msg.sender` is different from `_offer.recipient` so `SimpleMarket.cancel` will treat this as a V2 offer, while it can be a valid V1 offer. So for a V1 offer, it tries to send pay token amount to `_offer.owner`, and it is `address(0)` for V1 offers. So it will revert and `SimpleMarket.cancel` will not work although it passes `RubiconMarket.can_cancel` modifier. This will block `_buy` method.


## Tools Used
Manual Review

## Recommended Mitigation Steps


In `SimpleMarket.cancel`, we should refund pay token amount to `_offer.recipient` when `_offer.owner` is `address(0)`.
`RubiconMarket.sellAllAmount` returns meaningless value
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L1066


# Vulnerability details

## Impact
`RubiconMarket.sellAllAmount` returns a meaningless, wrong value.

## Proof of Concept

`RubiconMarket.sellAllAmount` calculates `fill_amt` and then returns fee deducted value of it. 

```solidity
    fill_amt = calcAmountAfterFee(fill_amt);
```

`fill_amt` is in offer's pay token, and fee is deducted from offer's buy token. So deducting fee from `fill_amt` is meaningless and not correct. When we handle the fee, it should be added to `pay_amt` parameter of `sellAllAmount`. So `sellAllAmount` returns a meaningless value. In `Position` contract, this return value is not used, and `Position` is safe. But EOAs and other contracts also can interact with `RubiconMarket`, so this problem should be noticed.


## Tools Used
Manual Review

## Recommended Mitigation Steps

We can safely remove the last line of `RubiconMarket.sellAllAmount`.

```solidity
 -      fill_amt = calcAmountAfterFee(fill_amt);
```