xanoutas/solidity-security-findings huggingface dataset:
Errors can occur when users redeem their assets.
Audit: 2024-03-ondo-finance-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/ousgInstantManager.sol#L388-L456


# Vulnerability details

## Impact
Users can lost their funds because of the loss of the `buidl`.

## Proof of Concept
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/ousgInstantManager.sol#L388-L456
```
  function _redeem(
    uint256 ousgAmountIn
  ) internal returns (uint256 usdcAmountOut) {
    require(
      IERC20Metadata(address(usdc)).decimals() == 6,
      "OUSGInstantManager::_redeem: USDC decimals must be 6"
    );
    require(
      IERC20Metadata(address(buidl)).decimals() == 6,
      "OUSGInstantManager::_redeem: BUIDL decimals must be 6"
    );
    uint256 ousgPrice = getOUSGPrice();
    uint256 usdcAmountToRedeem = _getRedemptionAmount(ousgAmountIn, ousgPrice);

    require(
      usdcAmountToRedeem >= minimumRedemptionAmount,
      "OUSGInstantManager::_redeem: Redemption amount too small"
    );
    _checkAndUpdateInstantRedemptionLimit(usdcAmountToRedeem);

    if (address(investorBasedRateLimiter) != address(0)) {
      investorBasedRateLimiter.checkAndUpdateRedeemLimit(
        msg.sender,
        usdcAmountToRedeem
      );
    }

    uint256 usdcFees = _getInstantRedemptionFees(usdcAmountToRedeem);
    usdcAmountOut = usdcAmountToRedeem - usdcFees;
    require(
      usdcAmountOut > 0,
      "OUSGInstantManager::_redeem: redeem amount can't be zero"
    );

    ousg.burn(ousgAmountIn);

    uint256 usdcBalance = usdc.balanceOf(address(this));

    if (usdcAmountToRedeem >= minBUIDLRedeemAmount) {
      // amount of USDC needed is over minBUIDLRedeemAmount, do a BUIDL redemption
      // to cover the full amount
      _redeemBUIDL(usdcAmountToRedeem);
    } else if (usdcAmountToRedeem > usdcBalance) {
      // There isn't enough USDC held by this contract to cover the redemption,
      // so we perform a BUIDL redemption of BUIDL's minimum required amount.
      // The remaining amount of USDC will be held in the contract for future redemptions.
      _redeemBUIDL(minBUIDLRedeemAmount);
      emit MinimumBUIDLRedemption(
        msg.sender,
        minBUIDLRedeemAmount,
        usdcBalance + minBUIDLRedeemAmount - usdcAmountToRedeem
      );
    } else {
      // We have enough USDC sitting in this contract already, so use it
      // to cover the redemption and fees without redeeming more BUIDL.
      emit BUIDLRedemptionSkipped(
        msg.sender,
        usdcAmountToRedeem,
        usdcBalance - usdcAmountToRedeem
      );
    }

    if (usdcFees > 0) {
      usdc.transfer(feeReceiver, usdcFees);
    }
    emit RedeemFeesDeducted(msg.sender, feeReceiver, usdcFees, usdcAmountOut);

    usdc.transfer(msg.sender, usdcAmountOut);
  }
```

This code assumed the `ousgInstantManager` have the certain amount of the `buidl` token.
But there is no mechanism to get the `buidl` token.
So if protocol doesn't transfer the `buidl` tokens to the `OUSGInstantManager`, users can lost t
Stuck `OUSG` tokens at `rOUSG` contract due to rounding down operations
Audit: 2024-03-ondo-finance-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L431-L443


# Vulnerability details

Users can wrap `OUSG` tokens into `rOUSG` tokens by calling `rOUSG::wrap` and unwrap them by calling `rOUSG::unwrap`. The function `unwrap()` will convert the amount given on the argument, i.e. `rOUSG` tokens, to shares in order to burn those shares and transfer the `OUSG` tokens to the user. The calculations are the following:

$$
\begin{aligned}
\text{ousgSharesAmount} &= rOUSGAmount \times \frac{sharesMultiplier \times 1e18}{oUSGPrice}
\\\\
\text{ousgTokensAmount} &= \frac{ousgSharesAmount}{sharesMultiplier}
\end{aligned}
$$

The value of `ousgTokensAmount` will be the amount of `OUSG` tokens that the contract will send the user that is unwrapping the `rOUSG` tokens.

Because solidity rounds down by default, the value of `ousgTokensAmount` will be rounded down. This is because the value of `oUSGPrice` will always be higher than `1e18`, in fact, will always be higher than `105e18`, as per the `ousgInstantManager` contract indicates. 

Therefore, after a user wraps and unwraps some tokens, some tokens will be left at the `rOUSG` contract. This amount may be tiny in a single unwrap operation, but it will become increasingly large over time. This will lead to a significant amount of `oUSG` tokens being stuck at the `rOUSG` contract as time passes. 

## Impact

Over time, `OUSG` tokens will get stuck on the `rOUSG` contract. 

## Proof of Concept

The `unwrap` function, the two indicated lines of code is where the rounding down happens:

https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L431-L443
```solidity
  function unwrap(uint256 _rOUSGAmount) external whenNotPaused {
    require(_rOUSGAmount > 0, "rOUSG: can't unwrap zero rOUSG tokens");
>>  uint256 ousgSharesAmount = getSharesByROUSG(_rOUSGAmount);
    if (ousgSharesAmount < OUSG_TO_ROUSG_SHARES_MULTIPLIER)
      revert UnwrapTooSmall();
    _burnShares(msg.sender, ousgSharesAmount);
    ousg.transfer(
      msg.sender,
>>    ousgSharesAmount / OUSG_TO_ROUSG_SHARES_MULTIPLIER
    );
    emit Transfer(msg.sender, address(0), _rOUSGAmount);
    emit TransferShares(msg.sender, address(0), ousgSharesAmount);
  }
```

And the `getSharesByROUSG` function is always rounding down because the `OUSG` price will always be bigger than `1e18`. In fact, it will always be higher than `105e18`, as per the `ousgInstantManager` contract. 

https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L363-L368
```solidity
  function getSharesByROUSG(
    uint256 _rOUSGAmount
  ) public view returns (uint256) {
    return
      (_rOUSGAmount * 1e18 * OUSG_TO_ROUSG_SHARES_MULTIPLIER) / getOUSGPrice();
  }
```


## Tools Used

Manual Review

## Recommended Mitigation Steps

It's recommended to create a special function for the protocol team to rescue the stuck `OUSG` tokens from the `rOUSG` contract. 



## Assessed
It is not possible to burn the tokens of a user who was removed from KYC registry.
Audit: 2024-03-ondo-finance-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/78779c30bebfd46e6f416b03066c55d587e8b30b/contracts/ousg/rOUSG.sol#L597-L600


# Vulnerability details

## Impact
In the rOUSG contract, the `burn` function calls the `burnShares` function, which internally calls the `_beforeTokenTransfer` function. Have a look at this check in the `_beforeTokenTransfer` function:

```
    if (from != address(0)) {
      // If not minting
      require(_getKYCStatus(from), "rOUSG: 'from' address not KYC'd");
    }
```

As the tokens are being burnt, the execution enters the above check as the `from` address is not 0. The problem lies in the require statement. It checks whether the KYC status of the `from` address is true. If false, the execution reverts.

This works well for users who have a positive KYC status. But, when the admin wants to burn the tokens from a user whose KYC status was removed, they wouldn't be able to do so. As the `_getKYCStatus` would output false for a user who was removed from the KYC registry and hence the function reverts.

Note that in the contest readme, the KYC related issue talks about this:

>specifically when a user’s KYCRegistry or Sanction status changes in between different actions, leaving them at risk of their funds being locked. If someone gets sanctioned on the Chainalysis Sanctions Oracle or removed from Ondo Finance’s KYC Registry their funds are locked.

This is different from the issue being discussed here. The known issue talks about changing KYC status in between different actions. But, for this issue, KYC status need not change in between actions.  

## Proof of Concept
```solidity
  function _beforeTokenTransfer(
    address from,
    address to,
    uint256
  ) internal view {
    // Check constraints when `transferFrom` is called to facliitate
    // a transfer between two parties that are not `from` or `to`.
    if (from != msg.sender && to != msg.sender) {
      require(_getKYCStatus(msg.sender), "rOUSG: 'sender' address not KYC'd");
    }

    if (from != address(0)) {
      // If not minting
      require(_getKYCStatus(from), "rOUSG: 'from' address not KYC'd");
    }

    if (to != address(0)) {
      // If not burning
      require(_getKYCStatus(to), "rOUSG: 'to' address not KYC'd");
    }
  }
```

## Tools Used
Manual review

## Recommended Mitigation Steps
The above check can be updated to have the following:

`if (from != address(0) && to != address(0))`

This would allow tokens to be burnt even from a user who does not have a positive KYC status.


## Assessed type

Invalid Validation
Intended interaction with a contract will fail, leading to potential loss of funds
Audit: 2024-03-ondo-finance-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/78779c30bebfd46e6f416b03066c55d587e8b30b/contracts/ousg/rOUSGFactory.sol#L121-L132
https://github.com/code-423n4/2024-03-ondo-finance/blob/78779c30bebfd46e6f416b03066c55d587e8b30b/contracts/ousg/ousgInstantManager.sol#L794-L811


# Vulnerability details

## Impact
the intended interaction with a contract will fail, leading to a potential loss of funds or a failed operation, as the value is transferred to an EOA without executing the intended logic. 
Here is how it works: Low-level calls return success if there is no code present at the specified address.
If the guardian intends to call a contract with some data and some value but mistakenly provides an address that is not a contract (i.e., an EOA), the following will happen with the call:

Call Execution: The low-level call will be executed with the provided data and value.

Return Success: Since EOAs do not have associated code to execute, the call will not execute any code but will return true, indicating that the call itself did not revert. The value sent will be transferred to the EOA.

Data Ignored: The data sent in the call will be ignored because there is no contract code to interpret or act upon the data.

Potential Risks: This situation is not ideal because the intended contract interaction will not occur. The value will be transferred, but the expected effects of the data payload will not be realized, potentially leading to a loss of funds.

## Proof of Concept
```solidity
function multiexcall(
    ExCallData[] calldata exCallData
  ) external payable override onlyGuardian returns (bytes[] memory results) {
    results = new bytes[](exCallData.length);
    for (uint256 i = 0; i < exCallData.length; ++i) {
      (bool success, bytes memory ret) = address(exCallData[i].target).call{
        value: exCallData[i].value
      }(exCallData[i].data);
      require(success, "Call Failed");
      results[i] = ret;
    }
  }
```
https://github.com/code-423n4/2024-03-ondo-finance/blob/78779c30bebfd46e6f416b03066c55d587e8b30b/contracts/ousg/rOUSGFactory.sol#L121-L132
## Tools Used
Manual Review

## Recommended Mitigation Steps

To handle both scenarios in the multiexcall function—transferring value to an EOA and calling a contract with data—you can introduce a conditional check that only performs the isContract verification when data is being sent. Here's an example of how you could modify the multiexcall function to accommodate this:

```solidity
function multiexcall(
    ExCallData[] calldata exCallData
) external payable onlyGuardian returns (bytes[] memory results) {
    results = new bytes[](exCallData.length);
    for (uint256 i = 0; i < exCallData.length; ++i) {
        // Check if the target is a contract only if data is provided
        if (exCallData[i].data.length > 0) {
            require(AddressUpgradeable.isContract(exCallData[i].target), "Target must be a contract when data is provided");
        }

        (bo
msg.sender get approval of token when it is being transferred from _sender to _recipient
Audit: 2024-03-ondo-finance-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L285


# Vulnerability details

## Impact
msg.sender despite not being _recipient gets approval of remaining token of _sender after transfer to recipient

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

## Tools Used
Manual review

## Recommended Mitigation Steps
work on code logic and architecture


## Assessed type

Access Control
`Users` cannot redeem their shares if either `unwrap` or `burnShares` is paused
Audit: 2024-03-ondo-finance-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/78779c30bebfd46e6f416b03066c55d587e8b30b/contracts/ousg/rOUSG.sol#L554


# Vulnerability details

## Impact
`Users` cannot redeem their `OUSG` which leads to denial of service.

## Proof of Concept 
 When users call the function `redeem` to redeem their `OUSG` the function checks if redeem is paused using the `whenRedeemNotPaused`. the function then calls the `unwrap` which which also has `whenNotPaused` modifier which also calls the `burnShares` function which checks if contractis paused using the `whenNotPaused` modifier. The issue here is that if the `rOUSG` contract is paused, users cannot redeem their funds any longer.
```solidity
  function _burnShares(
    address _account,
    uint256 _sharesAmount
  ) internal whenNotPaused returns (uint256) {
    require(_account != address(0), "BURN_FROM_THE_ZERO_ADDRESS");

    _beforeTokenTransfer(_account, address(0), _sharesAmount);

    //more code here...

function unwrap(uint256 _rOUSGAmount) external whenNotPaused {
    require(_rOUSGAmount > 0, "rOUSG: can't unwrap zero rOUSG tokens");
    uint256 ousgSharesAmount = getSharesByROUSG(_rOUSGAmount);
    if (ousgSharesAmount < OUSG_TO_ROUSG_SHARES_MULTIPLIER)
      revert UnwrapTooSmall();
    _burnShares(msg.sender, ousgSharesAmount);
    //more code here...
```

## Tools Used
Manual Review
## Recommended Mitigation Steps
Users funds shouldn't be stuck in the contract so I suggest an alternative means of redeeming tokens if the `rOUSG` is paused.


## Assessed type

DoS
Users can increase their balances by transfering shares
Audit: 2024-03-ondo-finance-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L501-L519


# Vulnerability details

## Impact
Users will increase their balances just by transferring funds a malicious user can use this to have more balance with multiple EOA that transfer rOUSG and shares in order to steal funds. 

## Proof of Concept
Alice mint some rOusg transfer an amount to bob and then transfer 1 share it will increase the sum of both balances.
Here is a test that proves that :
````
function test_user_can_increaseTheirShares() public {
        //We set The price at 112 $
     oracleCheckHarnessOUSG.setPrice(112000000000000000008);
 vm.prank(OUSG_GUARDIAN);
 //set the mint limit to be able to mint rOUSG
        ousgInstantManager.setInstantMintLimit(10_000_000e6);
    deal(address(USDC), alice, 8873775455486);
    vm.startPrank(alice);
    USDC.approve(address(ousgInstantManager), 8873775455486);
    ousgInstantManager.mintRebasingOUSG(8873775455486);
        vm.stopPrank();

    vm.startPrank(alice);
     rOUSGToken.transfer(bob,383116225977);
     uint sumOfBalanceBefore = rOUSGToken.balanceOf(alice)+rOUSGToken.balanceOf(bob);
     rOUSGToken.transferShares(bob, 1); //alice transfer 1 shares
       vm.stopPrank();
            uint256 sumOfBalanceAfter = rOUSGToken.balanceOf(alice)+rOUSGToken.balanceOf(bob);

     assert(sumOfBalanceAfter>sumOfBalanceBefore);
}
````
just copy past it in the rOUSG.t.sol file and setUp a fork environment.

## Tools Used
Echidna

## Recommended Mitigation Steps
In think the problem is that the system is too permissive about the sum of shares that you can transfer. I recommend to have a minimum amount for transferring shares .


## Assessed type

MEV
Partial Pausing of either mint or redeem can lead to arbitrage opportunities for users
Audit: 2024-03-ondo-finance-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/ousgInstantManager.sol#L780
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/ousgInstantManager.sol#L768


# Vulnerability details

Minting & Redeeming inside the OUSGInstantManager contract can be paused by the admin having the `PAUSER_ROLE` role.
However both these functions are not paused at once but can be done individually. Only the admin having the `DEFAULT_ADMIN_ROLE` can unpause.

*Note - The publicly known issues does address the arbitrage risks by front running but nowhere in the scope it is mentioned issues related with partial Pausing functionality.*

## Proof of Concept
`pauseMint()` & `pauseRedeem()` are called by the admin to pause minting & redeeming inside the instant manager contract.

```solidity
File: ousgInstantManager.sol

  function pauseMint() external onlyRole(PAUSER_ROLE) {
    mintPaused = true;
    emit MintPaused();
  }
  ...
  function pauseRedeem() external onlyRole(PAUSER_ROLE) {
    redeemPaused = true;
    emit RedeemPaused();
  }
```

### Scenario 1:

- Admin only pauses redeem by calling `pauseRedeem()`.
- Price of ousg starts falling but stays above the "MINIMUM_OUSG_PRICE".
- Some users decide to redeem to minimize their losses but the function reverts.
- Meanwhile minting is still active & other users are buying tokens at the decreased price to profit when the redeeming is enabled in the future & the price again goes up.
- The only choice for the previous users is to buy more tokens to minimize the losses but given the high minimum amount requirement for minting (100k usdc), only a few users might be able to. The rest have to suffer loss in this case.

### Scenario 2:

- Admin only pauses minting by calling `pauseMint()`.
- Price of ousg starts increasing.
- Previous users who have minted ousg tokens earlier (before pausing), start redeeming thereby making a profit.
- Rest of the users are forced to watch others make profit because they couldn't mint during paused phase.

## Impact
Partial pausing causes arbitrage opportunities to the users

## Tools Used
Manual Review

## Recommended Mitigation Steps
Make a single pause function that pauses both minting & redeeming at once by the pauser.


## Assessed type

Timing
Admin can’t burn tokens from either revoked KYC addresses or KYC addresses added to sanction list due to a check in _beforeTokenTransfer
Audit: 2024-03-ondo-finance-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-ondo-finance/blob/78779c30bebfd46e6f416b03066c55d587e8b30b/contracts/ousg/rOUSG.sol#L624


# Vulnerability details

## Impact

The current implementation of the `burn` function in the rOUSG contract restricts the ability of the admin with the `BURNER_ROLE` to burn tokens from addresses that have either been removed from the KYC list (`KYCRegistry::removeKYCAddresses`) or added to the external chainalysis's sanction list. This is because the `_beforeTokenTransfer` function enforces KYC checks for all token transfer operations, including burning tokens. As a result, if an address is no longer KYC-approved or has been sanctioned, the `burn` function will fail to execute, preventing the admin from burning tokens as intended. This limitation can hinder essential operations, affecting the integrity and functionality of the protocol.

## Proof of Concept

Let’s check the `burn` function which calls the internal `_burnShares` function (see line 632) :

```solidity
contracts/ousg/rOUSG.sol:
618:   /**
619:    * @notice Admin burn function to burn rOUSG tokens from any account
620:    * @param _account The account to burn tokens from
621:    * @param _amount  The amount of rOUSG tokens to burn
622:    * @dev Transfers burned shares (OUSG) to `msg.sender`
623:    */
624:   function burn(
625:     address _account,
626:     uint256 _amount
627:   ) external onlyRole(BURNER_ROLE) {
628:     uint256 ousgSharesAmount = getSharesByROUSG(_amount);
629:     if (ousgSharesAmount < OUSG_TO_ROUSG_SHARES_MULTIPLIER)
630:       revert UnwrapTooSmall();
631: 
632:     _burnShares(_account, ousgSharesAmount);
633: 
634:     ousg.transfer(
635:       msg.sender,
636:       ousgSharesAmount / OUSG_TO_ROUSG_SHARES_MULTIPLIER
637:     );
638:     emit Transfer(address(0), msg.sender, getROUSGByShares(ousgSharesAmount));
639:     emit TransferShares(_account, address(0), ousgSharesAmount);
640:   }

554:   function _burnShares(
555:     address _account,
556:     uint256 _sharesAmount
557:   ) internal whenNotPaused returns (uint256) {
558:     require(_account != address(0), "BURN_FROM_THE_ZERO_ADDRESS");
559: 
560:     _beforeTokenTransfer(_account, address(0), _sharesAmount);
561: 
562:     uint256 accountShares = shares[_account];
563:     require(_sharesAmount <= accountShares, "BURN_AMOUNT_EXCEEDS_BALANCE");
564: 
565:     totalShares -= _sharesAmount;
566: 
567:     shares[_account] = accountShares - _sharesAmount;
568: 
569:     return totalShares;
570:   }
```

As can be seen in `_burnShares` function it calls `_beforeTokenTransfer` in line 560 with the account that we want to burn tokens form. Here is the code of `_beforeTokenTransfer`:

```solidity
contracts/ousg/rOUSG.sol:
586:   function _beforeTokenTransfer(
587:     address from,
588:     address to,
589:     uint256
590:   ) internal view {
591:     // Check constraints when `transferFrom` is called to facliitate
592:     // a transfer between two parties