xanoutas/solidity-security-findings huggingface dataset:
The user will receive more/less funds when calling unwrap() if the price of USDY falls/rises than expected
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
There is a wrap() function (https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L434-L440) called by users to wrap their USDY tokens . In the future, to withdraw tokens, the user calls the unwrap() function (https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L449-L456). However, in the unwrap() function, the user can have more funds in case the price of USDY falls. Based on the case with the UST, this may well happen.

## Proof of Concept
1. Suppose the user has called the wrap(uint256 _USDYAmount) function with the input parameter _USDYAmount = 90000 * 1e18
2. User got shares = _USDYAmount * BPS_DENOMINATOR = 90000 * 10000 = 90000 * 1e18 * 1e4

In RUSDY this is (_shares * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR) = (90000 * 1e18 * 1e4 * 1 * 1e18) / (1e18 * 10000) = 90000 * 1e18

3. Then the user called the unwrap(uint256 _rUSDYAmount) function with the input parameter _rUSDYAmount = 6000 * 1e18
4. In https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L451 usdyAmount = getSharesByRUSDY(_rUSDYAmount) is calculated:

usdyAmount = (_rUSDYAmount * 1e18 * BPS_DENOMINATOR) / oracle.getPrice()

Let's say the price of USDY went down and became oracle.getPrice() = 1e17

usdyAmount = (6000 * 1e18 * 1e18 * 10000) / 1e17 = 6000 * 1e5 * 1e18

The verification at https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L452 will pass.
if (usdyAmount < BPS_DENOMINATOR) revert UnwrapTooSmall();


5. In https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L454, the transfer is made to the user and the transfer amount is finally calculated:

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

usdyAmount / BPS_DENOMINATOR = 6000 * 1e18 * 1e5 / 10000 = 60000 * 1e18

The user will receive 60000 USDY. Although it only displayed _rUSDYAmount = 6000.

In this case, its remaining shares will be:
90000 * 1e18 * 1e4 - 6000 * 1e18 * 1e5 = 30000 * 1e18

That is, the user withdrew _rUSDYAmount = 6000, but received 60000 USDY. But this did not affect the remaining shares.

The opposite situation also occurs in the case of an increase in the price of USDY.

## Tools Used
Manual review

## Recommended Mitigation Steps
Make a peg to 1$


## Assessed type

Context
Rounding in the `unwrap` function in rUSDY may cause fund loss for users.
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

### Impact
Rouding with `BPS_DENOMINATOR` in function `unwrap` in rUSDY.sol may cause users to be transfered back less than expected USDY.

### POC
The rUSDY.sol contract provides a way to wrap an amount of USDY as shares in order to gain profit in rUSDY. This is done through the `wrap()` function, which users deposit an amount of USDY to have their internal USDY contract balance increased by that amount multiplied by `BPS_DENOMINATOR` (1e5).

Users can redeem a specified amount of rUSDY with the `unwrap()` function, which burns the equivalent shares from user's internal contract balance and transfers back USDY back to the user.

In particular, the `unwrap()` function first computes the removed amount from the contract through function `getSharesbyRUSDY()`, which is multiplied by `BPS_DENOMINATOR` on line [391](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L391). This amount is then directly removed from the user's internal USDY contract balance (which is also multiplied by `BPS_DENOMINATOR`). Then, the amount is divided by `BPS_DENOMINATOR`, and the result is returned to the user on line [454](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L454). However, due to the implicit rounding in Solidity to round down, the amount of USDY shares burned from the contract's internal USDY balance may not be equivalent to the amount of USDY transfered back to the user.

#### Scenario:
We use the intial price range used in the test cases of:
DailyIR: 1.0013368 * 1e27 ([here](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/forge-tests/rusdy/rUSDY_dynamic.t.sol#L10))
Start: July 31([here](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/forge-tests/rusdy/rUSDY_dynamic.t.sol#L8))
End: August 31([here](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/forge-tests/rusdy/rUSDY_dynamic.t.sol#L9))
Starting Price: 1e18 ([here](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/forge-tests/rusdy/rUSDY_dynamic.t.sol#L11))

A user wants to redeem 1e18 rUSDY after 7 days. He or she has already wrapped a sufficient amount of USDY.

Simulating the daily interest rate given in the test cases as 1.00013368 * 1e27, the price after seven days will be 1000936140000000000. This is done through the `getPrice()` function call, code of which is provided below:
```solidity=
function test_get_price_seven() public{
      vm.warp(periodStart + 7 days);
      console.log("Price: ", oracleUSDY.getPrice());
  }
```

This result will be returned by `oracle.getPrice()` in `getSharesByRUSY()` (line [391](https://github.com/code-423n4/2023-09-ondo
All the funds will be lost if the destination bridge is paused
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Destination bridge is pausable, so if for a chain a destination bridge is paused, all the funds being bridged from different source bridges from different chains will be lost.
## Proof of Concept
Destination bridge inherits from the openzeppelin pausable.sol. And _execute function cannot be called if the contract is paused. As discussed in my other submissions for one transaction if _execute is once called and it reverts it become not replayable ever again.

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

```solidity
  function _execute(
    string calldata srcChain,
    string calldata srcAddr,
    bytes calldata payload

  // @audit contract is pausable and hence is vulnerable.
  ) internal override whenNotPaused {
    (bytes32 version, address srcSender, uint256 amt, uint256 nonce) = abi
      .decode(payload, (bytes32, address, uint256, uint256));

    if (version != VERSION) {
      revert InvalidVersion();
    }
    if (chainToApprovedSender[srcChain] == bytes32(0)) {
      revert ChainNotSupported();
    }
    if (chainToApprovedSender[srcChain] != keccak256(abi.encode(srcAddr))) {
      revert SourceNotSupported();
    }
    if (isSpentNonce[chainToApprovedSender[srcChain]][nonce]) {
      revert NonceSpent();
    }

    isSpentNonce[chainToApprovedSender[srcChain]][nonce] = true;

    bytes32 txnHash = keccak256(payload);
    txnHashToTransaction[txnHash] = Transaction(srcSender, amt);
    _attachThreshold(amt, txnHash, srcChain);
    _approve(txnHash);
    _mintIfThresholdMet(txnHash);
    emit MessageReceived(srcChain, srcSender, amt, nonce);
  }
```

So the _execute function must not revert at any point else the tokens burnt on source chain are done and dusted.

As there are multiple chains and each have its own source and destination contracts, there is always a chance that if some source is working but the destination contract for that is paused hence the _execute function called by the axelar node will revert.

## Tools Used
Manual review
## Recommended Mitigation Steps
Will need to change the whenNotPaused modifier in a way that if the protocol is paused, simply mint the tokens incoming to a recovery address controlled by the ondo. So those tokens can be than dealt with later on.


## Assessed type

Other
abi.encode() function does not support dynamic arrays in Solidity version 0.8.16 or earlier.
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description

The bug is in the burnAndCallAxelar() function. The function uses the abi.encode() function to encode the payload to send to the AxelarGateway contract. However, the abi.encode() function was changed in Solidity version 0.8.17 to remove the support for dynamic arrays. This means that the burnAndCallAxelar() function will not work correctly if it is compiled with Solidity version 0.8.16 or earlier.


## Impact 

The impact of this bug is that if the code is compiled with Solidity version 0.8.16 or earlier, the burnAndCallAxelar() function will fail to execute. This could result in the loss of tokens, if the burnFrom() function is able to successfully execute.

The burnFrom() function will only be able to successfully execute if the payload variable is not a dynamic array. If the payload variable is a dynamic array, the burnFrom() function will fail to execute and revert with the error message abi.encode: dynamic arrays are not supported.

## Proof of concept 

1 . Compile the sourcebridge with Solidity version 0.8.16 or earlier.
2 . Deploy the contract to a testnet.
3 . Call the burnAndCallAxelar() function with a dynamic array as the payload.
4 . The function will fail to execute.

**Expected behavior:** The burnAndCallAxelar() function should successfully execute and mint the requested amount of tokens on the destination chain.

**Actual behavior:** The burnAndCallAxelar() function will fail to execute and revert with the error message abi.encode: dynamic arrays are not supported.

**Cause:** The abi.encode() function was changed in Solidity version 0.8.17 to remove the support for dynamic arrays. This is because dynamic arrays are not supported by the Ethereum Virtual Machine (EVM).

## Recommendation 


The fix for this bug is to update the abi.encode() function to use the new signature. The new signature is:

abi.encode(types...)



in another Case ,

If the contract uses Solidity version 0.8.17 or later, the burnAndCallAxelar() function will not have any problems. The abi.encode() function will be able to successfully encode the payload variable, and the burnAndCallAxelar() function will be able to execute without error.


## Tools used

Manual Review 


## Assessed type

Other
Users will not receive on Token on Destination chain because the internal `_executeWithToken(...)` function is not implemented.
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L28
https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/external/axelar/AxelarExecutable.sol#L69


# Vulnerability details

## Impact
Loss of tokens on the Destination chain.

## Proof of Concept
The DestinationBridge.sol contract inherits `AxelarExecutable.sol` which has the `_executeWithToken(...)` that's called at the end of `executeWithToken(...)` function.

The `_executeWithToken(...)` is not implemented and just has an empty block that does nothing.
```
// false detection from slither
  // slither-disable-next-line dead-code
  function _executeWithToken(
    string calldata sourceChain,
    string calldata sourceAddress,
    bytes calldata payload,
    string calldata tokenSymbol,
    uint256 amount
  ) internal virtual {}
```
and the above internal function that does nothing is called in the `executeWithToken` function below.
```
function executeWithToken(
    bytes32 commandId,
    string calldata sourceChain,
    string calldata sourceAddress,
    bytes calldata payload,
    string calldata tokenSymbol,
    uint256 amount
  ) external {
    bytes32 payloadHash = keccak256(payload);

    if (
      !gateway.validateContractCallAndMint(
        commandId,
        sourceChain,
        sourceAddress,
        payloadHash,
        tokenSymbol,
        amount
      )
    ) revert NotApprovedByGateway();

    _executeWithToken(sourceChain, sourceAddress, payload, tokenSymbol, amount);
  }
```
## Tools Used
Manual Review

## Recommended Mitigation Steps
Implement the internal `_executeWithToken(...)` function. Currently the `_executeWithToken(...)` internal function is just an empty block.


## Assessed type

Other
Stealing extra mint fund by applying reentrancy attack on _execute with calling approve() again due to external call before crucial state update
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
By applying reentrancy attack involving the function _mintIfThresholdMet(), an user can steal extra amount of mint fund.

## Proof of Concept
The functions _mintIfThresholdMet() make external mint call prior to updating the txnHashToTransaction state. If the real world asset tokens contains an ERC777 token, attacker can mint free RWA tokens again.

- Bob has a contract that is an approver
A sample bob contract has this:
```solidity
interface IBridge {
    function approve(bytes32 txnHash) external {}
}
contract AttackBridge {
    
    address private owner;
    IBridge private bridge;

    IERC1820Registry internal constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);

    bytes32 txnHash;
    
    constructor(address _bridge, bytes32 _txnHash) {
        owner = msg.sender;
        bridge = Bridge(_bridge);
        txnHash = _txnHash;

        _ERC1820_REGISTRY.setInterfaceImplementer(
            address(this),
            _ERC1820_REGISTRY.interfaceHash("ERC777TokensRecipient"),
            address(this)
        );
    }
    
    function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) external {
        require(msg.sender == address(bridge), "not bridge");

        bridge.approve(txhash);
    }

}
```
- Bob uses contract to make bridge operation
- _execute function gets called on destination chain contract which call the _mintIfThresholdMet.
- after receiving mint, contract reenter before transaction state is deleted and calls approve to mint again

## Tools Used
Visual Studio Code
## Recommended Mitigation Steps
Move external calls after state updates. It is best practice to make external calls after updating state in accordance with the check-effect-interact pattern.
so specifically, change [this](https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L349) line of code to this:
```diff
-      TOKEN.mint(txn.sender, txn.amount);
-     delete txnHashToTransaction[txnHash];
+      delete txnHashToTransaction[txnHash];
+      TOKEN.mint(txn.sender, txn.amount);
```


## Assessed type

Reentrancy
Tokens transferred with bridge can get lost if destination transaction can’t be executed
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L145
https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L89


# Vulnerability details

## Impact
User could lost his tokens, if tx on destination chain will fail(revert). There is no option to recover the tokens on source chain. Revert could be cause by function _attachThreshold(). This function finds the correspondence between the amount and threshhold. If a match is not found, then tx wil revert (revert NoThresholdMatch()). For example, this may happen due to the fact that DestinationBridge has pause mode enabled at the moment, or owner of the bridge has not configured the appropriate setting for the amount that the user sent.

## Proof of Concept
1. User call burnAndCallAxelar() on source chain. Tokens will burn and contract send message to AxelarGateway.
2. On destination chail AxelarGateway call internal function _execute, which was override in DestinationBridge.sol and has modifier whenNotPaused. 
3. DestinationBridge has pause mode enabled at the moment
4. Tx will revert.
5. User can't recover his burned tokens on source chain.

## Tools Used
Manual review

## Recommended Mitigation Steps

Provide a cancel option if the transaction failed, from the source chain or destination chain, and allow the user to get the token refund.



## Assessed type

Other
Reorgs could revert the setRange function and lead to a long lasting stale price of USDY
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/rwaOracles/RWADynamicOracle.sol#L158


# Vulnerability details

## Summary

Reorgs could revert the setRange function and therefore lead to stale prices for a long time (depending on the off chain protection, against it)

## Vulnerability Details

Here is the setRange function of the USDY price oracle:

```solidity
function setRange(
  uint256 endTimestamp,
  uint256 dailyInterestRate
) external onlyRole(SETTER_ROLE) {
  Range memory lastRange = ranges[ranges.length - 1];

  // Check that the endTimestamp is greater than the last range's end time
  if (lastRange.end >= endTimestamp) revert InvalidRange();

  uint256 prevClosePrice = derivePrice(lastRange, lastRange.end - 1);
  ranges.push(
    Range(lastRange.end, endTimestamp, dailyInterestRate, prevClosePrice)
  );
  emit RangeSet(
    ranges.length - 1,
    lastRange.end,
    endTimestamp,
    dailyInterestRate,
    prevClosePrice
  );
}
```

As we can see, the function will revert if lastRange.end >= endTimestamp. Depending on the time the setter calls this function and the duration of these ranges, a reorg could potentially lead to a revert of this function and if this is not recognized directly and could potentially lead to a long lasting stale price.

## Impact

Long lasting stale price of the USDY price oracle, which could lead to a variety of problems

## Tools Used

Manual Review

## Recommendations

Use a duration instead of a fixed timestamp inside setRange, so that there is no need to check if lastRange.end > endTimestamp, because it will just set lastRange.end + duration as the next endTimestamp.


## Assessed type

Oracle
Missing Slippage Protection in unwrap function
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L451


# Vulnerability details

## Summary

The unwrap function swaps rUSDY to USDY and calls the oracle during these process to get the current USDY price. There is no slippage protection implemented, which can lead to loss of funds.

## Vulnerability Details

Unexpected changes between the call to the unwrap function and the time the transaction is finalized in a block, can lead to a trade the user did not intend to execute. This can lead to a loss of user funds, a bad user experience and depending on further development probably also MEV opportunities which are bad for the protocol. Here is one example in which the missing slippage protection could lead to a loss of user funds:

The current price of USDY is calculated by this function:

```solidity
function derivePrice(
  Range memory currentRange,
  uint256 currentTime
) internal pure returns (uint256 price) {
  uint256 elapsedDays = (currentTime - currentRange.start) / DAY;
  return
    roundUpTo8(
      _rmul(
        _rpow(currentRange.dailyInterestRate, elapsedDays + 1, ONE),
        currentRange.prevRangeClosePrice
      )
    );
}
```

This calls the roundUpTo8 function:

```solidity
function roundUpTo8(uint256 value) internal pure returns (uint256) {
  uint256 remainder = value % 1e10;
  if (remainder >= 0.5e10) {
    value += 1e10;
  }
  value -= remainder;
  return value;
}
```

And as we can see inside the roundUpTo8 function, if the current value would fluctuate around 0.5e10, the price could very likely change during the process of calling unwrap() and the transaction getting finalized inside a block. Other examples would be an update via the setRange function, or a reorg which leads to a revert of the current range.

## Impact

Loss of funds

## Tools Used

Manual Review

## Recommendations

Implement a slippage protection mechanism into this function.


## Assessed type

Invalid Validation
Risk  of Token Get Stuck  in burnAndCallAxelar Function due  to  Use of  Same Chain ID
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The burnAndCallAxelar function within the contract presents a potential risk where tokens can become stuck if the same chain ID is used for both the source and destination chains. In this scenario, tokens are burned from the user's account, and a cross-chain call is attempted to the same chain. If the cross-chain call to the same chain fails, the burned tokens may become irrecoverable, leading to potential financial loss for the user. This issue poses a risk to the integrity of cross-chain operations within the contract.

## Proof of Concept


  function burnAndCallAxelar(
    uint256 amount,
    string calldata destinationChain
  ) external payable whenNotPaused {
    // check destinationChain is correct
    string memory destContract = destChainToContractAddr[destinationChain];

    if (bytes(destContract).length == 0) {
      revert DestinationNotSupported();
    }

    if (msg.value == 0) {
      revert GasFeeTooLow();
    }

    // burn amount
    TOKEN.burnFrom(msg.sender, amount);

    bytes memory payload = abi.encode(VERSION, msg.sender, amount, nonce++);

    _payGasAndCallContract(destinationChain, destContract, payload);
  }


The burnAndCallAxelar function introduces a potential vulnerability when the same destinationChain is specified as the source chain. If the user initiates a cross-chain call to the same chain (i.e., the source and destination chains are identical), the following sequence of events can occur:

Tokens Burned: The function proceeds to burn tokens from the user's account based on the amount provided.

Cross-Chain Call Attempt: After burning the tokens, the function attempts a cross-chain call to the same chain using the same destinationChain.

Potential Issue: Since the source and destination chains are the same, the cross-chain call may not function as expected. The destination chain may not recognize the call, or the contract execution may not occur as intended.

Token Stuck: As a result of the cross-chain call failure, the burned tokens may effectively become stuck or irrecoverable, as there is no valid destination for them.

## Tools Used
Manual review

## Recommended Mitigation Steps
Implement validation to ensure that the destinationChain is not the same as the source chain before proceeding with token burning and cross-chain calls. Prevent the tokens from being burned if the chains match.


## Assessed type

Invalid Validation