xanoutas/solidity-security-findings huggingface dataset:
proposeRedemption() should use the current price, not a cached price
Audit: 2024-03-dittoeth-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibShortRecord.sol#L27-L28
https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/RedemptionFacet.sol#L56-L211


# Vulnerability details

## Summary

Redemption proposals should be processed using an up to date price rather than a cached price which may be stale

## Vulnerability Details

In the docs it is stated [here](https://dittoeth.com/technical/redemptions#proposing-a-redemption):

> `shortRecords` that are invalid to be proposed:
> if it's greater than the max allowable CR

`proposeRedemption()` uses a cached price p.oraclePrice to calculate the collateral Ratio of Short Records being proposed.
The cached price may be using stale data so the CRs in the list of proposals may now have different CRs which violate the `C.MAX_REDEMPTION_CR` check and therefore should never have been included in the slate.

```
    function proposeRedemption(
        address asset,
        MTypes.ProposalInput[] calldata proposalInput,
        uint88 redemptionAmount,
        uint88 maxRedemptionFee
    ) external isNotFrozen(asset) nonReentrant {

        // SOME CODE

        p.oraclePrice = LibOracle.getPrice(p.asset);

        // SOME CODE

        p.currentCR = currentSR.getCollateralRatio(p.oraclePrice);
        if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue;

        // SOME CODE
    }
```

## Impact

Invalid Short Records are added to the slate.
Paying off their debt increases their CR beyond the max allowed level which can affect stability of the protocol.

## Tools Used

Manual Review
Foundry Testing

## Recommendations

Use `getSavedOrSpotOraclePrice()` which will use `getPrice()` if it's not stale to save gas or else return the spot price.

```diff
    function proposeRedemption(
        address asset,
        MTypes.ProposalInput[] calldata proposalInput,
        uint88 redemptionAmount,
        uint88 maxRedemptionFee
    ) external isNotFrozen(asset) nonReentrant {

        // SOME CODE

-       p.oraclePrice = LibOracle.getPrice(p.asset);
+       p.oraclePrice = LibOracle.getSavedOrSpotOraclePrice(p.asset);

        // SOME CODE

        p.currentCR = currentSR.getCollateralRatio(p.oraclePrice);
        if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue;

        // SOME CODE
    }
```


## Assessed type

Oracle
Lack of a stale check for the base oracle in `LibOracle::oracleCircuitBreaker()`
Audit: 2024-03-dittoeth-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOracle.sol#L125-L126
https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOracle.sol#L77


# Vulnerability details

## Impact

> **This report raises an issue regarding the lack of a stale price check for the base oracle (ETH/USD price) in the `oracleCircuitBreaker()` only**, as the lack of a stale price check for the non-ETH/USD asset oracle (multi-asset oracle) was flagged as a known issue.

The `LibOracle::oracleCircuitBreaker()` does not check the stale price for the base oracle (ETH/USD price). Specifically, I'm talking about the condition: "`block.timestamp > 2 hours + baseTimeStamp`" (i.e., the  [2-hour stale heartbeat](https://dittoeth.com/technical/oracles#invalid-fetch-data) as per the docs). Hence, the function cannot verify whether or not the `baseChainlinkPrice` is stale.

Consequently, the `oracleCircuitBreaker()` will [not revert the transaction](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOracle.sol#L128) as expected if the `baseChainlinkPrice` is stale. As a result, the protocol's core functions will consume the stale price, harming the protocol's and users' funds.

## Proof of Concept

The [`oracleCircuitBreaker()`](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOracle.sol#L125-L126) does not check the stale price for the base oracle (ETH/USD price) (i.e., the condition: "``block.timestamp > 2 hours + baseTimeStamp``") compared to the [`baseOracleCircuitBreaker()`](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOracle.sol#L77).

Without the stale price check mentioned above, the `oracleCircuitBreaker()` cannot verify whether the `baseChainlinkPrice` is stale. For this reason, the `oracleCircuitBreaker()` will [not revert the transaction](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOracle.sol#L128) as expected if the `baseChainlinkPrice` is stale.

```solidity
    //@audit -- This report raises an issue regarding the lack of a stale price check for the base oracle (ETH/USD price) in the oracleCircuitBreaker() only, as the lack of a stale price check for the non-ETH/USD asset oracle (multi-asset oracle) was flagged as a known issue
    function oracleCircuitBreaker(
        uint80 roundId,
        uint80 baseRoundId,
        int256 chainlinkPrice,
        int256 baseChainlinkPrice,
        uint256 timeStamp,
        uint256 baseTimeStamp
    ) private view {
@1      bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0
@1          || baseRoundId == 0 || baseTimeStamp == 0 || baseTimeStamp > block.timestamp |
Insufficient Collateral Handling on Partial Short Exits
Audit: 2024-03-dittoeth-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/ExitShortFacet.sol#L198-L200


# Vulnerability details

## Description 
the bug is in the exitShort function this functio is  lies in the static adjustment of collateral without reevaluating the collateral ratio after the market bid's outcome. 
and this is fails to account for real-time fluctuations in market conditions,
as price slippage or partial fulfillment of the exit order,
leading to potentially under-collateralized positions. Specifically, the subtraction of e.ethFilled from short.collateral is performed without ensuring that the remaining collateral is adequate to maintain a safe collateral ratio,
given the new reduced ERC debt (short.ercDebt -= e.ercFilled).


## Impact
the bug is lead to potentially under-collateralized positions and attackers can exploit this  by creating conditions for under-collateralization

## Proof of Concept
i fuzz with this scenario    
 Buyback Amount: 82 ERC
 Price: 2 ETH per ERC
 Collateral: 56 ETH
 ERC Debt: 95 ERC
```python
# Parameters
buyback_amount = 82  # ERC
price = 2  # ETH per ERC
collateral = 56  # ETH
erc_debt = 95  # ERC

# Calculation
eth_required = buyback_amount * price  # ETH required for the buyback

# Logic Check (Simulating Contract's Behavior)
if eth_required > collateral:
    print(f"Operation failed: Insufficient collateral. {eth_required} ETH required, but only {collateral} ETH available.")
else:
    new_collateral = collateral - eth_required  # This would be the bug in the contract logic
    print(f"Operation proceeded. New collateral: {new_collateral} ETH")

# Output from this scenario
print(f"ETH Required: {eth_required}, Available Collateral: {collateral}")
```
as result The ETH required for the buyback would be 82 * 2 = 164 ETH. 
and the available collateral is only 56 ETH the available collateral is only 56 ETH
the check correctly identifies that the required 164 ETH exceeds the available 56 ETH of collateral, and as per the simulated condition
```python
Operation failed: Insufficient collateral. 164 ETH required, but only 56 ETH available.
```
the logic for handling partial exits does not accurately adjust the collateral leading to potential inconsistencies in state.
## Tools Used
manual review 
## Recommended Mitigation Steps
check after the bid is executed to compare the new collateral ratio against the initial ratio


## Assessed type

Other
Liquidation could be blocked if there is not enough ethEscrowed in TAPP
Audit: 2024-03-dittoeth-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/PrimaryLiquidationFacet.sol#L225


# Vulnerability details


## Impact

Liquidation could be blocked in some cases caused by `_liquidationFeeHandler` 

## Proof of Concept

`liquidate` method calls `_liquidationFeeHandler` method to handle the fees for the caller. The caller receives the fee from the [TAPP.ethEscrowed](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/PrimaryLiquidationFacet.sol#L219)

```solidity
	TAPP.ethEscrowed -= callerFee;
	VaultUser.ethEscrowed += callerFee;
```

However, in case there’s not enough ethEscrowed of TAPP, The caller is given (portion of?) tappFee instead of gasFee.
```solidity
		// Give caller (portion of?) tappFee instead of gasFee
		VaultUser.ethEscrowed += callerFee - m.gasFee + tappFee;
		m.totalFee -= m.gasFee;
		TAPP.ethEscrowed -= m.totalFee;
```


The issue is, if `TAPP.ethEscrowed` is lower than `m.ethFilled.mulU88(m.callerFeePct)`  then this line will revert
```solidity
VaultUser.ethEscrowed += callerFee;
```

as you see totalFee math:
```
m.totalFee += tappFee + callerFee;
```

Here is a very simplified example (numbers for demo only):
tappFee = 10
m.ethFilled.mulU88(m.callerFeePct)  = 30
m.gasFee = 40
TAPP.ethEscrowed = 20

This means
callerFee = 30 + 40 = 70
m.totalFee = 10 + 70 = 80

Now let's execute this code

```solidity
		m.totalFee -= m.gasFee;
		TAPP.ethEscrowed -= m.totalFee;
```

m.totalFee = m.totalFee - 40 = 40
**TAPP.ethEscrowed = TAPP.ethEscrowed - 40 = 20 - 40**

As you can see, a revert is occuring here because TAPP.ethEscrowed is lower than m.totalFee after previous calculations.


## Tools Used

Manual review

## Recommended Mitigation Steps

Add an option to allow liquidation without the caller receiving fees, this is good for the protocol. In case a liquidation has to be done, it should be possible in any case if the caller chooses not to take fee.


## Assessed type

Other
Yield in `updateYield` is not always updated as it should be
Audit: 2024-03-dittoeth-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibVault.sol#L99


# Vulnerability details

## Impact

1.  Yield in `updateYield` is not always updated (inaccurate).
2.  `TAPP.ethEscrowed` is not up to date.
3.  `Vault.dethTotal` is not up to date, `_ethConversion` would return wrong amount in `withdraw`.

## Proof of Concept

[if (dethYieldRate == 0) return](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibVault.sol#L99) this return is intentional, since we don't have `dethYieldRate`, no need to update `Vault.dethYieldRate`. However we have a case that could cause `Vault.dethTotal` not to be up to date.

```solidity
    function updateYield(uint256 vault) internal {
        AppStorage storage s = appStorage();


        STypes.Vault storage Vault = s.vault[vault];
        STypes.VaultUser storage TAPP = s.vaultUser[vault][address(this)];
        // Retrieve vault variables
        uint88 dethTotalNew = uint88(getDethTotal(vault)); // @dev(safe-cast)
        uint88 dethTotal = Vault.dethTotal;
        uint88 dethCollateral = Vault.dethCollateral;
        uint88 dethTreasury = TAPP.ethEscrowed;


        // Pass yield if > 0
        if (dethTotalNew <= dethTotal) return;
        uint88 yield = dethTotalNew - dethTotal;


        // If no short records, yield goes to treasury
        if (dethCollateral == 0) {
            TAPP.ethEscrowed += yield;
            Vault.dethTotal = dethTotalNew;
            return;
        }


        // Assign yield to dethTreasury
        uint88 dethTreasuryReward = yield.mul(dethTreasury).divU88(dethTotal);
        yield -= dethTreasuryReward;
        // Assign tithe of the remaining yield to treasuryF
        uint88 tithe = yield.mulU88(vault.dethTithePercent());
        yield -= tithe;
        // Calculate change in yield rate
        uint80 dethYieldRate = yield.divU80(dethCollateral);
        if (dethYieldRate == 0) return;
        // Realize new totals if yield rate increases after rounding
        TAPP.ethEscrowed += dethTreasuryReward + tithe;
        Vault.dethTotal = dethTotalNew;
        Vault.dethYieldRate += dethYieldRate;
        Vault.dethCollateralReward += yield;
    }
```

Assuming [updateYield](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibVault.sol#L69) gets called, if there is no generated yield from [dethTotalNew](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibVault.sol#L75) then a return will be done at [if (dethTotalNew <= dethTotal) return;](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibVault.sol#L81) since dethTotalNew will equal dethTotal, which makes sense.  
Lets assume there a generated yield and the code continue to execute until it ge
Possible to avoid checkRecoveryModeViolation
Audit: 2024-03-dittoeth-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibSRUtil.sol#L115


# Vulnerability details

## Impact
Adding a short while the market is in recovery mode.

## Proof of Concept
Assuming the current oracle price at the moment for the `deth` in `dusd`is too low ([assetCR](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibSRUtil.sol#L115) is very low) then the market now is in Recovery Mode.

`checkRecoveryModeViolation` is checking if a shorter is able to create a short order or not according to shortRecord CR. if shortRecord CR is too low then shorter shouldn't be able to create a short.
 ```
	uint256 assetCR = Asset.dethCollateral.div(oraclePrice.mul(Asset.ercDebt));
	if (assetCR < recoveryCR) {
		// Market is in recovery mode and shortRecord CR too low
		return true;
	}
```

A shorter can avoid `checkRecoveryModeViolation` by relying on an oracle savedPrice. let's say the current oracle price represent a recovery mode for the market but the old saved oracle price doesn't. since the shorter relied on previous saved oracle price (the price was healthy), he will be able to create short order and violate the market recovery mode.

How oracle price could be savedPrice when `checkRecoveryModeViolation` is called?
[getSavedOrSpotOraclePrice](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/ShortOrdersFacet.sol#L79) is being called before `checkRecoveryModeViolation` method and p.oraclePrice passed to it. In `getSavedOrSpotOraclePrice` a check is happening, that if we should update the oracle price according to creationTime of the asset (which gets updated when reading from Oracle)  [if (LibOrders.getOffsetTime() - getTime(asset) < 15 minutes)](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOracle.sol#L169), if It doesn't cross 15 mins then it will return [getPrice](https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOracle.sol#L170) which is the last saved oracle price not the current oracle price that comes from `getOraclePrice`.

As result a `savedPrice` can cause an issue for the market and allow a shorter to create a short while the market according to the current oracle price is in recovery mode.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Use the current oracle price method `getOraclePrice` for `checkRecoveryModeViolation` instead of `getSavedOrSpotOraclePrice` so it skips the 15 mins delay.


## Assessed type

Other
Using an order book format on chain presents opportunities for MEV and front running.
Audit: 2024-03-dittoeth-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/BidOrdersFacet.sol#L20
https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/AskOrdersFacet.sol#L14


# Vulnerability details

## Impact
A decentralized order book where every bid, ask, or cancel is on chain allows validators to extract MEV from bidders/sellers on Ditto and front runners to profit at the expense of Ditto users.

To place or cancel an order, you must call functions in BidOrdersFacet.sol, AskOrders.sol, OrdersFacet.sol, or ShortRecordFacet.sol. These calls must be processed by validators. This gives them the chance to add or reorder transactions to make a profit.  

Front running takes advantage of a similar concept except that you don't have to be a validator to do it. Front running bots observe pending transactions in the mem pool and when they see a transaction that they could profit off of if they place their transactions in front of it (often this is a very large order that would move the price), they quickly submit their transactions and then pay much higher gas to ensure their transactions are processed first. 

The impact of front running & MEV is that third parties make a profit at the expense of the users of Ditto. This can discourage people from placing bids and asks, leading to less liquidity on your platform. One way to handle this is to give a small maker's fee to anyone placing limit orders. Ditto has this fee for short orders and some limit bids but I would recommend it for all bids, asks, and shorts outstanding for at least one block.

## Proof of Concept

A validator could, for example, observe that someone is trying to cancel an ask because the price moved against them and slip in a bid of their own in front of the cancel, which would be filled by the ask of the person trying to cancel at an unfavorable price. Then the validator could sell for an immediate profit in the same block. The validator will make profit at a loss to users of Ditto. They (or front running bots) can also front run big fills that they expect to move the price.

## Tools Used
Manual review

## Recommended Mitigation Steps
A lot of platforms that run on an order book model provide a small incentive for market makers (ie, people who place limit orders). It is usually like 50 basis points of the amount filled. Having a lot of outstanding liquidity is very important for an exchange or people won't use it. There is currently a reward for short orders or limit bids outstanding for a certain period of time but not for limit asks or bids outstanding for less time.

If you provide this fee, it will incentivize people to place orders even if there is a risk of being impacted by front running or MEV.


## Assessed type

MEV
Can manipulate the C.SHORT_STARTING_ID ShortRecord of the TAPP
Audit: 2024-03-dittoeth-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/PrimaryLiquidationFacet.sol#L244-L247
https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibSRUtil.sol#L124


# Vulnerability details

## Impact

Attackers can make it so that risky debts are not liquidated, and unliquidated risky debts can accumulate over the long term.

## Proof of Concept

ShortRecord of TAPP can also be liquidated. If all ercDebt is liquidated, `LibShortRecord.deleteShortRecord` is called. It moves the `C.SHORT_STARTING_ID` SR of TAPP to the reusing ID list and close it.

Later, when user's ShortRecord is liquidated, `LibShortRecord.fillShortRecord` is called to change the status of `C.SHORT_STARTING_ID` to FullyFilled and update the value in TAPP's `C.SHORT_STARTING_ID` ShortRecord.

However, `LibShortRecord.fillShortRecord` does not move the `C.SHORT_STARTING_ID` that has been moved to the reusing ID list back to the active state list.

```solidity
function _fullorPartialLiquidation(MTypes.PrimaryLiquidation memory m) private {
    STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)];
    uint88 decreaseCol = min88(m.totalFee + m.ethFilled, m.short.collateral);

@>  if (m.short.ercDebt == m.ercDebtMatched) {
        // Full liquidation
        LibSRUtil.disburseCollateral(m.asset, m.shorter, m.short.collateral, m.short.dethYieldRate, m.short.updatedAt);
@>      LibShortRecord.deleteShortRecord(m.asset, m.shorter, m.short.id);
        ...
    } else {
        ...
        // TAPP absorbs leftover short, unless it already owns the short
        if (m.loseCollateral && m.shorter != address(this)) {
            // Delete partially liquidated short
            LibShortRecord.deleteShortRecord(m.asset, m.shorter, m.short.id);
            // Absorb leftovers into TAPP short
@>          LibShortRecord.fillShortRecord(
                m.asset,
@>              address(this),
@>              C.SHORT_STARTING_ID,
                SR.FullyFilled,
                m.short.collateral,
                m.short.ercDebt,
                s.asset[m.asset].ercDebtRate,
                m.short.dethYieldRate
            );
        }
    }

    ...
}
```

If you mint a ShortRecord as NFT and transfer it, the recipient creates a new ShortRecord. If you send an NFT to TAPP, `C.SHORT_STARTING_ID` can be reused and overwrite the original value.

```solidity
function transferShortRecord(address from, address to, uint40 tokenId) internal {
    ...

@>  uint8 id = LibShortRecord.createShortRecord(
        asset, to, SR.FullyFilled, short.collateral, short.ercDebt, short.ercDebtRate, short.dethYieldRate, tokenId
    );

    nft.owner = to;
    nft.shortRecordId = id;
    nft.shortOrderId = 0;
}
```

This is PoC. Add it to LiquidationPrimary.t.sol.

```solidity
function test_PoCTappSRManipulate() public { // same with test_PrimaryPartialShort1ThenPartialShort2
Anyone can call withdraw from BridgeReth leading to possible loss
Audit: 2024-03-dittoeth-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/bridges/BridgeReth.sol#L82


# Vulnerability details

## Impact
Protocol loses funds if BridgeReth logic is called by arbitrary address

## Proof of Concept

Here, withdraw uses doesn't have access control nor accounting mechanism, this lets an attacker to drain funds
```

    function withdraw(address to, uint256 amount) external onlyDiamond returns (uint256) {
        IRocketTokenRETH rocketETHToken = _getRethContract();
        // Calculate dETH equivalent value in rETH
        uint256 rethValue = rocketETHToken.getRethValue(amount);
        // Transfer rETH from this bridge contract
        // @dev RETH uses OZ ERC-20, don't need to check success bool
        rocketETHToken.transfer(to, rethValue);
        return rethValue;
    }
```

## Tools used
Manual

## Recommended steps
fix access control


## Assessed type

Access Control