xanoutas/solidity-security-findings huggingface dataset:
USDC will make distribution complicated due to rounding
Audit: 2024-02-althea-liquid-infrastructure-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/main/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L275


# Vulnerability details

In `_beginDistribution()`, the entitlement per unit of ERC20 is calculated as a division of the contract's balance by its total internal supply of tokens: `entitlement = balance / supply`. USDC, even in cosmos, operate with 6 decimals instead of the standard 18, so for example, 1000 USDC has roughly the same value as 1000 USDT, but the later is `1e12` decimals higher.

During the distribution, when calculating the entitlement, if the contract's total supply has 18 decimals, the USDC entitlement will always round down to zero.

## Impact
For every Liquid Infrastructure ERC20 that may want to distribute USDC, their decimals will have to be even lower than 6 decimals, and the total supply would have to be low enough to guarantee distribution. This does not affect other currencies such as WETH or USDT.

The same issue will be present with nBTC as well. Submitting this as a medium since it's technically possible to prevent this vulnerability by lowering the decimals to below 6, as well as keeping a low total supply.

## Proof of Concept
>Assume 100 thousand USDC is deposited at the contract during the distribution
>Assume the total supply is 100 thousand tokens at 18 decimals
>During distribution, entitlement will be `balance / supply = 100_000_000_000 / 1e23 = 1e-12 => 0`
>`entitlement` is then pushed into the `erc20EntitlementPerUnit` array as zero
>When `distribution()` is called, `entitlement * this.balanceOf(recipient) = 0` is transfered, meaning no user acquire any tokens at all.

## Tools Used
Manual code reading

## Recommended Mitigation Steps
Instead of calculating entitlement per tokens, store the ERC20 balance at time of distribution and calculate the entitlement per account during a withdrawal instead. This way, you can multiply the recipient's balance by the contract's balance at the time, and then divide by the total supply, which will prevent rounding to zero.

At `distribute()`:

```solidity
                for (uint j = 0; j < distributableERC20s.length; j++) {
                    IERC20 toDistribute = IERC20(distributableERC20s[j]); 
                    uint256 entitlement = (balanceAtDistribution * this.balanceOf(recipient)) / this.totalSupply();
                    if (toDistribute.transfer(recipient, entitlement)) {
                        receipts[j] = entitlement;
                    }
                }
```





## Assessed type

Decimal
`distribute` function uses unsafe `transfer` method with IERC20
Audit: 2024-02-althea-liquid-infrastructure-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L198-L237
https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L224


# Vulnerability details

## Impact
`distribute` function uses unsafe `transfer` method with IERC20.

## Bug
Some tokens do not implement the ERC20 standard properly. For example `Tether` (USDT)'s `transfer()` and `transferFrom()` functions do not return booleans as the specification requires, and instead have no return value. In such a case, their function signatures do not match and therefore the calls made, revert (see this [link](https://gist.github.com/IllIllI000/2b00a32e8f0559e8f386ea4f1800abc5) for a test case).

As many stablecoins will interact as `_distributableErc20s`, the same case is possible in `LiquidInfrastructureERC20` contract's `distribute` function, where `transfer` is used.

## Tools Used
Foundry

## Recommended Mitigation Steps
Add these lines in `distribute` function of `LiquidInfrastructureERC20` contract.
```javascript
    function distribute(uint256 numDistributions) public nonReentrant {
        require(numDistributions > 0, "must process at least 1 distribution");
        if (!LockedForDistribution) {
            require(
                _isPastMinDistributionPeriod(),
                "MinDistributionPeriod not met"
            );
            _beginDistribution();
        }

        uint256 limit = Math.min(
            nextDistributionRecipient + numDistributions,
            holders.length
        );

        uint i;
        for (i = nextDistributionRecipient; i < limit; i++) {
            address recipient = holders[i];
            if (isApprovedHolder(recipient)) {
                uint256[] memory receipts = new uint256[](
                    distributableERC20s.length
                );
                for (uint j = 0; j < distributableERC20s.length; j++) {
                    IERC20 toDistribute = IERC20(distributableERC20s[j]);
                    uint256 entitlement = erc20EntitlementPerUnit[j] *
                        this.balanceOf(recipient);
--                    if (toDistribute.transfer(recipient, entitlement)) {
++                    if (toDistribute.safeTransfer(recipient, entitlement)) {
                        receipts[j] = entitlement;
                    }
                }

                emit Distribution(recipient, distributableERC20s, receipts);
            }
        }
        nextDistributionRecipient = i;

        if (nextDistributionRecipient == holders.length) {
            _endDistribution();
        }
    }
```


## Assessed type

ERC20
Inefficient Gas Usage in LiquidInfrastructureERC20: Conditional Statement in 'distribute' Function May Lead to Expensive Errors
Audit: 2024-02-althea-liquid-infrastructure-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L198-L237


# Vulnerability details

The ``LiquidInfrastructureERC20::distribute`` function checks if the `LockedForDistribution` bool is false. Only after that it calls the ``LiquidInfrastructureERC20::_beginDistribution()`` function.

But if the `LockedForDistribution` bool is set to "True", the condition fails, and it skips the _beginDistribution() call entirely. Then, it continues with the rest of the function execution.

This behavior is due to the structure of the conditional statement.

## Impact
As the ``LiquidInfrastructureERC20::distribute`` is a public function anyone can call it and it is likely to be used often, It may cause expensive errors and possibly `Denial of Service` in some cases due to not doing what it was meant to do

## Proof of Concept

<details>

```javascript

function distribute(uint256 numDistributions) public nonReentrant {
       
        require(numDistributions > 0, "must process at least 1 distribution");
        if (!LockedForDistribution) {
            require(_isPastMinDistributionPeriod(), "MinDistributionPeriod not met");
            _beginDistribution();
        } 

        uint256 limit = Math.min(nextDistributionRecipient + numDistributions, holders.length);

        uint256 i;
        for (i = nextDistributionRecipient; i < limit; i++) {
            address recipient = holders[i];
            if (isApprovedHolder(recipient)) {
                uint256[] memory receipts = new uint256[](distributableERC20s.length);
                for (uint256 j = 0; j < distributableERC20s.length; j++) {
                    IERC20 toDistribute = IERC20(distributableERC20s[j]);
                    uint256 entitlement = erc20EntitlementPerUnit[j] * this.balanceOf(recipient);
                    if (toDistribute.transfer(recipient, entitlement)) {
                        receipts[j] = entitlement;
                    }
                }

                emit Distribution(recipient, distributableERC20s, receipts);
            }
        }
        nextDistributionRecipient = i;

        if (nextDistributionRecipient == holders.length) {
            _endDistribution();
        }
    }
 ```
 </details>

## Tools Used
None

## Recommended Mitigation Steps
The solution of this error is to make this "if statement" applicable for the whole function.

<details>

 ```  diff

 function distribute(uint256 numDistributions) public nonReentrant {
       
        require(numDistributions > 0, "must process at least 1 distribution");
        if (!LockedForDistribution) {
            require(_isPastMinDistributionPeriod(), "MinDistributionPeriod not met");
            _beginDistribution();
 -       } 

        uint256 limit = Math.min(nextDistributionRecipient + numDistributions, holders.length);

        uint256 i;
        for (i = nextDistributionRecip
Loss of Fees due precision error in _beginDistribution() and distribute(uint256 numDistributions)
Audit: 2024-02-althea-liquid-infrastructure-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/main/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L257


# Vulnerability details

## Description
The fee calculation logic performs division in the middle of the calculation, rather than at the end. This results in reduced precision and accuracy of the final fee amounts. 

The _beginDistribution() is called in the distribute(uint256 numDistributions) function, in the _beginDistribution(), there is a division between the uint256 ```entitlement = balance / supply;```. This division will lead to a precision errors when there is a reminder in the division between balance / supply. This precision error is compounded by the fact that there is a multiplication in the distribute(uint256 numDistributions), the result of the balance / supply is multiplied by the recipient balance.

## Impact
The impacts of this precision loss are:

Inaccurate amounts is sent to the recipient
User confusion over incorrect amount received
Small deviations can compound over many transactions.

## Proof of Concept
```solidity
  function _beginDistribution() internal {
        require(!LockedForDistribution, "cannot begin distribution when already locked");
        LockedForDistribution = true;

        // clear the previous entitlements, if any
        if (erc20EntitlementPerUnit.length > 0) {
            delete erc20EntitlementPerUnit;
        }

        // Calculate the entitlement per token held
        uint256 supply = this.totalSupply();

        for (uint256 i = 0; i < distributableERC20s.length; i++) {
            uint256 balance = IERC20(distributableERC20s[i]).balanceOf(address(this));

>>> Division   uint256 entitlement = balance / supply;
            erc20EntitlementPerUnit.push(entitlement);
        }

        nextDistributionRecipient = 0;
        emit DistributionStarted();
    }




 function distribute(uint256 numDistributions) public nonReentrant {
        require(numDistributions > 0, "must process at least 1 distribution");
        if (!LockedForDistribution) {
            require(_isPastMinDistributionPeriod(), "MinDistributionPeriod not met");
>>>>> Division         _beginDistribution();
        }

        uint256 limit = Math.min(nextDistributionRecipient + numDistributions, holders.length);

        uint256 i;
        for (i = nextDistributionRecipient; i < limit; i++) {
            address recipient = holders[i];

            if (isApprovedHolder(recipient)) {
                uint256[] memory receipts = new uint256[](distributableERC20s.length);
                for (uint256 j = 0; j < distributableERC20s.length; j++) {
                    IERC20 toDistribute = IERC20(distributableERC20s[j]);

               
 >>>>> Multiplication      uint256 entitlement = erc20EntitlementPerUnit[j] * this.balanceOf(recipient);
                   
                    if (toDistribute.transfer(recipient, entitlement)) {
                        receipts[j] = entitleme
Varying decimals between distributable ERCs and liquidInfrastructure ERC decimals can cause user distribution to always be 0
Audit: 2024-02-althea-liquid-infrastructure-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/main/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L275


# Vulnerability details

## Impact
User rewards will always be less than expected or equal to 0

## Proof of Concept
In the _beginDistribution function the contracts token balance is divided by the contracts balance of the token to be distributed. In a situation whereby the token decimals for the distributed token is less than 18 which is the decimal of the liquidInfrastructure ERC20 token, the entitlement will always return 0 (i.e for USDC the entitlement will be Xe6/Ye18 = 0).

```
        uint256 supply = this.totalSupply();
        for (uint i = 0; i < distributableERC20s.length; i++) {
            uint256 balance = IERC20(distributableERC20s[i]).balanceOf(
                address(this)
            );
            uint256 entitlement = balance / supply;
            erc20EntitlementPerUnit.push(entitlement);
        }
```

In the above block of code, the balances are being divided without accounting for differences in token decimals

## Tools Used
Manual Review

## Recommended Mitigation Steps
Account for token decimals before calculations or consider performing operations on token balances in terms of token units and not whole tokens.


## Assessed type

Decimal
Inability to Handle ERC721 Tokens Transferred to Smart Contracts
Audit: 2024-02-althea-liquid-infrastructure-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/main/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L418


# Vulnerability details


## Impact
The current implementation of the `releaseManagedNFT` function does not account for the scenario where the recipient (`to`) is a smart contract that is required to implement the `onERC721Received` function. According to the ERC-721 standard, a smart contract must implement this function to safely receive NFTs, ensuring they are not accidentally sent to contracts that cannot handle them. Without this check, there's a risk that NFTs could be permanently locked within contracts that do not recognize or cannot interact with these tokens, resulting in potential loss of assets and negatively impacting user trust and the contract's usability.

## Proof of Concept
The issue is present in the `releaseManagedNFT` function of the `LiquidInfrastructureERC20` smart contract. This function transfers an NFT from the contract to a specified address without verifying if the recipient address is a contract that correctly implements the `onERC721Received` function.

- **Code Snippet**:
  ```solidity
  function releaseManagedNFT(
      address nftContract,
      address to
  ) public onlyOwner nonReentrant {
      LiquidInfrastructureNFT nft = LiquidInfrastructureNFT(nftContract);
      nft.transferFrom(address(this), to, nft.AccountId());
      ...
  }
  ```
  The code transfers an NFT without checking if the recipient can handle it.

## Tools Used
- Manual code review .

## Recommended Mitigation Steps

**Check if the Recipient is a Contract**: Before transferring the NFT, determine if the `to` address is a smart contract(this check can be bypassed but under very specific condition which is very unlikely)


## Assessed type

ERC721
Broken Invariant
Audit: 2024-02-althea-liquid-infrastructure-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/main/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L397
https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/main/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L464


# Vulnerability details

## Impact
Broken Invariant

## Proof of Concept
This invariant has been mentioned in the audit details, discord channel, and specs that the contract `must` already be the owner of the nftContract before it's added to the ManagedNFTs collection.

> *this contract must already be the owner of the `nftContract`*

This invariant is also checked in the `addManagedNFT()`:
```solidity
function addManagedNFT(address nftContract) public onlyOwner {
        LiquidInfrastructureNFT nft = LiquidInfrastructureNFT(nftContract);
        address nftOwner = nft.ownerOf(nft.AccountId());
@>      require(
            nftOwner == address(this),
            "this contract does not own the new ManagedNFT"
        ); @note
        ManagedNFTs.push(nftContract);
        emit AddManagedNFT(nftContract);
    }
```
However, this can be easily passed by setting them on the constructor, as the `constructor` doesn't handle this invariant when setting ManagedNFTs collection:
```solidity
constructor(
        string memory _name,
        string memory _symbol,
        address[] memory _managedNFTs,
        address[] memory _approvedHolders,
        uint256 _minDistributionPeriod,
        address[] memory _distributableErc20s
    ) ERC20(_name, _symbol) Ownable() {
@>      ManagedNFTs = _managedNFTs;
        LastDistribution = block.number;

        for (uint i = 0; i < _approvedHolders.length; i++) {
            HolderAllowlist[_approvedHolders[i]] = true;
        }

        MinDistributionPeriod = _minDistributionPeriod;

        distributableERC20s = _distributableErc20s;

        emit Deployed();
    }
```
## Tools Used
Mnaual Review

## Recommended Mitigation Steps
Always add invariant checks on the constructor as well





## Assessed type

Invalid Validation
fee-on-transfer underlying can cause problems
Audit: 2024-02-althea-liquid-infrastructure-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L223


# Vulnerability details

## Impact
The current implementation doesn't work with fee-on-transfer underlying tokens. Considering that Compound can have these kind of tokens (ex. USDT can activate fees), this issue can affect the protocol.

The problem arise when transferring tokens, basically blocking all functions in `LiquidInfrastructureERC20.sol` for that particular token, since the contract wrongly assumes balances values. This becomes particularly problematic in the following scenario: a market for USDT is running without problems, then they activate the fee: this effectively blocks users from redeeming the underlying.

##Proof of Concept
It is possible for `toDistribute.transfer(recipient, entitlement)`; to fail for numerous reasons. Such as if a user has been blacklisted (in certain ERC20 tokens) or if a token is paused or there is an attack and the token is stuck.

## Tools Used
manual

## Recommended Mitigation Steps

check the balance of the contract before and after every time a token is transferred to see the effective quantity


## Assessed type

ETH-Transfer
Withdraw can be stuck
Audit: 2024-02-althea-liquid-infrastructure-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/main/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L359-L386


# Vulnerability details

## Impact
LiquidInfrastructureERC20 can't withdraw any ERC20 from managed NFTs.

## Proof of Concept
In some scenarios, if more than one NFT managed contracts are released, the withdraw can be stuck until at least one NFT managed is added.

```
// 4 NFT managed contracts added
    function test_withdraw() public {
        // Withdraw from all except the last one
        liERC20.withdrawFromManagedNFTs(3);
    
        uint256 nextWithdrawal = liERC20.nextWithdrawal(); // 3
        
        // release 2 managed NFTs
        liERC20.releaseManagedNFT(address(liNFT1), address(this));
        liERC20.releaseManagedNFT(address(liNFT2), address(this));

        // withdraw nr withdrawals -> 1
        liERC20.withdrawFromManagedNFTs(1);
        nextWithdrawal = liERC20.nextWithdrawal(); // remain 3

        // withdraw nr withdrawals -> 2
        liERC20.withdrawFromManagedNFTs(2);
        nextWithdrawal = liERC20.nextWithdrawal(); // remain 3

        // withdraw nr withdrawals -> 0
        liERC20.withdrawFromManagedNFTs(0);
        nextWithdrawal = liERC20.nextWithdrawal(); // remain 3

        // Withdraw() is in stuck until at least a new managed NFT will be added

        liNFT1.transferFrom(address(this), address(liERC20), 1);
        liERC20.addManagedNFT(address(liNFT1));

        liERC20.withdrawFromManagedNFTs(1);
        nextWithdrawal = liERC20.nextWithdrawal(); // reset to 0      
    }
```

Here in the test, 4 NFT managed contracts have been added. The first withdraw function withdraws from all except the last one, then 2 NFT managed are removed.
At this point the `nextWithdrawal = 3` and `ManagedNFTs.length = 2`, so the `limit` calculated in `withdrawFromManagedNFTs(numWithdrawal)` will be always 2, the min between the managed nft length and `numWithdrawals + nextWithdrawal`, whatever will be the `numWithdrawal` pass as input param. the next for loop will be always skipped because `nextWithdrawal > limit`, and the `nextWithdrawal` won't change. It  can be "fixed" adding at least one nft managed so `ManagedNFTs.length > 2` and the for loop can be executed at least one time to reset `nextWithdrawal `.

## Tools Used
Foundry

## Recommended Mitigation Steps

An easy solution can be to include a check within `releaseManagedNFT()`. It may revert if `nextWithdrawal != 0`.


## Assessed type

Token-Transfer
`from` addresses that have been disapproved can still hold tokens and perform transfers with them if no longer approved
Audit: 2024-02-althea-liquid-infrastructure-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/main/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L133


# Vulnerability details

## Impact
`from` addresses that have been disapproved can still hold tokens and perform transfers with them if no longer approved

## Proof of Concept
In the `_beforeTokenTransfer()`,
approval checks were only made to the receiver. However, the sender (`from`) is missing an approval check
```solidity
function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual override {
        require(!LockedForDistribution, "distribution in progress");
        if (!(to == address(0))) {
            require(
                isApprovedHolder(to),
                "receiver not approved to hold the token"
            );
        }
        if (from == address(0) || to == address(0)) {
            _beforeMintOrBurn();
        }
        bool exists = (this.balanceOf(to) != 0);
        if (!exists) {
            holders.push(to);
        }
    }
```
`from` addresses that have been disapproved can still hold tokens and perform transfers with them if no longer approved

## Tools Used
Manual Review

## Recommended Mitigation Steps
consider adding a check on the `from` address too, either on the `_beforeTokenTransfer()` or `_afterTokenTransfer()`.


## Assessed type

Access Control