xanoutas/solidity-security-findings huggingface dataset:
Front-running Vulnerability Allows Unauthorized Contract Control
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The identified vulnerability allows malicious actors to prematurely execute a contract's initialization function, potentially enabling unauthorized control over contract configuration and security. This vulnerability could result in unintended parameter changes and the execution of malicious code by unauthorized parties.

## Proof of Concept


    function initialize(
    address blocklist,
    address allowlist,
    address sanctionsList,
    address _usdy,
    address guardian,
    address _oracle
    ) public virtual initializer {
    __rUSDY_init(blocklist, allowlist, sanctionsList, _usdy, guardian, _oracle);
    }

## Tools Used
Manual review

## Recommended Mitigation Steps
Modify the initializer function to only allow a specific address (e.g., the contract deployer) to call it. You can achieve this by adding a modifier that checks msg.sender before executing the initialization logic.



## Assessed type

Access Control
High Vulnerability in SourceBridge Contract: Loss of Tokens on Failed Destination Chain Calls
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Smart contract calls often revert. In such cases any ether sent along with the transaction is returned and sometimes the remaining gas (depending on whether an assert caused the reversion or not).

For contracts involving ERC20 tokens it is also expected that, should a contract call fail, one’s tokens are not lost/transferred elsewhere.

The function burnAndCallAxelar function does not appear to take contract call failure on the destination chain into account, even though this could be quite a common occurrence.

Tokens are burned on line 77 but there is no mechanism in the code base to return these burned tokens in the case that the contract call fails on the destination chain.

The impact is that users can lose funds in the process.

## Proof of Concept
Due to limitations; I can only provide a test scenario and a reference to confirm this vulnerability:

1. Deploy the necessary contracts and set up the conditions required for the test. Deploy the SourceBridge contract and configuring a destination chain.

2. Simulate a scenario where the contract call on the destination chain fails. For example, not enough token to burn or any other condition that would to cause the contract call to fail (apart from the already existing parameters)

3. Call the vulnerable function (burnAndCallAxelar). We expect this function to fail due to the vulnerability however the token are already burnt and the minting process halted due to the failed call on the destination chain. 

See reference: 
https://github.com/code-423n4/2022-04-axelar-findings/issues/35

## Tools Used
Manual

## Recommended Mitigation Steps
To address this vulnerability, Ondo Finance should implement error handling and a mechanism to revert the token burn (minting and refunding) on the source chain if the contract call on the destination chain fails. This way, users will not lose their tokens in case of failures during cross-chain transactions.


## Assessed type

call/delegatecall
Tokens can get stuck in bridge
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
A potential scenario arises wherein a user, aiming to bridge their tokens but providing insufficient gas (greater than 0 but inadequate), could encounter their tokens becoming stranded within the bridge. Should the user lack the financial means to augment the gas using `addNativeGas()`, or if the destination chain experiences an outage, their tokens may remain inaccessible indefinitely.

## Proof of Concept
I have adapted the `test_bridge` test found in `SourceBridge.t.sol`. In this instance, we initiate a call to `burnAndCallAxelar()` while forwarding a mere one wei of value, an amount that is clearly insufficient. Consequently, the tokens are still burned, leading to the user's complete token loss until they can forward an adequate amount of gas to genuinely bridge their tokens.

```diff
diff --git a/forge-tests/bridges/SourceBridge.t.sol.orig b/forge-tests/bridges/SourceBridge.t.sol
index 6b9c083..08e9b51 100644
--- a/forge-tests/bridges/SourceBridge.t.sol.orig
+++ b/forge-tests/bridges/SourceBridge.t.sol
@@ -155,7 +155,7 @@ contract Test_SourceBridge is USDY_BasicDeployment, SourceBridgeEvents {
       "optimism",
       AddressToString.toString(mockReceiver),
       payloadHash,
-      0.01 ether,
+      1,
       alice
     );
 
@@ -171,12 +171,9 @@ contract Test_SourceBridge is USDY_BasicDeployment, SourceBridgeEvents {
 
     // Bridge Tokens
     vm.prank(alice);
-    usdyBridge.burnAndCallAxelar{value: 0.01 ether}(100e18, "optimism");
+    usdyBridge.burnAndCallAxelar{value: 1}(100e18, "optimism");
 
-    assertEq(usdy.balanceOf(address(usdyBridge)), 0);
     assertEq(usdy.balanceOf(alice), 0);
-    assertEq(alice.balance, 0.99 ether);
-    assertEq(AXELAR_GAS_SERVICE.balance, gasReceiverBalanceBefore + 0.01 ether);
   }
 
   modifier initializeAlice() {
```

Additionally (a case that I cannot showcase in Foundry), the destination chain could be suffering from an outage, which would result in the minting not happening, but the burning having already happened. In this case, it would be also very helpful to have a possibility for users to recover their tokens.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Addressing this issue presents challenges, as there are limitations in Axelar's functionality to cancel transactions directly from the EVM. Several potential solutions exist, each with its trade-offs:

1. **Hold Tokens Until Minting Confirmation**: One approach is to refrain from burning any tokens until confirmation of their successful minting on the destination bridge. This approach, while seemingly straightforward, introduces new attack vectors and necessitates an oracle to validate token minting on the destination chain. However, it is the easiest but least ideal solution due to its security risks.

2. **Introduce an External "Cancel" Service**: A more robust solution involve
If AXELAR_GATEWAY.callContract fails after the tokens have been burnt on the source chain, the user's tokens would be permanently lost.
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/SourceBridge.sol#L77-L81


# Vulnerability details

## Impact
The user permanently loses their tokens with no way to recover them. The tokens are burnt on the source chain but never minted on the destination chain.

## Proof of Concept
If AXELAR_GATEWAY.callContract fails after the tokens have been burnt on the source chain, the user's tokens would be permanently lost.

The tokens are burnt first using TOKEN.burnFrom before calling _payGasAndCallContract which calls AXELAR_GATEWAY.callContract.

So if AXELAR_GATEWAY.callContract fails for any reason after the burn, the tokens are gone from the source chain but were never minted on the destination chain. The user has no way to recover their tokens.
This could happen if:
- The Axelar network is having issues and the message doesn't get delivered
- The contract on the destination chain fails or rejects the mint for some reason
- Any other failure in the cross-chain message passing
## Tools Used
Manual
## Recommended Mitigation Steps 
The call to burn the tokens should be wrapped in the same call as the cross-chain message passing


## Assessed type

Other
Users will get less amount of rUSDY when using wrap() due to rounding error in getRUSDYByShares()
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Users will get less amount of rUSDY when using wrap() due to rounding error in getRUSDYByShares()

## Proof of Concept
The function `getRUSDYByShares()` returns the `amount` of `rUSDY` that corresponds to `_shares` of `usdy`

``` function getRUSDYByShares(uint256 _shares) public view returns (uint256) {
    return (_shares * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR);
  }
```
but due to division before multiplication in `return (_shares * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR)` precision loss and rounding error can get less rUSDY than the actual required amount.
And in wrap() function

```function wrap(uint256 _USDYAmount) external whenNotPaused {
    require(_USDYAmount > 0, "rUSDY: can't wrap zero USDY tokens");
    _mintShares(msg.sender, _USDYAmount * BPS_DENOMINATOR);
    usdy.transferFrom(msg.sender, address(this), _USDYAmount);
    emit Transfer(address(0), msg.sender, getRUSDYByShares(_USDYAmount));
    emit TransferShares(address(0), msg.sender, _USDYAmount);
  }
```

```
emit Transfer(address(0), msg.sender, getRUSDYByShares(_USDYAmount));
```

since there is less amount returned by `getRUSDYByShares(_USDYAmount)` user will get less amount of `rUSDY` while wrapping certain amount of  `USDY` token.
## Tools Used
Manual Review
## Recommended Mitigation Steps
Try implementing multiplication before division in `getRUSDYByShares()` function to avoid rounding off error





## Assessed type

Math
The ordering of approve() and mintIfThresholdMet() could allow approving a transaction and minting tokens even if the threshold is not met. 
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L197-L203
https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L337-L340


# Vulnerability details

## Impact
Threshold system is bypassed and anyone can mint tokens without proper approvals.

## Proof of Concept
True, the current ordering of _approve() and _mintIfThresholdMet() in the DestinationBridge contract could allow approving a transaction and minting tokens even if the threshold is not met.

Here is how the vulnerability works:
- The _execute() function calls _approve() before _mintIfThresholdMet().
- _approve() allows the caller to approve the transaction by adding themselves to the approvers array without checking if the threshold is met.
- Then _mintIfThresholdMet() checks if the threshold is met and mints tokens if so.
- But since _approve() was already called, the caller could have added themselves as an approver and increased the approvers length even if the threshold was not originally met.
- So _mintIfThresholdMet() will now think the threshold is met since the approvers length was increased in _approve().

In summary, the issue is that _approve() is called before _mintIfThresholdMet(). This allows approving a transaction without checking if the threshold is met.

This allows anyone to artificially increase the approvers length and cause tokens to be minted without enough real approvers.


## Tools Used

## Recommended Mitigation Steps 
_mintIfThresholdMet() should be called before _approve(), to check the threshold before allowing a new approval.


## Assessed type

Other
Removed approvers are not cleaned up from past approvals in this contract. Their past approvals still count even if they are no longer authorized.
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
malicious approver to approve transactions, get removed, but still have their past approvals count.

## Proof of Concept
This simply checks if the number of approvers in the TxnThreshold struct is greater than or equal to the number needed. It does not check that those approvers are still authorized.

So if Approver A and B approve a transaction, then Approver A is removed, the transaction will still pass the threshold check since A's past approval still counts.

The impact is that removed approvers can still contribute to approving transactions if they approved before being removed.

This could allow for an attack where:

- Malicious temporary approvers are added
- They collude to approve a malicious transaction
- They are then removed as approvers
- But their past approvals still count, allowing the malicious transaction to go through

## Tools Used
Manual
## Recommended Mitigation Steps
_checkThresholdMet function could be updated to check that all approvers are still authorized


## Assessed type

Other
The approve() and mintIfThresholdMet() functions are called separately. This could allow approving a transaction without minting tokens if the threshold is not yet met
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L158-L161
https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L338
https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L340-L341


# Vulnerability details

## Impact
Transactions could be approved but never minted if the threshold is never reached. Tokens would be locked up indefinitely.

## Proof of Concept
The vulnerability is that transactions could be approved but never mint tokens if the approval threshold is never reached.
the _approve() and _mintIfThresholdMet() functions being called separately can allow approving a transaction without minting tokens if the threshold is not yet met.

Here is how it happens:

- _approve() is an internal function that is called when approve() is called by an approver. It adds the msg.sender to the approvers array for that transaction.
- _mintIfThresholdMet() is an internal function that mints tokens if the threshold is met. It is called at the end of _approve() and in the external _execute() function.
- By calling _approve() separately, an approver can add their approval without calling _mintIfThresholdMet(). This allows approvers to approve a transaction without minting tokens.

For example:
• Transaction requires 5 approvals
• 3 approvers call _approve()
• The transaction is approved by 3 approvers but the threshold is not reached
• Tokens are never minted
This could potentially lock user funds if the approval threshold is too high.


## Tools Used
Manual
## Recommended Mitigation Steps
- Require _mintIfThresholdMet() to be called after every _approve(), instead of only calling it sometimes
- Move the token minting logic into the _approve() function


## Assessed type

Other
Wrap function should not be minting shares
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Each share corresponds to an increasing number of `rUSDY` token. In the `wrap()` function, each `USDY` is converted to one share of `rUSDY`, which means that as interest accrues, one `USDY` can be converted to more than one `rUSDY`. 

## Proof of Concept
https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L434-L440

## Tools Used

Manual Review

## Recommended Mitigation Steps

Each `USDY` should be converted to one `rUSDY`, instead of one share of `rUSDY`, so that interest only accrues after a user wraps `USDY` to `rUSDY`, and not starting all the way from the first timestamp that interest starts accruing. 


## Assessed type

Other
Contract Could Run Out of USDY balance
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L449-L456
https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L672-L683
https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L434-L440


# Vulnerability details

## Impact
There's one source and two sinks for USDY in the `rUSDY` contract. 

Source: `wrap()` function
Sink 1: `unwrap()` function
Sink 2: `burn()` function

There is no function that increases USDY balance in the contract that reflects the interest accrual. 

As such, if interest is paid out, the payout will come from other users' deposit (via the `wrap()` function). This means that some users will end up unable to withdraw the full balance of their USDY from the contract. 

## Proof of Concept

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

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

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


## Tools Used

Manual Review

## Recommended Mitigation Steps

There should be a function that ensures that USDY balance in the contract increases in sync with accrued interest. 


## Assessed type

Other
Interest Does Not Accrue
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The `rUSDY` token is supposed to be the interest bearing version of the `USDY` token. When a user wraps `USDY` to `rUSDY`, interest should accrue, such that when the user unwraps `rUSDY` back to `USDY`, the new `USDY` balance should include the accrued interest, and should be higher than the user's initial `USDY` balance. However, the `unwrap()` function calculates the `USDY` amount incorrectly, such that all the accrued interest is lost in the unwrapping process, and the user does not receive any interest income. 


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

## Proof of Concept
```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_rUSDY_1 is USDY_BasicDeployment {
  uint256 periodStart = 1690833600; // JUL 31 8pm UTC
  uint256 periodEnd = 1693512000; // AUG 31 8pm UTC
  uint256 dailyIR = 1.00013368 * 1e27; // (1 + Daily IR, scaled to 1e27),
  uint256 rangeStartPrice = 1e18; // Range Initial price

  function setUp() public override {
    super.setUp();
    oracleUSDY.overrideRange(
      0,
      periodStart,
      periodEnd,
      dailyIR,
      rangeStartPrice
    );
    vm.warp(periodStart + 1);
  }

  function test_rUSDY_interest_accrual() public dealAlice{
    vm.startPrank(alice);
    usdy.approve(address(rUSDYToken), 100e18);
    rUSDYToken.wrap(100e18);

    assertEq(100e18, rUSDYToken.balanceOf(alice));
    assertEq(10_000 * 100e18, rUSDYToken.sharesOf(alice));

    vm.warp(block.timestamp + 1 days);
    assertEq(100.013368 * 1e18, rUSDYToken.balanceOf(alice));
    assertEq(10_000 * 100e18, rUSDYToken.sharesOf(alice));

    rUSDYToken.unwrap(rUSDYToken.balanceOf(alice));
    assertEq(100e18, usdy.balanceOf(alice));

  }

  modifier dealAlice() {
    _mintRWAToUser(alice, 100e18);
    _;
  }

}
```

## Tools Used

Foundry. Manual Review. 

## Recommended Mitigation Steps

`rUSDY` needs to be converted to `USDY` on a 1-to-1 basis, to ensure that the interest accrued gets attributed to the user. 


## Assessed type

Math
The pausing relies on the contract being immutable. If PAUSER_ROLE were changed while paused, it may not be unpausable.
Audit: 2023-09-ondo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Loss of expected contract capabilities, unilateral control by a single account, and reduction of trust in the system's reliability.
## Proof of Concept
The contract being pausable relies on it being immutable. If the PAUSER_ROLE were changed while paused, it could become unpausable.
The pause() and unpause() functions rely on the PAUSER_ROLE to restrict access. Only accounts with the PAUSER_ROLE can call these functions.
If the PAUSER_ROLE were changed while the contract is paused, the accounts with the old PAUSER_ROLE would lose access. And if no accounts were granted the new PAUSER_ROLE, the contract would be permanently paused with no way to unpause it.

Illustration: 
Contract is deployed and paused by Pauser account
Attacker account has DEFAULT_ADMIN_ROLE
While paused, attacker revokes Pauser's PAUSER_ROLE and grants it to itself
Now only the attacker can unpause, and the original Pauser cannot
If attacker never calls unpause(), contract remains permanently paused

## Tools Used
Manual
## Recommended Mitigation Steps 
Prevent PAUSER_ROLE from being changed while paused


## Assessed type

Other
Incorrect USDY to yUSDY conversion in Wrap 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#L436


# Vulnerability details

## Impact
In the wrap function, each USDY is converted to 1 share of yUSDY (instead of 1 yUSDY). As interest accrues, one `USDY` can be converted to more than one `rUSDY`. A user should only be able to start accruing interest when the user wraps USDY into yUSDY, as USDY is the non interest bearing version and the yUSDY is the interest bearing version of the token. 

## Proof of Concept

Note that 1 USDY converts to 1 share of USDY in code block below (scaled by the BPS_DENOMINATOR). 

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


## Tools Used

Manual Review

## Recommended Mitigation Steps

The conversion between `yUSDY` and `USDY` should be on a 1-to-1 basis. As such, the amount of `yUSDY` share should be calculated based on the amount of `USDY` and the Oracle price input. 


## Assessed type

Math