xanoutas/solidity-security-findings huggingface dataset:
Async in calculating positionTVL between connectors
Audit: 2024-04-noya-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-noya/blob/main/contracts/connectors/UNIv3Connector.sol#L127-#L150
https://github.com/code-423n4/2024-04-noya/blob/main/contracts/connectors/CamelotConnector.sol#L88-#L97
https://github.com/code-423n4/2024-04-noya/blob/main/contracts/connectors/MorphoBlueConnector.sol#L118-#L135


# Vulnerability details

## Vulnerability details
There is asynchronous in calculating positionTVL between connectors:

1, Example 1: UNIv3Connector:

    function _getPositionTVL(HoldingPI memory p, address base) public view override returns (uint256 tvl) {
        PositionBP memory positionInfo = registry.getPositionBP(vaultId, p.positionId);
        uint256 tokenId = abi.decode(p.data, (uint256));
        (address token0, address token1) = abi.decode(positionInfo.data, (address, address));
        uint256 amount0;
        uint256 amount1;
        (int24 tL, int24 tU, uint24 fee) = abi.decode(p.additionalData, (int24, int24, uint24));
        {
            IUniswapV3Pool pool = IUniswapV3Pool(factory.getPool(token0, token1, fee));
            bytes32 key = keccak256(abi.encodePacked(positionManager, tL, tU));

            (uint128 liquidity,,, uint128 tokensOwed0, uint128 tokensOwed1) = pool.positions(key);

            (uint160 sqrtPriceX96,,,,,,) = pool.slot0();
            (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(
                sqrtPriceX96, TickMath.getSqrtRatioAtTick(tL), TickMath.getSqrtRatioAtTick(tU), liquidity
            );
            amount0 += tokensOwed0;
            amount1 += tokensOwed1;
        }

        tvl += valueOracle.getValue(token0, base, amount0);  // <---
        tvl += valueOracle.getValue(token1, base, amount1);  // <---
    }

In `UNIv3Connector`, tvl is calculated based on total value of amount0 and amount1 in uniswap pair, which do not including fee despise it have

Example 2: CamelotConnector

    function _getPositionTVL(HoldingPI memory p, address base) public view override returns (uint256 tvl) {
        (address tokenA, address tokenB) =
            abi.decode(registry.getPositionBP(vaultId, p.positionId).data, (address, address));
        address pool = factory.getPair(tokenA, tokenB);
        uint256 totalSupply = IERC20(pool).totalSupply();
        (uint256 reserves0, uint256 reserves1,,) = ICamelotPair(pool).getReserves();

        uint256 balanceThis = IERC20(pool).balanceOf(address(this));
        return balanceThis * (_getValue(tokenA, base, reserves0) + _getValue(tokenB, base, reserves1)) / totalSupply;   // <---
    }

Similar to example 1, this one calculate tvl based on supply amount, borrow amount and collateral without fee received

Example 3: MorphoBlueConnector

    function _getPositionTVL(HoldingPI memory p, address base) public view override returns (uint256 tvl) {
        PositionBP memory positionInfo = registry.getPositionBP(vaultId, p.positionId);
        if (positionInfo.positionTypeId == MORPHO_POSITION_ID) {
            Id id = abi
Unauthorized Centralization of Control in Multisignature Wallet
Audit: 2024-04-noya-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-noya/blob/main/contracts/governance/Keepers.sol#L42-L56


# Vulnerability details

## Impact
Much as centralization should be addressed in QA, this issue is classified as high severity due to its potential to compromise the security and intended decentralized governance of the multisignature wallet. The ability of any single owner to unilaterally modify the set of owners presents a significant risk, as it can lead to the centralization of control. This could result in unauthorized access to the assets or funds managed by the wallet, subverting the security model that depends on distributed consent among multiple trusted parties.

## Proof of Concept
The `updateOwners` function in the `Keepers` smart contract allows any current owner to add new owners or remove existing ones. The current implementation does not require consensus or multiple approvals for such critical changes, posing a risk that a single compromised or malicious owner could seize control of the wallet:

https://github.com/code-423n4/2024-04-noya/blob/main/contracts/governance/Keepers.sol#L42-L56

```solidity
    function updateOwners(address[] memory _owners, bool[] memory addOrRemove) public onlyOwner {
        uint256 numOwnersTemp = numOwners;
        for (uint256 i = 0; i < _owners.length; i++) {
            if (addOrRemove[i] && !isOwner[_owners[i]]) {
                isOwner[_owners[i]] = true;
                numOwnersTemp++;
            } else if (!addOrRemove[i] && isOwner[_owners[i]]) {
                isOwner[_owners[i]] = false;
                numOwnersTemp--;
            }
        }
        require(numOwnersTemp <= 10 && threshold <= numOwnersTemp && threshold > 1);
        numOwners = numOwnersTemp;
        emit UpdateOwners(_owners, addOrRemove);
    }
```
In this scenario, a malicious or compromised owner could potentially remove all other owners and add new ones under their control, effectively taking over the wallet without other owners' consent.

## Tools Used
Manual

## Recommended Mitigation Steps
Modify the `updateOwners` function to require signatures from a majority of the owners (or another predefined number) before any changes to the ownership structure can be executed. This ensures that no single owner can make unilateral changes.

Additionally, a self-removal feature may be implemented:

```solidity
function removeSelf() public {
    require(isOwner[msg.sender], "Caller is not an owner");

    // Ensure the number of remaining owners can still meet the threshold
    require(numOwners - 1 >= threshold, "Cannot remove, threshold too high");

    isOwner[msg.sender] = false;
    numOwners--;

    // Emit an event for the removal
    emit OwnerRemoved(msg.sender);
}
```


## Assessed type

Access Control
_getValue in Silo connector will return 0 + inflated Oracle price
Audit: 2024-04-noya-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-noya/blob/9c79b332eff82011dcfa1e8fd51bad805159d758/contracts/connectors/SiloConnector.sol#L123-L128


# Vulnerability details

# **Vulnerability Detail**

In the Silo connector's **`_getPositionTVL`** function, the deposited amount and borrowed amount are obtained and used to calculate the Total Value Locked (TVL) by querying an Oracle for asset prices. However, a critical flaw arises from the incorrect handling of decimals in these calculations, leading to inaccurate TVL reporting.

```solidity
uint256 price = _getValue(assets[i], base, 1e18); //@audit
totalDepositAmount += depositAmount * price / 1e18;
totalBAmount += borrowAmount * price / 1e18;
```

The issue stems from using **`1e18`** as the divisor in the TVL calculation, regardless of the decimals of the underlying assets. As a result, the TVL is erroneously inflated, leading to incorrect reporting.

The Silo protocol's core contract, **`Silo.sol`**, creates individual Silos for specific assets, with each Silo's share tokens being based on the decimals of the underlying assets. For instance, if USDC with 6 decimals is deposited, the corresponding share tokens will also have 6 decimals.

Silo.sol is the main component of the Silo protocol. Each Silo is created for each specific assets. Thus, every shares (collateralToken/collateralOnlyToken/debtToken) that will be minted/burned to the msg.sender are based on the decimals of the underlying assets. Take a look on the BaseSilo.sol `_initAssetsTokens` → `_initAsset` → `ShareToken.sol` where we could see such constructor

It is is implementation of the ShareToken.sol from the Silo. 

```solidity
constructor(address _silo, address _asset) {
        silo = ISilo(_silo);
        asset = _asset;
        _decimals = IERC20Metadata(_asset).decimals();
    }
```

Silo currently allows to deposit USDC, WETH and XAI. 

# **Proof of Concept**

Let’s assume that we deposit the USDC which has 6 decimals, so the share token would have 6 decimals also.

1. `uint256 depositAmount = IERC20(assetsS[i].collateralToken).balanceOf(address(this));` Would be retrieved in the SiloConnector in the _getTVL function
2. After, the _getValue function from the Oracle(assume chainlink) would be calculated on this asset in such way → `_getValue(assets[i], base, 1e18)`
3. So, assume we have USDC/USD feed and 1e18 as an amount. Decimals would be taken on USDC token
4. (amountIn * uintprice) / (sourceTokenUnit) would be calculated in the ChainLink. → 
(1e18 * 100002000) / 1e8 = 1000020000000000000 it returns already inflated number of the ****`depositAmount` if it is USDC. 

It is first vulnerability, that causes the incorrect decimals handling. But assume that the price would be returned is correct. 

1. For example the depositAmount is 100e6(shareToken).
    
    totalDepositAmount += depositAmount * price / 1e18;
    
    totalDepositAmount = 100e6 * 100002000(assume correct Chainlink price) / 1e18 = 0.0100002
    
    S
Attacker could break all the withdrawal group if he burns the share before the executeWithdraw
Audit: 2024-04-noya-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-noya/blob/9c79b332eff82011dcfa1e8fd51bad805159d758/contracts/accountingManager/AccountingManager.sol#L418-L419


# Vulnerability details

# **Vulnerability Detail**

AccountingManager.sol has a function that allows user to burns the share. Attacker could exploit it to break the withdrawal group, thus prevent the users from receiving their assets and make the protocol non-functional.

Right before the executeWithdraw function, attacker could burn their share, and the whole executeWithdrawal flow will revert when it will try to burn attackers shares 

```solidity
_burn(data.owner, shares);
```

In this scenario even the resetMiddle will not allow to prevent such kind of attacks, because the currentWithdrawalGroup is already started.

```solidity
if (newMiddle > withdrawQueue.middle || newMiddle < withdrawQueue.first || currentWithdrawGroup.isStarted) {
                revert NoyaAccounting_INVALID_AMOUNT();
            }
```

# **Proof of Concept**

The users deposit their funds

Attacker deposit some small amount of base token.

Deposit flow initiated by the manager. Attacker and user’s receive their shares

After that the attacker decided to withdraw their shares.

The withdrawal flow of the group begins and attackers data is included in this group

1. calculateWithdrawShares
2. neededAssetsForWithdraw
3. startCurrentWithdrawGroup
4. fulfillCurrentWithdrawGroup

These functions are triggered by the manager along the withdrawal flow.

And right before the key function called ‘`executeWithdraw`’ that sends assets to the user attacker frontruns the function and burn his share via this function

```solidity
 function burnShares(uint256 amount) public {
        _burn(msg.sender, amount);
    }
```

With such kind of step attacker could break the withdrawal group and prevent it from the execution, and the contract basically will be stuck(non-functional).

```solidity
function testVaultSingleDepositFlow() public {
        console.log("-----------Base Workflow--------------");

        uint256 _amount = 10_000 * 1e6;
        console.log("  Balance before deposit: %s USDC", _amount / 1e6);

        
        _dealWhale(baseToken, address(alice), address(0x1AB4973a48dc892Cd9971ECE8e01DcC7688f8F23), _amount);

        address attacker = address(888);
        _dealWhale(baseToken, address(attacker), address(0x1AB4973a48dc892Cd9971ECE8e01DcC7688f8F23), 1e6);

        vm.startPrank(alice);
        SafeERC20.forceApprove(IERC20(USDC), address(accountingManager), _amount);
        accountingManager.deposit(address(alice), _amount, address(0));
        vm.stopPrank();

        vm.startPrank(attacker);
        SafeERC20.forceApprove(IERC20(USDC), address(accountingManager), 1e6);
        accountingManager.deposit(address(attacker), 1e6, address(0));
        vm.stopPrank();

        vm.startPrank(owner);
        accountingManager.calculateDepositShares(2);
        vm.warp(block.timestamp + 35 minutes);
xecuteDeposit could be DoS and non functional if receiver address(0).
Audit: 2024-04-noya-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-noya/blob/9c79b332eff82011dcfa1e8fd51bad805159d758/contracts/accountingManager/AccountingManager.sol#L193-L219


# Vulnerability details

# **Vulnerability Detail**

On the first step, the user call the deposit function, which transferFrom their tokens into the AccountManager contract and set all the necessary info regarding this deposit position. 

The second step, when the position array is fulfilled, the calculateDepositShares is called on the deposits that have happened on the step 1. During this steps the amount of shares to be minted is calculated.

On the third step, the executeDeposit function is called by manager and mints the actual shares to the users. 

Unfortunately, this flow could be totally hijacked and become non-functional if the malicious user during the first step(actual deposit) set the receiver as address(0). Thus, during the 3 step(when the shares are actually minted) the whole tx would revert due to this check in the ERC20 token.

```solidity
function _mint(address account, uint256 value) internal {
        if (account == address(0)) {
            revert ERC20InvalidReceiver(address(0));
        }
```

# **Proof of Concept**

```solidity
//forge test --match-path testFoundry/TestAccounting.sol --match-test testInflationAttack -vvvv
    function testInflationAttack() public{
        console.log("-----------SetUp--------------");
        uint256 _amount = 10_001 * 1e6;
        console.log("  Balance before deposit: %s USDC", _amount / 1e6);

        address attacker = address(777);

        //deal the token to alice
        _dealWhale(baseToken, address(alice), address(0x1AB4973a48dc892Cd9971ECE8e01DcC7688f8F23), 10 * _amount);

        //deal 1 USDC to the attacker
        vm.prank(address(alice));
        IERC20(baseToken).transfer(address(attacker), 1e6);

        //Deposit 1 USDC from the attacker
        vm.prank(address(attacker));
        SafeERC20.forceApprove(IERC20(USDC), address(accountingManager), 1e6);
        vm.prank(address(attacker));
        accountingManager.deposit(address(0), 1e6, address(0));
        
        //Deposit from the alice
        vm.startPrank(alice);
        SafeERC20.forceApprove(IERC20(USDC), address(accountingManager), 3 * _amount);
        accountingManager.deposit(address(alice), _amount, address(0));
        accountingManager.deposit(address(alice), _amount, address(0));
        
        console.log("Balance after deposit: %s USDC", IERC20(USDC).balanceOf(alice) / 1e6);

        uint256[] memory arr = new uint256[](5);
        arr[0] = 0;
        arr[1] = 1;
        arr[2] = 2;
        vm.stopPrank();
        vm.startPrank(owner);
        accountingManager.calculateDepositShares(3);

        console.log("-----------Execute Deposit--------------");

        vm.warp(block.timestamp + 35 minutes);

        accountingManager.executeDeposit(10, connector, "");
    
    }
```

# **Impact**

The attacker could DoS the deposit flow and an
Insufficient access control in modifier `NoyaGovernanceBase.onlyManager()` allows a Vault to access another Vault's `onlyManager` functions
Audit: 2024-04-noya-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-noya/blob/9c79b332eff82011dcfa1e8fd51bad805159d758/contracts/governance/NoyaGovernanceBase.sol#L33


# Vulnerability details

## Impact 
The keeper of a Vault can steal funds from another Vault. The keeper of a Vault can also access any Vault's `onlyManager` functions, modify or execute omnichain operations,  access `AccountingManager` priviledge functions and access a Connector's privilege functions.  
## Proof of Concept 
Alongside the keeper contract and the emergency address, the modifier `NoyaGovernanceBase.onlyManager()` also allows the `BalancerFlashLoan` contract to continue execution. The `BalancerFlashLoan` contract is a single instance assigned in the `Registry` contract. The issue is that in `BalancerFlashLoan.receiveFlashLoan()` any registered Vault can instruct the `BalancerFlashLoan` contract to perform arbitrary operations [code snippet](https://github.com/code-423n4/2024-04-noya/blob/9c79b332eff82011dcfa1e8fd51bad805159d758/contracts/connectors/BalancerFlashLoan.sol#L81-L82). Therefore, a Vault can access other Vault's functions that are supposed to be protected by the `onlyManager` modifier, which includes access to a Vault's `AccountingManager` deposit or withdraw functions, omnichain bridging functions, and deposit, withdraw and swap functions inside Connectors.

## Coded POC  
The aim of the POC is to showcase how one Vault can steal funds from another Vault's Pendle Connector. The test consists of 2 Vaults (ID-5 and ID-10).  In the beginning of the test  
ID-5 receives and executes deposits that go into its `PendleConnector`. The keeper of ID-10 then leverages the `BalancerFlashLoan` contract to retrieve the funds to an arbitrary address.

* Add the contents of the following gist in a solidity file under `/testsFoundry` - [poc gist](https://gist.github.com/alexxander77/eceff97732ee74322d8c43b7f39e650f)
* Execute with `forge test --match-test testVaultIssueGovernance -vv --fork-url <rpc url>`

## Tools used
Manual Review
## Recommended Mitigation Steps
In the case of a flash loan the `NoyaGovernanceBase.onlyManager()` also must check if the caller into `BalancerFlashLoan.makeFlashLoan()` is authorized to perform `onlyManager()` calls.


## Assessed type

Access Control
executeDeposit could be DoS and non functional if receiver address(0).
Audit: 2024-04-noya-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-noya/blob/9c79b332eff82011dcfa1e8fd51bad805159d758/contracts/accountingManager/AccountingManager.sol#L277-L278


# Vulnerability details

## Impact
Detailed description of the impact of this finding.

## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

## Tools Used

## Recommended Mitigation Steps


## Assessed type

Invalid Validation
Title: Missing Access Control Allows Unauthorized Token Transfers and Operations
Audit: 2024-04-noya-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-noya/blob/main/testFoundry/utils/mocks/ConnectorMock.sol#L17


# Vulnerability details


Finding Submission
Risk Rating: High Risk

Issue Type: Security Vulnerability - Access Control

Title: Missing Access Control Allows Unauthorized Token Transfers and Operations

Links to Affected Code:

ConnectorMock.sol: Lines 17-33
Vulnerability Details
Impact
The ConnectorMock contract lacks proper access control mechanisms, allowing any external address to call its functions. This absence of restriction can lead to unauthorized and potentially malicious actions, including:

Transferring tokens to unintended addresses.
Executing swaps and bridge operations without proper authorization.
Exposing the contract to potential abuse and financial loss.
Proof of Concept
The following functions can be called by any external address due to the lack of access control:

sendTokensToTrustedAddress
swap
bridge
For instance, in the current implementation of the sendTokensToTrustedAddress function:function sendTokensToTrustedAddress(address token, uint256 amount, address caller, bytes memory data)
    external
    returns (uint256)
{
    IERC20(token).safeTransfer(msg.sender, amount);
    return amount;
}
There is no modifier to restrict access, meaning any address can call this function and transfer tokens from the contract to themselves.

Tools Used
Manual code review
Recommended Mitigation Steps
Implement Access Control: Use OpenZeppelin’s Ownable or AccessControl libraries to restrict sensitive functions to authorized addresses only.
Add Modifiers: Apply onlyOwner or appropriate access control modifiers to the following functions:
sendTokensToTrustedAddress
swap
bridge
Example Fix
Adding Access Control using Ownable:import "@openzeppelin/contracts/access/Ownable.sol";

contract ConnectorMock is Ownable {
    using SafeERC20 for IERC20;

    receive() external payable { }
    fallback() external payable { }
    constructor() { }

    function sendTokensToTrustedAddress(address token, uint256 amount, address caller, bytes memory data)
        external
        onlyOwner
        returns (uint256)
    {
        IERC20(token).safeTransfer(msg.sender, amount);
        return amount;
    }

    function swap(
        address swapHandler,
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        bytes memory swapData,
        uint256 routeId
    ) external onlyOwner {
        SwapAndBridgeHandler(swapHandler).executeSwap(
            SwapRequest(address(this), routeId, amountIn, tokenIn, tokenOut, swapData, false, 0)
        );
    }

    function bridge(address swapHandler, BridgeRequest calldata bridgeRequest) external onlyOwner {
        SwapAndBridgeHandler(swapHandler).executeBridge(bridgeRequest);
    }
}
By implementing these changes, only the contract owner can execute the functions, mitigating the risk of unauthorized access and ensuring the security and integrity of the contract operati
In the PendleConnector during the burnLP, receiverSy and receiverPt should be address(connector) but the receiverPT is market instead. 
Audit: 2024-04-noya-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-noya/blob/9c79b332eff82011dcfa1e8fd51bad805159d758/contracts/connectors/PendleConnector.sol#L202-L208


# Vulnerability details

# **Vulnerability Detail**

During the burnLP function in the Pendle connector, receiverPt is incorrectly set to the address(market), which will result in the loss of the of the PT tokens. According to the devs comment, he agreed that both tokens should be forward to the connector.(#2 question)

https://ibb.co/R9yb1qf 

According to the Pendle docs PT token the principal portion of an underlying yield-bearing asset. This is the base, principal asset deployed in the underlying protocol such as Lido, Renzo, and Aave (e.g. ETH in stETH, ETH in ezETH, USDC in aUSDC).

# **Proof of Concept**

It is a burnLP function in the PendleConnector, you can see that the receiverPT(second address) is address(market)

```solidity
 function burnLP(IPMarket market, uint256 amount) external onlyManager nonReentrant {
        IERC20(address(market)).safeTransfer(address(market), amount);
        //@audit q- why the receiverPt is the 
        market.burn(address(this), address(market), amount);
        market.skim();
        emit BurnLP(address(market), amount);
    }
```

It is a burn function directly from the PendleMarket.sol.

```solidity
function burn(
        address receiverSy,
        address receiverPt,
        uint256 netLpToBurn
    ) external nonReentrant returns (uint256 netSyOut, uint256 netPtOut) {
        MarketState memory market = readState(msg.sender);

        _burn(address(this), netLpToBurn);

        (netSyOut, netPtOut) = market.removeLiquidity(netLpToBurn);

        if (receiverSy != address(this)) IERC20(SY).safeTransfer(receiverSy, netSyOut);
        if (receiverPt != address(this)) IERC20(PT).safeTransfer(receiverPt, netPtOut);

        _writeState(market);

        emit Burn(receiverSy, receiverPt, netLpToBurn, netSyOut, netPtOut);
    }
```

# **Impact**

The principal token would be sent to the incorrect address. Thus, the underlying asset would be lost.

# **Recommendation**

Change receiverPT to the address(connector)


## Assessed type

Error