xanoutas/solidity-security-findings huggingface dataset:
Attackers that have compromised signer keys can financially overwhelm the protocol to take it over
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/main/src/Timelock.sol#L521
https://github.com/code-423n4/2024-10-kleidi/blob/main/src/Timelock.sol#L550


# Vulnerability details

## Impact
There is no limit on the number of scheduled tranasctions in the Timelock. As a result, in some cases it may be viable for an attacker that has compromised the signer keys to financially overwhelm the protocol team. They would do that by submitting more transactions than the protocol has cash to cancel(in terms of gas fees).

## Proof of Concept
Suppose the following scenario:
- A Kleidi safe instance is used to manage an admin role for a lending protocol that has 5 million in locked collateral. However, the protocol team only has 250k operating cashflow.
- A malicios entity manages to compromise the private keys of the Safe and schedules a massive number of transactions. Paying say 2 million in gas costs.
- The guardian pauses the protocol and the protocol team manages to gain control of the protocol via a recovery spell, but does not have enough available cash to actually cancel all malicios transactions.
- The attackers execute a malicios transaction and profit 3 million from draining the managed lending protocol.

Note that the guardian may attempt to pause the protocol before the attackers have managed to deploy the huge number of transactions. In the current version of the protocol, the guardian will be dossed because the function to pause will be [running out of gas](https://github.com/code-423n4/2024-10-kleidi/blob/main/src/Timelock.sol#L692) in the loop while canceling scheduled transactions, but this attack is viable regardless. It really depends on the cash that the protocol team has available, it is very easy to spend a huge amount on gas, or in this case make someone else spend a huge amount on gas, especially on mainnet. Right now at ~14 GWEI gas price, each block costs ~1200 USD. At 5 blocks per minute this is 360,000 USD per hour.

## Recommended Mitigation Steps
Allow the protocol to set a variable for a maximum number of scheduled transaction at the same time. That can be customizeable via a cold signer transaction. For example, if the protocol does not expect more than 10 tranasctions at the same time in the worst case, they should be able to set that and avoid the above attack. The check should be enforced on the schedule and scheduleBatch functions.






## Assessed type

Other
Guardian can be DoSed by submitting many transactions at once
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/main/src/Timelock.sol#L687


# Vulnerability details

## Impact
The Guardian role is supposed to pause the timelock in case of compromised cold signer keys, as explained in the [docs](https://github.com/solidity-labs-io/kleidi/blob/main/docs/EDGECASES.md#malicious-safe-signer-takeover-scenario). The pause() function will pause the contract and remove all current live proposals, allowing time for a Recovery spell to rotate to a new set of signers. However, the pause() function does an unbounded loop over the current live proposals, an array that the attackers can add any number of entries to. The attackers can flood the array with entries, essentially making calls to function revert due to running out of gas, thus DoSing the Guardian and essentially rendering the role useless.

If the team does not notice the malicios activity on time and the timelock is not deployed/executed before its too late, the attackers can do serious damage, like draining of funds/complete takeover of the system. Since the guardian role is rendered useless, the protocol will be in a race with the clock to save itself.

## Proof of Concept
We can see that the pause function will load the _liveProposals array into memory and iterate over all of its entries, removing each of them from the unbounded array:

https://github.com/code-423n4/2024-10-kleidi/blob/main/src/Timelock.sol#L687
```js
    function pause() public override {
        /// check that msg.sender is the pause guardian, pause the contract
        super.pause();

        bytes32[] memory proposals = _liveProposals.values();
        for (uint256 i = 0; i < proposals.length; i++) {
            bytes32 id = proposals[i];

            delete timestamps[id];
            assert(_liveProposals.remove(id));

            emit Cancelled(id);
        }
    }
```

Consider the following attack scenario:

- A malicios entity manages to compromise the cold singer keys, enough of them to sign transactions - say 2 out of 3 keys.
- The malicios entity begins submitting massive amounts of malicios transactions to drain the timelock.
- The guardian attempts to pause the contract, but due to the large amount of entries in the liveProposals array, the transaction reverts.
- As a result the guardian is rendered useless by the attackers.

Also, note that the malicios signers can frontrun calls to Timelock::cancel since Safe::execTransaction uses a nonce to check validity. By increasing the nonce in a frontrunning transaction they will make honest transactions revert. So, honest signers can be blocked from manually canceling the malicios transactions.

The protocol team will have the last option of rotating out the signers using a recovery spell. However, if this action is not taken before the time left for the malicios transcation to become valid is less than the recovery spell delay, the malicios transactions will go through.

Furthermore, while only the Safe can schedul
Unable to set to check consecutive parameters
Audit: 2024-10-kleidi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/Timelock.sol#L1119-L1123
https://github.com/code-423n4/2024-10-kleidi/blob/ab89bcb443249e1524496b694ddb19e298dca799/src/BytesHelper.sol#L35-L67


# Vulnerability details

## Impact

Unable to check hot signer call parameters precisely.

## Proof of Concept

When checking call parameters, the [start, end) of the call parameters, that is, from index [start] to [end-1], are parsed and checked.

For example, let's say `start` is 4 and `end` is 6. `sliceBytes` parses 2 bytes, and the parsed bytes are data[4] and data[5].

```solidity
function sliceBytes(bytes memory toSlice, uint256 start, uint256 end)
    public
    pure
    returns (bytes memory)
{
    require(
        start < toSlice.length,
        "Start index is greater than the length of the byte string"
    );
    require(
        end <= toSlice.length,
        "End index is greater than the length of the byte string"
    );
    require(start < end, "Start index not less than end index");

    uint256 length = end - start;
    bytes memory sliced = new bytes(length);

    for (uint256 i = 0; i < length; i++) {
@>      sliced[i] = toSlice[i + start];
    }

    return sliced;
}
```

Therefore, when verifying parameters of a function with multiple parameters, the `startIndex` of the next parameter should be set to be the same as the `endIndex` of the previous parameter(When parameters are contiguous).

For example, let's say we allow a `function deposit(uint16, uint16)` and want to check both parameters. To check this, the call parameters are parsed as follows:

- function signature: data[0]~data[3]
- first parameter: data[4]~[19]
- second parameter: data[20]~[35]

To parse this, the Index should be set as follows:

- Index for checking the first parameter(`_calldataList[contractAddress][selector][0]`)
    - startIndex: 4, endIndex: 20
- Index for checking the second parameter(`_calldataList[contractAddress][selector][1]`)
    - startIndex: 20, endIndex: 36

However, when registering the Index, it is restricted from setting the next `startIndex` to be the same as the previous `endIndex`. Therefore, it is not possible to check consecutive parameters.

```solidity
function _addCalldataCheck(
    address contractAddress,
    bytes4 selector,
    uint16 startIndex,
    uint16 endIndex,
    bytes[] memory data
) private {
    ...
    {
        bool found;
        for (uint256 i = 0; i < indexes.length; i++) {
            ...
            require(
@>              startIndex > indexes[i].endIndex
@>                  || endIndex < indexes[i].startIndex,
                "CalldataList: Partial check overlap"
            );
        }
        ...
    }
    ...
}
```

This is the PoC:

1. First, we demonstrate that `sliceBytes` parses [start]~[end-1]. Add this test at BytesHelper.t.sol file to run it.
    
    ```solidity
    function testPoCSliceBytes() public {
        bytes memory toSlice = hex"0
Hypervisor.stake does not transfer tokens
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

cmichel


# Vulnerability details


## Vulnerability Details

The Hypervisor's `stake` action states:

> token transfer: transfer staking tokens from msg.sender to vault

But no tokens are ever transferred.

## Impact

Anyone with a permission can lock any amount of tokens.

## Recommended Mitigation Steps

Transfer the tokens or clarify how this comment is supposed to be understood.
Can lock more tokens than in contract
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

cmichel


# Vulnerability details


## Vulnerability Details

The `Visor.timeLockERC20` allows locking any amount of tokens exceeding the contract's token balance.

## Impact

The recipient might think that they'll receive the tokens after expiry but it could be that the contract is already out of tokens by then.

## Recommended Mitigation Steps

Make sure that the contract has enough tokens to cover all locks at all times.
Approval for NFT transfers is not removed after transfer
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

cmichel


# Vulnerability details


## Vulnerability Details

The `Visor.transferERC721` does not reset the approval for the NFT.

## Impact

An approved delegatee can move the NFT out of the contract once.
It could be moved to a market and bought by someone else who then deposits it again to the same vault.
The first delegatee can steal the NFT and move it out of the contract a second time.

## Recommended Mitigation Steps

Reset the approval on transfer.
Wrong TimeLockERC20 event emitted
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

cmichel


# Vulnerability details


## Vulnerability Details

The `Visor.timeLockERC721` function emits the `TimeLockERC20` event but should emit `TimeLockERC721` instead.

## Impact

It allows tricking the backend into registering ERC20 token transfers that never happened which could lead to serious issues when something like an accounting app uses this data.

## Recommended Mitigation Steps

Emit the correct event.
Potential reentrancy in the `swapTo` function of `NFTXVaultUpgradeable`
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

shw


# Vulnerability details

## Impact

The `swapTo` function in the `NFTXVaultUpgradeable` contract should use the modifier `nonReentrant` to prevent reentrancy, which could happen when a user receives an NFT and calls the `swapTo` function again in the `onERC721Received` or `onERC1155Received` functions he implemented.

## Proof of Concept

Both the `mintTo`, `redeemTo` includes a `nonReentrant` modifier to prevent reentrancy, while the function `swapTo` does not. Currently, the modifier is applied in the `swap` function instead of `swapTo`.

Referenced code:
[NFTXVaultUpgradeable.sol#L259-L289](https://github.com/code-423n4/2021-05-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXVaultUpgradeable.sol#L259-L289)

## Tools Used

None

## Recommended Mitigation Steps

Move the `nonReentrant` modifier from `swap` to `swapTo`.
NFTXLPStaking Is Subject To A Flash Loan Attack That Can Steal Nearly All Rewards/Fees That Have Accrued For A Particular Vault
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

jvaqa


# Vulnerability details

NFTXLPStaking Is Subject To A Flash Loan Attack That Can Steal Nearly All Rewards/Fees That Have Accrued For A Particular Vault

## Impact

The LPStaking contract does not require that a stake be locked for any period of time.

The LPStaking contract also does not track how long your stake has been locked.

So an attacker Alice can stake, claim rewards, and unstake, all in one transaction.

If Alice utilizes a flash loan, then she can claim nearly all of the rewards for herself, leaving very little left for the legitimate stakers.

The fact that the NFTXVaultUpgradeable contract contains a native flashLoan function makes this attack that much easier, although it would still be possible even without that due to flashloans on Uniswap, or wherever else the nftX token is found.

Since a flash loan will easily dwarf all of the legitimate stakers' size of stake, the contract will erroneously award nearly all of the rewards to Alice.

## Proof of Concept

(1) Wait until an NFTX vault has accrued any significant amount of fees/rewards
(2) FlashLoanBorrow a lot of ETH using any generic flash loan provider
(3) FlashLoanBorrow a lot of nftx-vault-token using NFTXVaultUpgradeable.flashLoan()
(4) Deposit the ETH and nftx-vault-token's into Uniswap for Uniswap LP tokens by calling Uniswap.addLiquidity()
(5) Stake the Uniswap LP tokens in NFTXLPStaking by calling NFTXLPStaking.deposit()
(6) Claim nearly all of the rewards that have accrued for this vault due to how large the flashLoaned deposit is relative to all of the legitimate stakes by calling NFTXLPStaking.claimRewards()
(7) Remove LP tokens from NFTXLPStaking by calling NFTXLPStaking.exit();
(8) Withdraw ETH and nftx-vault-token's by calling Uniswap.removeLiquidity();
(9) Pay back nftx-vault-token flash loan
(10) Pay back ETH flash loan

Here is an example contract that roughly implements these steps in pseudocode:

contract AliceAttackContract {


    bytes32 constant private NFTX_FLASH_LOAN_RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");


    uint256 largeAmountOfEther = 10_000 ether;


    uint256 targetVaultId;


    address targetVaultAddress;


    // attackVaultWithId calls onEthFlashLoan(), which subsequently calls NFTX's onFlashLoan() (flashloans use a callback structure in order to revert if the flash loan is not paid back).

    function attackVaultWithId(uint256 vaultId, address vaultAddress) external {

        targetVaultId = vaultId;
        targetVaultAddress = vaultAddress;

        EthFlashLoanProvider.borrowFlashLoan(largeAmountOfEther); /* this calls onEthFlashLoan() in between mint and burn */

    }


    // onEthFlashLoan is called by the line EthFlashLoanProvider.borrowFlashLoan() in attackVaultWithId() (flashloans use a callback structure in order to revert if the flash loan is not paid back).

    function onEthFlashLoan(...) external {

        NFTXVaultUpgradeable(vaultAddress).flashLoan( /* this calls onFlashLoan() in
An attacker can cause an overflow in the flashLoan function
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

janbro


# Vulnerability details

## Summary
An attacker can cause an overflow in the flashLoan function where 0 tokens are burned after a large amount of tokens are minted, if there is a flash loan fee, due to not utilizing safe math.

## Risk Rating
Critical

## Vulnerability Details
An attacker can craft an amount to mint such that the amount + fee overflows to a small value or even 0, which would leave the user with amount tokens minted and only a small portion of the tokens being burned. This would allow the attacker to mint enough tokens to drain the pool of all NFTs. Note: The fee must be larger than the current totalSupply(), however this attack can be utilized when a new vault with a non zero flash fee is created. 

ERC20FlashMintUpgradeable.sol,
Line 64:
```
function flashLoan(
    IERC3156FlashBorrowerUpgradeable receiver,
    address token,
    uint256 amount,
    bytes memory data
)
    public virtual override returns (bool)
{
    uint256 fee = flashFee(token, amount);
    _mint(address(receiver), amount);
    require(receiver.onFlashLoan(msg.sender, token, amount, fee, data) == RETURN_VALUE, "ERC20FlashMint: invalid return value");
    uint256 currentAllowance = allowance(address(receiver), address(this));
    require(currentAllowance >= amount + fee, "ERC20FlashMint: allowance does not allow refund");
    _approve(address(receiver), address(this), currentAllowance - amount - fee);
    _burn(address(receiver), amount + fee);
    return true;
}
```

## Impact
Pool drained of NFTs

## Proof of Concept
The fee has to be an amount larger than 0, then the amount to loan should be calculated with 115792089237316195423570985008687907853269984665640564039457584007913129639936 - fee.

1. call `flashLoan(receiver, token, 115792089237316195423570985008687907853269984665640564039457584007913129639931, data)`
2. The `fee` is calculated as `5`
3. `_mint(address(receiver), 115792089237316195423570985008687907853269984665640564039457584007913129639931)` mints large value of tokens
4. The attacker drains the contract of NFTs in the `onFlashLoan` function of their malicious contract with their newly minted tokens using `redeem(...)`
5. `require(currentAllowance >= amount + fee, "ERC20FlashMint: allowance does not allow refund");` passes due to the overflow in `amount + fee` being equivalent to 0. The current allowance can be ignored.
6.  `_burn(address(receiver), amount + fee)` results in 0 tokens being burnt, allowing the attacker to successfully drain the contract of all NFTs and keep any remaining tokens.


## Tools Used
Manual Code Review

## Recommended Mitigation Steps
Utilize safe math in the flashLoan function when calculating `amount + fee`
_sendForReceiver is vulnerable to reentrancy. This enables a receiver to drain the remaining fees to distribute. 
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

janbro


# Vulnerability details

## Summary
_sendForReceiver is vulnerable to reentrancy. This enables a receiver to drain the remaining fees to distribute. 

## Risk Rating
Critical

## Vulnerability Details
NFTXFeeDistributor.sol

Line 163: (bool success, bytes memory returnData) = address(_receiver.receiver).call(payload);

_sendForReceiver can be reentered before remaining funds are distributed to other receivers through the external call. The `distribute(...)` function on line 48 has no privilege checks and can be subsequently called by a contract that is listed as a receiver.

## Impact
Stolen funds

## Proof of Concept
Given an amount `1,000x10**18` to distribute, a `defaultTreasuryAlloc` of `2x10**17`, an `_receiver.allocPoint` of `2x10**18` and 5 receivers.
In the best case the receiver is in position 0 of the feeReceivers[vaultId] array. Distribute is called, and the malicious receiver contract receives `(1000*10**18) * 2*10**18 / (10*10**18) = 200x10**18`. The malicious contract then calls `donate()` again in their contracts `receiveRewards` callback function and is awarded `(800*10**18-) * 2*10**18 / (10*10**18) = 160x10**18`. Although the treasury will take a fee everytime `distribute()` is called, the attacker can continue to steal funds as long as the balance of the fee distributor is greathr than `_treasuryAlloc` although the default value in this example is negligible. Subsequent `safeTransfer`'s to other receivers do not revert since the amount sent is calculated based on the current balance of the fund, which allows the attacker to leave no funds in the distributor without worrying about the transaction reverting in subsequent transfers. The following equation can be used to determine how much reward can be taken given n steps:

![formula](https://render.githubusercontent.com/render/math?math=%5csum_{n=0}^n%20%5c_tokenBalance%20*%20%5cfrac{%5c_receiver.allocPoint}{%5c_allocTotal}%20*%20(1%20-%20%5cfrac{%5c_receiver.allocPoint}{%5c_allocTotal})^n)

## Tools Used
Manual Code Review

## Recommended Mitigation Steps
Add a reentrancy guard to `_sendForReceiver` to prevent reentrancy due to the external call
`getRandomTokenIdFromFund` yields wrong probabilities for ERC1155
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

@cmichelio


# Vulnerability details


## Vulnerability Details

`NFTXVaultUpgradeable.getRandomTokenIdFromFund` does not work with ERC1155 as it does not take the deposited `quantity1155` into account. 

## Impact

Assume `tokenId0` has a count of 100, and `tokenId1` has a count of 1.
Then `getRandomId` would have a pseudo-random 1:1 chance for token 0 and 1 when in reality it should be 100:1.

This might make it easier for an attacker to redeem more valuable NFTs as the probabilities are off.

## Recommended Mitigation Steps

Take the quantities of each token into account (`quantity1155`) which probably requires a design change as it's currently hard to do without iterating over all tokens.
Missing access restriction on `NFTXVaultUpgradeable.finalizeFund`
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

@cmichelio


# Vulnerability details


## Vulnerability Details

Missing access restriction on `NFTXVaultUpgradeable.finalizeFund`.

## Impact

Anyone can lock out the manager by calling `finalizeFund` which sets the manager to 0.
This griefing attack can prevent managers from setting correct fees, vault features, eligibility modules, etc. on the Vault which then needs to be redeployed (but users could have already deposited tokens) or go through a lengthy governance process and let the owner restore the manager but then `finalizeFund` can just be called again.


## Recommended Mitigation Steps

Make `finalizeFund` only callable by the manager.
`distribute` DoS on missing `receiveRewards` implementation
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

@cmichelio


# Vulnerability details


## Vulnerability Details

`NFTXEligiblityManager._sendForReceiver` should check `returnData.length == 1` before decoding, otherwise if it returns no return data, the `abi.decode` call and with it the whole `distribute` function will revert.

## Impact

A single badly implemented `feeReceiver` can break the whole `distribute` function and do a denial of service by reverting the transaction.

## Recommended Mitigation Steps

Change to: `bool tokensReceived = returnData.length == 1 && abi.decode(returnData, (bool));`.
Missing overflow check in `flashLoan`
Audit: 2021-05-nftx-findings
Severity: HIGH

# Handle

@cmichelio


# Vulnerability details

## Vulnerability Details

`ERC20FlashMintUpgradeable.flashLoan` does not check for an overflow when adding the fees to the flashloan amount.
The functionality might have been copied from https://eips.ethereum.org/EIPS/eip-3156 but this one already has overflow checks as it uses solidity 0.8.0.


## Impact

This leads to an issue where the attacker does not need to pay back the flashloan as they will burn 0 tokens:

```solidity
_burn(address(receiver), amount + fee);
```

They end up with a huge profit.

> Luckily, this is currently not exploitable as the fee is set to 0 so there's no possibility to overflow. However, if governance decides to change the flashloan fee, flashloans can be taken without having to repay them.



## Recommended Mitigation Steps

Use SafeMath.