xanoutas/solidity-security-findings huggingface dataset:
Low level calls to accounts with no code will succeed in `multiexcall` function 
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/SourceBridge.sol#L160
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L126


# Vulnerability details

### Impact
Low level calls behave differently than function calls in Solidity. Calls at the EVM level `to accounts with no code are successful`, this is the expected and normal behavior. It is Solidity that adds checks to prevent accidental calls to accounts with no code while compiling code for normal function calls.

This means that if the target account has no code, then the `SourceBridge:multiexcall` will be executed successfully and the same for `rUSDYFactory:multiexcall` . An accidental mistake may go unnoticed and may also cause unexpected loss of funds, as this call may include call value for transferring `native currency`.

The `sponsor` has provided us with the following explanation regarding this `multiexcall` function: 
```Shell
"its a function to allow rescuing any arbitrary operation, mainly ERC20 and ERC721 but open to interacting with other contracts too."
``` 
and the function looks like:
- https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/SourceBridge.sol#L160

```solidity
  * @param exCallData Struct consisting of
   *       1) target - contract to call
   *       2) data - data to call target with
   *       3) value - eth value to call target with
   */
   function multiexcall(
    ExCallData[] calldata exCallData
  ) external payable override onlyOwner 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;
    }
  }
```

As written in the [solidity documentation](https://docs.soliditylang.org/en/develop/control-structures.html#error-handling-assert-require-revert-and-exceptions), the low-level functions call, delegatecall and staticcall return true as their first return value if the account called is non-existent, as part of the design of the EVM. Account existence must be checked prior to calling if needed.

### Proof of Concept
I make this PoC to illustrate better the issue.
Follow the following steps:

1. `copy and paste` this test in `forge-tests/SourceBridge.t.sol`

```solidity
    function testCallsToAccountsNoCodeSucceed() public {
    IMulticall.ExCallData[] memory calls = new IMulticall.ExCallData[](1);
    calls[0] = IMulticall.ExCallData(
      makeAddr("address with no code"),
      abi.encodeWithSignature("success()"),
      1 ether
    );
    // Target has no code
    assertEq(calls[0].target.code.length, 0);
    // Call succeeds even though the target has no code and no implementation
    usdyBridge.multiexcall{value: calls[0].value }(calls);
    console.log("Call succeeds even though the t
A user with the BURNER_ROLE role should be able to burn rUSDY tokens from the balance of a blocked address
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L672-L683
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L678
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L626-L655


# Vulnerability details

## Impact
There are functions for blocking users:
setBlocklist() - https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L698-L702
setAllowlist() - https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L709-L713
setSanctionsList() - https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L720-L724

Suppose the user has rUSDY tokens. Then it was added to the blocklist. His tokens will then be blocked.

There is a burn() function (https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L672-L683) where you can burn rUSDY tokens from any account . If a user is locked out, then their position must be forced to burn() in order for them to get their USDY tokens back. However, it is not possible to call the burn() function on a blocked user.

## Proof of Concept
The burn() function calls the _burnShares() function (https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L678). The _burnShares() function contains _beforeTokenTransfer() (https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L626-L655) which returns an error if the user is on the blocklist or not on the whitelist. So calling burn() will return an error.

## Tools Used
Manual review

## Recommended Mitigation Steps
Remove _beforeTokenTransfer() call from _burnShares() function


## Assessed type

Context
Every user's rUSDY balance can be changed suddenly by updating `RWADynamicOracle.ranges` before `block.timestamp`
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L75-L87
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L151-L163
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L217-L227


# Vulnerability details

## Impact
`RWADynamicOracle.overrideRange` and `setRange` can change the USDY price in rUSDY and the rUSDY balance in a tx.
1. Users cannot believe the rUSDY balance because `ranges` can be updated by an admin at any time.
2. When USDY price in rUSDY changes in a tx, a mev bot can front-run the tx by unwrap rUSDY to USDY and wrap to rUSDY after the USDY price increases.
   An opposite way is also possible when USDY price is going to decrease through `overrideRange`.

## Proof of Concept
1. Add the file in `forge-tests/POC2.t.sol`
2. run `yarn test-forge --mt test_change_balance_by_setRange`
3. run `yarn test-forge --mt test_change_balance_by_overrideRange`

I explain in `@audit` comments. 
```solidity
pragma solidity 0.8.16;

import "contracts/rwaOracles/RWADynamicOracle.sol";
import "lib/forge-std/src/console.sol";
import "forge-tests/USDY_BasicDeployment.sol";

contract Test_Dynamic_Oracle_POC is USDY_BasicDeployment {
    uint256 dailyIR = 1.00013368 * 1e27; // (1 + Daily IR, scaled to 1e27),
    uint256 rangeStartPrice = 1e18; // Range Initial price

    function setUp() public override {
        super.setUp();
        uint256 periodStart = 1690833600; // JUL 31 8pm UTC
        uint256 periodEnd = periodStart + 1 days;
        oracleUSDY = new RWADynamicOracle(
            address(this),
            address(this),
            address(this),
            periodStart,
            periodEnd,
            dailyIR,
            rangeStartPrice
        );
        vm.prank(guardian);
        rUSDYToken.setOracle(address(oracleUSDY));

        vm.warp(periodEnd + 30 days);
        assertEq(oracleUSDY.getPrice(), 1e18);

        uint256 toMint = 1e18;
        _mintRWAToUser(alice, toMint);
        vm.startPrank(alice);
        usdy.approve(address(rUSDYToken), toMint);
        rUSDYToken.wrap(toMint);
        vm.stopPrank();
    }

    function test_change_balance_by_overrideRange() public {
        assertEq(rUSDYToken.balanceOf(alice), 1e18);

        // @audit push one range
        uint256 newEndTimestamp = block.timestamp - 29 days;
        uint256 newDailyIR = 2e27;
        oracleUSDY.setRange(newEndTimestamp, newDailyIR);

        assertEq(oracleUSDY.getPrice(), 2e18);
        assertEq(rUSDYToken.balanceOf(alice), 2e18);

        // @audit DEFAULT_ADMIN_ROLE can override the daily interest rate in the past
        // This changes the USDY price in rUSDY and the rUSDY balance of alice
        newDailyIR = 4e27;
        newEndTimestamp += 1 days;
        oracleUSDY.overrideRange(1, newEndTimestamp - 1 days, newEndTimestamp, newDailyIR, 1e18);
        assertEq(oracleUSDY.getPrice(), 4e18);
        ass
No removal mechanism from blocklist, allowlist and sanctionsList
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L698-L724


# Vulnerability details

## Impact
rUSDY.sol has setBlocklist() (sets the blocklist address), setAllowlist() (sets the allowlist address), and setSanctionsList() (sets the sanctions list address) functions:
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L698-L724

However, the mechanism for removing addresses from arrays is not implemented anywhere. Even in third-party root files that implement the mechanism of these functions. Conditions may arise when an address needs to be removed from the blocklist, allowlist and sanctionsList. For example, an erroneous entry into these lists. But it will be impossible to remove them. This will negatively affect the operation of the service.

Since these lists are checked in the _beforeTokenTransfer() function, the following functions will be blocked for these users: transferShares(), wrap(), unwrap(), transfer() and even burn().

## Tools Used
Manual review

## Recommended Mitigation Steps
Add removal mechanism from blocklist, allowlist and sanctionsList


## Assessed type

Context
Incorrect calculation of totalSupply(), balanceOf() in rUSDY.sol if the rate is unlinked from $1
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L216-L218
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L226-L228


# Vulnerability details

## Impact
In rUSDY.sol, the functions totalSupply(), balanceOf() are calculated.

totalSupply() (https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L216-L218):

function totalSupply() public view returns (uint256) {
     return (totalShares * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR);
}

balanceOf() (https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L226-L228):

function balanceOf(address _account) public view returns (uint256) {
     return (_sharesOf(_account) * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR);
}

The calculated value of these two functions depends on oracle.getPrice(). The oracle.getPrice() function determines the USDY price. However, the documentation for the competition states:

"Where as the price of a single USDY token varies over time, the price of a single rUSDY token is fixed at a price of 1 Dollar, with yield being accrued in the form of additional rUSDY tokens."

That is, it is assumed that the price of the USDY token may change. Given the collapse of UST, the price of stablecoins can change both for an increase and for a decrease.

The calculation of the totalSupply() and balanceOf() functions may be incorrect in case of low values or decoupling of the USDY rate from $1 when it goes down.

## Proof of Concept
1. Low values

Suppose:
totalShares = 9000
USDY = 1 * 1e18
_sharesOf(_account) = 9000

Means:
totalSupply = (totalShares * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR) = (9000 * 1e18) / (1e18 * 10000) = 0
balanceOf = (_sharesOf(_account) * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR) = (9000 * 1e18) / (1e18 * 10000) = 0

2. At low values, totalSupply and balanceOf can be equal to 0, although this is not true. Also, when the price of USDY deviates, these parameters can change up and down. This will display incorrect information about the totalSupply and balanceOf variables.

Suppose:
Option 1:
totalShares = 9 * 1e18
USDY = 1.02 * 1e18
_sharesOf(_account) = 3 * 1e18

Option 2:
totalShares = 9 * 1e18
USDY = 0.99 * 1e18
_sharesOf(_account) = 3 * 1e18

Means:
Option 1:
totalSupply = (totalShares * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR) = (9 * 1e18 * 1.02 * 1e18) / (1e18 * 10000) = 9.18 * 1e14
balanceOf = (_sharesOf(_account) * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR) = (3 * 1e18 * 1.02 * 1e18) / (1e18 * 10000) = 3.06 * 1e14

Option 2:
totalSupply = (totalShares * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR) = (9 * 1e18 * 0.99 * 1e18) / (1e18 * 10000) = 8.91 * 1e14
balanceOf = (_sharesOf(_account) * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR) = (3 * 1e18 * 0.99 * 1e18) / (1e18 * 10000) = 2.97 * 1e14

This shows that the value of totalSupply and balanceOf depends on the USDY price. This contradicts the claim that rUSDY is worth one
Lack of Oracle Price Validation in rUSDY
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L388-L392
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L216-L218
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L397-L399
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L226-L228


# Vulnerability details

## Summary 
Ondo's custom oracle, RWADynamicOracle, is responsible for delivering the price of USDY to the rUSDY token contract. The oracle is called in four different functions for the price of USDY; the results of which are also used in core functions in the token contract. However, no checks exist in any of these functions to make sure the price returned by the oracle is a non-zero value.


## Impact
This could lead to unexpected results and exploits in core functionalities of the token contract which depend on the oracle like `burn`, `unwrap`, `_transfer`, `wrap`, `totalSupply` and `balanceOf`.

## Proof of Concept
Below are the four view functions where the price oracle is called:
```
function totalSupply() public view returns (uint256) {
        return (totalShares * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR); //@audit - no checks for 0 price from oracle
    }

function balanceOf(address _account) public view returns (uint256) {
        return
            (_sharesOf(_account) * oracle.getPrice()) /
            (1e18 * BPS_DENOMINATOR); //@audit - no checks for 0 price from oracle
    }

 function getSharesByRUSDY(
        uint256 _rUSDYAmount
    ) public view returns (uint256) {
        return (_rUSDYAmount * 1e18 * BPS_DENOMINATOR) / oracle.getPrice(); //@audit - no checks for 0 price from oracle
    }

function getRUSDYByShares(uint256 _shares) public view returns (uint256) {
        return (_shares * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR); //@audit - no checks for 0 price from oracle
    }

```
Next are some core functions where the result from the view functions above are used for computation and state management.
```
function burn(
        address _account,
        uint256 _amount
    ) external onlyRole(BURNER_ROLE) {
        uint256 sharesAmount = getSharesByRUSDY(_amount); //@audit - oracle results used here

        _burnShares(_account, sharesAmount);

        usdy.transfer(msg.sender, sharesAmount / BPS_DENOMINATOR);

        emit TokensBurnt(_account, _amount);
    }

function unwrap(uint256 _rUSDYAmount) external whenNotPaused {
        require(_rUSDYAmount > 0, "rUSDY: can't unwrap zero rUSDY tokens");
        uint256 usdyAmount = getSharesByRUSDY(_rUSDYAmount); //@audit - oracle results used here
        if (usdyAmount < BPS_DENOMINATOR) revert UnwrapTooSmall();
        _burnShares(msg.sender, usdyAmount);
        usdy.transfer(msg.sender, usdyAmount / BPS_DENOMINATOR);
        emit TokensBurnt(msg.sender, _rUSDYAmount);
    }

function _transfer(
        address _sender,
        address _recipient,
        uint256 _amount
Maybe you get the same txnHash for different transactions in DestinationBridge._execute()
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L85-L114


# Vulnerability details

## Impact
DestinationBridge._execute() is a function that is executed when contract is called by Axelar Gateway (https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L85 -L114). The function calculates bytes32 txnHash = keccak256(payload). txnHash is calculated based on payload. However, srcChain is not taken into account in payload.

(bytes32 version, address srcSender, uint256 amt, uint256 nonce) = abi
       .decode(payload, (bytes32, address, uint256, uint256));

Let's assume that a request with the same version, srcSender, amt, nonce has been received from different srcChain. This could very well happen. For these different requests, you get the same txnHash:

bytes32 txnHash = keccak256(payload);

This may result in the deletion of the valid txnHash. This will cancel the first txnHash and all its votes, as the current txnHash will replace the new one txnHashToTransaction[txnHash] = Transaction(srcSender, amt);

## Tools Used
Manual review

## Recommended Mitigation Steps
Add srcChain to payload.


## Assessed type

Context
Sync Approvers length
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L255


# Vulnerability details

## Impact
In the `setThresholds` function we see that the setting for the amount and approvers can be updated at any time, which presents a problem where pending tokens not sent to msg.sender might require less or more approvers than the current approvers.

## Proof of Concept
A straightforward proof of concept is having the destination chain having pending transfers due to a number of approvers called, the owner can then call the setThresholds for that chain with new amounts vs approvers and this could potentially be less than the original require approvals, causing delays in released funds. For Example If A user bridges 10000 tokens which requires 10 approvers based on previous setting and updated to 9 approvers before the number of previous approvals for tokens minting is reached, despite being more than the length of the new approval, the minting of the tokens will not be done or delayed until another approver approves again.

## Tools Used
Manual Review
## Recommended Mitigation Steps
Add a function to sync pending transfer to new approval length after number of approvers has changed


## Assessed type

DoS
Removing an already added src/dest contract address is not possible
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L234


# Vulnerability details

## Impact
Axelar currently supports around 10 EVM-compatible blockchains. However, if they decide not to support some of these blockchains in the future, it could potentially pose a problem for contracts. Once `destinationChain` or `srcChain` has been added to `SourceBridge/DestinationBridge`, it can not be removed. The contract can be paused by the owner, but the corresponding contracts on the other blockchains will still support them and transactions can still be executed to them.

```solidity
  function setDestinationChainContractAddress(
    string memory destinationChain,
    address contractAddress
  ) external onlyOwner { 
    //@audit M1: what if Axelar remove some blockchain
    destChainToContractAddr[destinationChain] = AddressToString.toString(
      contractAddress
    );
    emit DestinationChainContractAddressSet(destinationChain, contractAddress);
  }
```
And in the DestinationBridge contract:

```solidity
  function addChainSupport(
    string calldata srcChain,
    string calldata srcContractAddress
  ) external onlyOwner {
    chainToApprovedSender[srcChain] = keccak256(abi.encode(srcContractAddress));
    emit ChainIdSupported(srcChain, srcContractAddress);
  }
```

Removing an already added src/dest contract address is not possible.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add the possibility to remove support for some of the blockchains in the future.


## Assessed type

Context
Use call instead of transfer
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L680 https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L454


# Vulnerability details

# Summary

In multiple functions, transfer() is used for native ETH transfers like `rUSDy#unwrap`,`rUSDy#burn`, and `DestinationBridge#rescueTokens`.

The transfer() and send() functions forward a fixed amount of 2300 gas.

For most of the time in the past, it is recommended to use these functions for value transfers to guard against reentrancy attacks. 

However, the gas cost of EVM instructions may change significantly during hard forks which may break already deployed contract systems that make fixed assumptions about gas costs. For example. EIP 1884 broke several existing smart contracts due to a cost increase in the SLOAD instruction.

# Impact
The use of the deprecated transfer() function for an address will inevitably make the transaction fail when:

The receiver smart contract does not implement a payable function.
The receiver smart contract does implement a payable fallback which uses more than 2300 gas unit.
The receiver smart contract implements a payable fallback function that needs less than 2300 gas units but is called through a proxy, raising the call's gas usage above 2300.

Additionally, using more than 2300 gas might be mandatory for some multisig wallets.

# Recommended Mitigation
Use call() instead of transfer().









## Assessed type

ETH-Transfer