xanoutas/solidity-security-findings huggingface dataset:
Unrecoverable Dust Shares Due to Lack of Minimal Constraints in `transferShares` Function
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

1. **Asset Locking**: Users can transfer very small shares ("dust") to other accounts, but the owner can't convert these dust shares back to USDY using the `burn` function. This effectively locks up the USDY asset.
  
2. **System Integrity**: These dust shares can clutter the blockchain and may be exploited for more complex attacks involving other contracts.

It is difficult to mitigate this issue without affecting users who may intentionally or unintentionally leave dust in their accounts.

### Affected Functions:

- [`transferShares`](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L416)
- [`burn`](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L575)

## Proof of Concept

The `transferShares` function allows transferring any `_sharesAmount` or leaving any small shares in `shares[_sender]`, even extremely small amounts that, due to truncation issues, can't be converted back into USDY using the `burn` function. 

### Code Snippet:

In `transferShares`:
```solidity
_transferShares(msg.sender, _recipient, _sharesAmount);
```
And in `burn`:
```solidity
usdy.transfer(msg.sender, sharesAmount / BPS_DENOMINATOR);
```

For example, if a user transfers dust shares (less than $1 \times 10^4$) to another account or remains in sender account, this amount will not be convertible back to USDY even by the owner due to the truncation in the `burn` function, making those USDY tokens effectively irretrievable.

## Recommended Mitigation Steps

Implement a soft limit on the minimum shares that can be transferred or remained when using the `transferShares` function, maybe allow for exceptions under certain conditions.

```diff
 function _transferShares(
     address _sender,
     address _recipient,
     uint256 _sharesAmount
   ) internal whenNotPaused {
     require(_sender != address(0), "TRANSFER_FROM_THE_ZERO_ADDRESS");
     require(_recipient != address(0), "TRANSFER_TO_THE_ZERO_ADDRESS");
 
     _beforeTokenTransfer(_sender, _recipient, _sharesAmount);
 
     uint256 currentSenderShares = shares[_sender];
     require(
       _sharesAmount <= currentSenderShares,
       "TRANSFER_AMOUNT_EXCEEDS_BALANCE"
     );
+    require(
+        _sharesAmount >= BPS_DENOMINATOR
+        || currentSenderShares - _sharesAmount >= BPS_DENOMINATOR,
+        "TRANSFER_MINIMUM_SHARES_CONSTRAINTS"
+    );
 
     shares[_sender] = currentSenderShares - _sharesAmount;
     shares[_recipient] = shares[_recipient] + _sharesAmount;
 }
```



## Assessed type

Math
Absence of Minimal Share Constraints in Some of rUSDY Token Operations
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Functions can return zero if the number of `shares` is too small (less than `BPS_DENOMINATOR` where it equal to `1e4` ). This can lead to misleading or incorrect events in the contract functions that use it.

### Affected Functions:
- [`transferShares`](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L416)
- [`_burnShares`](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L575)
- [`totalSupply`](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L217)
- [`balanceOf`](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/usdy/rUSDY.sol#L217)

## Proof of Concept

Consider the function `getRUSDYByShares(uint256 _shares)` which calculates:

$$
\text{rUSDY} = \frac{\text{shares} \times \text{oracle.getPrice()}}{1e18 \times \text{BPSDENOMINATOR}}
$$

For `_shares` less than $1 \times 10^4$, assuming the price returned by the oracle is $1 \times 10^{18}$, the above formula would result in a zero rUSDY value.

To result in truncation where rUSDY becomes zero, the numerator should be less than the denominator. Therefore:

$$
\text{shares} \times 10^{18} < 1 \times 10^{18} \times 1 \times 10^4
$$

$$
\text{shares} <  1 \times 10^4
$$


The formula has a dependency on the `oracle.getPrice()` value. If the price fluctuates, the limitation on `_shares` will also change. This could potentially add another layer of complexity and unexpected behavior, especially if `oracle.getPrice()` can return values significantly different from $ 1 \times 10^{18}$.

For example:

- If `oracle.getPrice()` returns a value much smaller than $1 \times 10^{18}$, the minimum `_shares` that will result in a non-zero `rUSDY` value will increase.
- If `oracle.getPrice()` returns a value much larger than $1 \times 10^{18}$, even very small `_shares` could result in a non-zero `rUSDY` value.

This becomes misleading when the result is used in calculations or event emissions in the aforementioned functions.

For example, in `transferShares`, the `Transfer` event could emit a zero value when transferring a non-zero, albeit small, amount of shares. 

### Foundry Test

```solidity
pragma solidity 0.8.16;

import "forge-tests/USDY_BasicDeployment.sol";

contract Test_PoC_rUSDY_Truncation is USDY_BasicDeployment {
  event Transfer(address indexed from, address indexed to, uint256 value);

  function setUp() public override {
    super.setUp();
    vm.pr
The _execute() function allows anyone to give first approval for txnHash
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

Any caller has the ability to initiate the `DestinationBridge.execute` function within the `DestinationBridge` smart contract, leading to approval txHash and minting if the threshold requirement is satisfied.
It appears that in the described scenario, non-approvers can approve transaction, which is not intended behavior.

## Proof-of-Concept
Include the following test in the Test_DestinationBridge to examine it:
```solidity
	function test_userCallExecuteAndBecomeFirstApprover() public {
    // remove initial threshold
    remove_threshold_arbitrum();

    // The Axelar gateway invokes the execute function, resulting in an error,
    // which could be any type of error, not necessarily limited to a "NoThresholdMatch" error.
    bytes32 cmdId = bytes32(
      0x9991faa1f435675159ffae64b66d7ecfdb55c29755869a18db8497b4392347e0
    );
    string memory srcChain = "arbitrum";
    string memory srcAddress = "0xce16F69375520ab01377ce7B88f5BA8C48F8D666";
    address srcSender = alice;
    uint256 amt = 100e18;
    uint256 nonce = 1;
    bytes memory payload = abi.encode(VERSION, srcSender, amt, nonce);

    approve_message(
      cmdId,
      srcChain,
      srcAddress,
      address(destinationBridge),
      keccak256(payload)
    );

    vm.prank(address(gateway_eth));
    vm.expectRevert(DestinationBridge.NoThresholdMatch.selector);
    destinationBridge.execute(cmdId, srcChain, srcAddress, payload);

    // Set threshold amounts
    amounts.push(500e18);
    numOfApprovers.push(1);
    vm.expectEmit(true, true, true, true);
    emit ThresholdSet(arb, amounts, numOfApprovers);
    vm.prank(guardian);
    destinationBridge.setThresholds(arb, amounts, numOfApprovers);
    (uint256 amount, uint256 threshold) = destinationBridge.chainToThresholds(
      arb,
      0
    );
    assertEq(amount, 500e18);
    assertEq(threshold, 1);

    // Anyone can call the execute function,
    // and even though Alice is not authorized to approve it,
    // she still calls it and meets the threshold.
    vm.prank(alice);
    assertEq(usdy.balanceOf(alice), 0);
    destinationBridge.execute(cmdId, srcChain, srcAddress, payload);
    assertEq(usdy.balanceOf(alice), amt);
  }
```

## Impact
This results in a situation where the initial callers of the `DestinationBridge.execute` function has the authority to approve it, even if they are not listed in the [approvers](https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L43) list.

## Tools Used
Manual

## Recommended Mitigation Steps
Consider verifying whether msg.sender is an approver:

```solidity
if (!approvers[msg.sender]) {
  _approve(txn
Possible to transfer less tokens than what is burnt
Audit: 2023-09-ondo-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
When we burn shares, the shares burned should be transferred to the `msg.sender`, however it's possible to transfer less than the amount burnt

## Proof of Concept
https://github.com/code-423n4/2023-09-ondo/blob/02ebedeeb85b708345a4deb53a2b543ecae160d0/contracts/usdy/rUSDY.sol#L672-L683

```solidity
File: /contracts/usdy/rUSDY.sol
672:  function burn(
673:    address _account,
674:    uint256 _amount
675:  ) external onlyRole(BURNER_ROLE) {
676:    uint256 sharesAmount = getSharesByRUSDY(_amount);

678:    _burnShares(_account, sharesAmount);

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

682:    emit TokensBurnt(_account, _amount);
683:  }
```

According to the comment, we transfer burned shares to the `msg.sender`, however, if `sharesAmount` is smaller than `BPS_DENOMINATOR` we would end up transferring less than what was actually burned.

If `sharesAmount` is 5000, the call `usdy.transfer(msg.sender, sharesAmount / BPS_DENOMINATOR);`  would end up transferring 0 amount since `5000 / 10000` would result in a zero(Solidity truncates)


## Tools Used
Manual review

## Recommended Mitigation Steps
Add a check to ensure that `sharesAmount > BPS_DENOMINATOR`


## Assessed type

Other
No Allowlist For Bridgeable ERC-20 Tokens
Audit: 2022-10-zksync-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1ERC20Bridge.sol#L111-L132


# Vulnerability details

## Vulnerability Details

We noticed that the `deposit` function of the `L1ERC20Bridge` contract (code snippet 1) permits a user to bridge any ERC-20 tokens (including deflationary and rebase tokens) from the L1 to the L2 network.

We considered that **permitting non-standard ERC-20 tokens, such as deflationary or rebase tokens, could cause unexpected behaviors introducing potential attacks on the dApps (Decentralized Applications) running on the L2 network**.

For instance, if a deflationary token (e.g., *SafeMoon*) was bridged to the L2 network, that token would be treated like a standard non-deflationary ERC-20 token. In other words, the same deflationary token on L1 and L2 would have different mechanisms.

```solidity
SNIPPET: 1
FILE: https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1ERC20Bridge.sol

111:    function deposit(
112:        address _l2Receiver,
113:        address _l1Token,
114:        uint256 _amount
115:    ) external payable nonReentrant senderCanCallFunction(allowList) returns (bytes32 txHash) {
116:        uint256 amount = _depositFunds(msg.sender, IERC20(_l1Token), _amount);
117:        require(amount > 0, "1T"); // empty deposit amount
118:
119:        bytes memory l2TxCalldata = _getDepositL2Calldata(msg.sender, _l2Receiver, _l1Token, amount);
120:        txHash = zkSyncMailbox.requestL2Transaction{value: msg.value}(
121:            l2Bridge,
122:            0, // L2 msg.value
123:            l2TxCalldata,
124:            DEPOSIT_ERGS_LIMIT,
125:            new bytes[](0)
126:        );
127:
128:        // Save the deposited amount to claim funds on L1 if the deposit failed on L2
129:        depositAmount[msg.sender][_l1Token][txHash] = amount;
130:
131:        emit DepositInitiated(msg.sender, _l2Receiver, _l1Token, amount);
132:    }
```

## Impact

We considered that **permitting non-standard ERC-20 tokens, such as deflationary or rebase tokens, could cause unexpected behaviors introducing potential attacks on the dApps (Decentralized Applications) running on the L2 network**.

For this reason, we considered this issue high severity.

## Proof of Concept

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1ERC20Bridge.sol#L111-L132

## Tools Used

VSCode (Manual Review)

## Recommended Mitigation Steps

We recommend **employing an allowlist for bridgeable ERC-20 tokens to enhance the security of all dApps running on the L2 network**.
Uninitializing Bridge Contracts' State Variables
Audit: 2022-10-zksync-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1ERC20Bridge.sol#L57-L60
https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1EthBridge.sol#L48-L51


# Vulnerability details

## Vulnerability Details

**The `L1ERC20Bridge` and `L1EthBridge` are implementation contracts that would be delegatecalled by their corresponding proxy contracts.** In other words, all state variables and assets would be stored in the proxy contracts. In contrast, the implementation contracts would keep the logic for processing the state variables stored in the proxy contracts.

**We found that the `L1ERC20Bridge` and `L1EthBridge` contracts initialize their state variables, including `zkSyncMailbox` and `allowList`, in the `constructor`** (L57 - 60 in code snippet 1 and L48 - 51 in code snippet 2).

**Specifically, initializing state variables in the implementation contracts' `constructor` would not be effective on the proxy contracts. Consequently, the resulting uninitialized state variables (i.e., `zkSyncMailbox` and `allowList`) can render the proxy contracts unusable.**

```solidity
SNIPPET: 1
FILE: https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1ERC20Bridge.sol

21:     contract L1ERC20Bridge is IL1Bridge, AllowListed, ReentrancyGuard {
            // (...SNIPPED...)

24:         /// @dev The smart contract that manages the list with permission to call contract functions
25:         IAllowList immutable allowList;
26: 
27:         /// @dev zkSync smart contract that used to operate with L2 via asynchronous L2 <-> L1 communication
28:         IMailbox immutable zkSyncMailbox;

            // (...SNIPPED...)

57:         constructor(IMailbox _mailbox, IAllowList _allowList) reentrancyGuardInitializer {
58:             zkSyncMailbox = _mailbox;
59:             allowList = _allowList;
60:         }

            // (...SNIPPED...)
288:    }
```

```solidity
SNIPPET: 2
FILE: https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1EthBridge.sol

17:     contract L1EthBridge is IL1Bridge, AllowListed, ReentrancyGuard {
18:         /// @dev The smart contract that manages the list with permission to call contract functions
19:         IAllowList immutable allowList;
20: 
21:         /// @dev zkSync smart contract used to interact with L2 via asynchronous L2 <-> L1 communication
22:         IMailbox immutable zkSyncMailbox;

            // (...SNIPPED...)

48:         constructor(IMailbox _mailbox, IAllowList _allowList) reentrancyGuardInitializer {
49:             zkSyncMailbox = _mailbox;
50:             allowList = _allowList;
51:         }

            // (...SNIPPED...)
246:     }
```

## Impact

**We found that the `L1ERC20Bridge` and `L1EthBridge` contracts initialize their state variables, including `zkSyncMailbox` and `allowList`, in the `constructor`** (L57 - 60 in code snippet 1 and L48 - 51 in code snippet 2).

**Specifically
L2 deposits might be reverted 
Audit: 2022-10-zksync-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/bridge/L1ERC20Bridge.sol#L179-L217
https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/bridge/L1EthBridge.sol#L132-L174


# Vulnerability details

## Impact
L1ERC20Bridge and L1EthBridge contracts have `claimFailedDeposit` function to withdraw funds from the initiated deposit, which failed when finalizing on L2. However, if the caller is allowed in `senderCanCallFunction` modifier, this function can be used to abuse the L2 deposits.

With this context, an actor/user/address who is monitoring the L1 mempool for the L2 deposits can call this function and griefs the deposited funds to go further on L2 and revert back to the `_depositSender`

## Proof of Concept
```solidity
    function claimFailedDeposit(
        address _depositSender,
        address _l1Token,
        bytes32 _l2TxHash,
        uint256 _l2BlockNumber,
        uint256 _l2MessageIndex,
        uint16 _l2TxNumberInBlock,
        bytes32[] calldata _merkleProof
    ) external nonReentrant senderCanCallFunction(allowList) {
        
        L2Log memory l2Log = L2Log({
            l2ShardId: 0,
            isService: true,
            txNumberInBlock: _l2TxNumberInBlock,
            sender: BOOTLOADER_ADDRESS,
            key: _l2TxHash,
            value: bytes32(0)
        });
        bool success = zkSyncMailbox.proveL2LogInclusion(_l2BlockNumber, _l2MessageIndex, l2Log, _merkleProof);
        require(success, "yn");


        uint256 amount = depositAmount[_depositSender][_l1Token][_l2TxHash];
        require(amount > 0, "y1");


        delete depositAmount[_depositSender][_l1Token][_l2TxHash];
        // Withdraw funds
        IERC20(_l1Token).safeTransfer(_depositSender, amount);


        emit ClaimedFailedDeposit(_depositSender, _l1Token, amount);
    }
```
[Permalink-1](https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/bridge/L1ERC20Bridge.sol#L179-L217)

```solidity
    function claimFailedDeposit(
        address _depositSender,
        address _l1Token,
        bytes32 _l2TxHash,
        uint256 _l2BlockNumber,
        uint256 _l2MessageIndex,
        uint16 _l2TxNumberInBlock,
        bytes32[] calldata _merkleProof
    ) external override nonReentrant senderCanCallFunction(allowList) {
        require(_l1Token == CONVENTIONAL_ETH_ADDRESS, "sj");


        // Checks
        uint256 amount = depositAmount[_depositSender][_l2TxHash];
        require(amount != 0);

        L2Log memory l2Log = L2Log({
            l2ShardId: 0,
            isService: true,
            txNumberInBlock: _l2TxNumberInBlock,
            sender: BOOTLOADER_ADDRESS,
            key: _l2TxHash,
            value: bytes32(0)
        });
        bool success = zkSyncMailbox.proveL2LogInclusion(_l2BlockNumber, _l2MessageIndex, l2Log, _merkleProof);
        requi
Lack of notice period for critical operations
Audit: 2022-10-zksync-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/bridge/L1ERC20Bridge.sol#L21
https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/bridge/L1EthBridge.sol#L17
https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/zksync/facets/DiamondCut.sol#L78
https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/zksync/facets/Governance.sol#L11


# Vulnerability details

## Impact
All user assets can be locked or stolen.

## Proof of Concept

All user assets can be locked or stolen if the L1ERC20Bridge or L1EthBridge is upgraded to a malicious contract.

All user assets can be locked or stolen if [governor](https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/zksync/facets/Governance.sol#L28) is lost and [`emergencyFreezeDiamond`](https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/zksync/facets/DiamondCut.sol#L78) is called.

All user assets can be stolen if [Governance](https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/zksync/facets/Governance.sol#L11) is set maliciously.

All these critical operations are no less dangerous than upgrading the diamond, but they all can be done in one block without any buffer time for users to react.

## Tools Used
n/a

## Recommended Mitigation Steps

The [DiamondCut](https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/zksync/facets/DiamondCut.sol#L12) has implemented a good mechanism with a UPGRADE_NOTICE_PERIOD and security council. Although the UPGRADE_NOTICE_PERIOD is set to 0 and securityCouncilMembers is not set during alpha, the mechanism will be safe and effective.

Therefore, all these critical operations should be operations be incorporated into this security mechanism.
Bridge should not allow ERC20 tokens with double-entry point
Audit: 2022-10-zksync-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/zksync/contracts/bridge/L2ERC20Bridge.sol#L60


# Vulnerability details

## Impact
If a token with double-entry point is escrowed in L1 bridge, the user could withdraw their token through the other entry point, thus rugpulling the bridge. And when they actually withdraw from L2, they will take other users' funds from the bridge.

## Proof of Concept
Some proxied tokens have multiple addresses. As an example consider the following snippet. rescueFunds is intended to allow the contract owner to return non pool tokens that were accidentaly sent to the contract. However, it assumes a single address per token and so would allow the owner to steal all funds in the pool.
```
mapping isPoolToken(address => bool);
constructor(address tokenA, address tokenB) public {
  isPoolToken[tokenA] = true;
  isPoolToken[tokenB] = true;
}
function rescueFunds(address token, uint amount) external nonReentrant onlyOwner {
    require(!isPoolToken[token], "access denied");
    token.transfer(msg.sender, amount);
}
```

## Tools Used
VS Code

## Recommended Mitigation Steps
Disallow double entry point tokens through the use of allowlist or blacklist.