xanoutas/solidity-security-findings huggingface dataset:
Upgraded Q -> 2 from #1028 [1725566988368]
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1028 as 2 risk. The relevant finding follows:

 11. Weird operator approval logic in nuke function
Links to affected code *

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L153-L162

Impact
In the nuke function, there are two approval checks. The first is standard, ensures that msg.sender is either the owner or is approved to spend the token. This means the token owner, token operator and any approved user can nuke the token. The next approval check then ensures that NukeFund.sol is approved to spend the token or is the operator of msg.sender. This logic means that NukeFund.sol has to be made the operator of the caller, not the owner for nuking to be successful. If owner approves NukeFund.sol to be one of his operators through the setApprovalForAll function. This operator status is ignored when the owner's approved user or another operator attempts to nuke the token. It fails due to the check marked @note below.

  function nuke(uint256 tokenId) public whenNotPaused nonReentrant {
    require(
      nftContract.isApprovedOrOwner(msg.sender, tokenId),
      'ERC721: caller is not token owner or approved'
    );
    require(
      nftContract.getApproved(tokenId) == address(this) ||
        nftContract.isApprovedForAll(msg.sender, address(this)), //@note
      'Contract must be approved to transfer the NFT.'
    );
To prove this, the test case below can be copied and pasted into NukeFund.test.ts.

  it('should nuke a token from operator', async function () {
    const tokenId = 1;

    // Mint a token
    await nft.connect(owner).mintToken(merkleInfo.whitelist[0].proof, {
      value: ethers.parseEther('1'),
    });

    // Send some funds to the contract
    await user1.sendTransaction({
      to: await nukeFund.getAddress(),
      value: ethers.parseEther('1'),
    });

    const prevNukeFundBal = await nukeFund.getFundBalance();
    // Ensure the token can be nuked
    expect(await nukeFund.canTokenBeNuked(tokenId)).to.be.true;

    const prevUserEthBalance = await ethers.provider.getBalance(
      await owner.getAddress()
    );
    
    // Approve user1
    await nft.connect(owner).approve(user1, tokenId);

    //user1 approves contract to be his own operator
    await nft.connect(owner).setApprovalForAll(await nukeFund.getAddress(), true);

    await nukeFund.connect(user1).nuke(tokenId);

    const curUserEthBalance = await ethers.provider.getBalance(
      await owner.getAddress()
    );

    const curNukeFundBal = await nukeFund.getFundBalance();
    expect(curUserEthBalance).to.be.gt(prevUserEthBalance);
    // Check if the token is burned
    // expect(await nft.ownerOf(tokenId)).to.equal(ethers.ZeroAddress);
    expect(await nft.balanceOf(owner)).to.eq(1);
    expect(curNukeFundBal).to.be.lt(prevNukeFundBal);
  });
The test reverts with a "Contract must be approved to transfer the NFT" error despite the fact that the owne
Funds can be locked indefinitely in NukeFund.sol
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L40-L61
https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L153-L183


# Vulnerability details

## Impact
- Permanent loss of funds
- Dependency on user action

## Proof of Concept
The NukeFund contract has a unique fund distribution model where the primary - and only - method to withdraw funds is through the nuke() function. This design creates a specific set of circumstances under which funds can be extracted from the contract.

Practical example:
Let's say the contract has accumulated 100 ETH in its fund. A user holding a qualified NFT (let's call it TokenID 42) wants to claim a portion of this fund.

```
function nuke(uint256 tokenId) public whenNotPaused nonReentrant {
    // ... (verification checks)
    uint256 claimAmount = /* calculated based on nuke factor */;
    fund -= claimAmount;
    nftContract.burn(tokenId);
    payable(msg.sender).call{value: claimAmount}("");
    // ... (event emissions)
}
```
https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L153-L183

In this scenario:

- The user calls nuke(42).
- The function calculates the claim amount based on the token's properties.
- The NFT (TokenID 42) is burned.
- The calculated amount is sent to the user.
- The fund is reduced by the claimed amount.

This process works as intended, but it relies entirely on NFT holders initiating the withdrawal process. There's no other mechanism for fund distribution or withdrawal, which leads to the second point.

2. Risk of permanently locked funds

If all NFTs are burned or if no one calls the nuke() function, there's a real risk that funds could remain permanently locked in the contract.

Practical example:
Imagine a scenario where the contract has accumulated 500 ETH over time. There are only 10 NFTs left that are eligible for nuking.

Scenario A: All NFTs are burned

- Users with the remaining 10 NFTs all call nuke().
- After these transactions, all NFTs are burned.
- The contract still holds, say, 100 ETH (depending on the nuke factor calculations).
- There are no more NFTs left to trigger the nuke() function.
- The remaining 100 ETH is now locked in the contract with no way to withdraw it.

OR

- Users decide to directly call burn() to ball their NFTs

```
 function burn(uint256 tokenId) external whenNotPaused nonReentrant {
    require(
      isApprovedOrOwner(msg.sender, tokenId),
      'ERC721: caller is not token owner or approved'
    );
    if (!airdropContract.airdropStarted()) {
      uint256 entropy = getTokenEntropy(tokenId);
      airdropContract.subUserAmount(initialOwners[tokenId], entropy);
    }
    _burn(tokenId);
  }

```
https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitFo
Wrong updation of lasttransferredtimestamp of an entity
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/TraitForgeNft.sol#L377-L379
https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityTrading/EntityTrading.sol#L53
https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityTrading/EntityTrading.sol#L104


# Vulnerability details

## Impact
Incorrectly updating the `lasttransferredtimestamp` variable will unfairly affect the nuking time period of the entity.

## Proof of Concept
In the EntityTrading contract, when a user lists an NFT for sale, and then cancels this listing, the entities `lasttransferredtimestamp` is updated even though there was no transfer of the NFT. 

1. User calls listNFTForSale() function, which in turn calls the transferFrom function to transfer the NFT to address(this) to facilitate the transfer.
(https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityTrading/EntityTrading.sol#L53)

2. The transferFrom function internally calls the _beforeTokenTransfer function in TraitForgeNFT contract. Which updates the `lastTokenTransferredTimestamp` variable.

3. The same updation happens when the listing is cancelled.(https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityTrading/EntityTrading.sol#L104)

This is an unintended behavior as there is no transfer of the NFT in a general sense.
## Tools Used
Manual Review

## Recommended Mitigation Steps
Update the _beforeTokenTransfer() function in TraitForgeNFT.sol as the following (line 377):
`if (from != to && from != address(EntityTradingContract) to != address(EntityTradingContract)) {`
     ` lastTokenTransferredTimestamp[firstTokenId] = block.timestamp;`
 `   }`


## Assessed type

Other
There is no mechanism to withdraw funds from NukeFund contract, this causes the remaining funds on NukeFund to be locked
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L1-L195


# Vulnerability details

## Impact

The remaining funds on NukeFund contract to be locked

## Proof of Concept

In general, the `NukeFund.sol` contract has a function for users who want to burn the NFTs they own and exchange them for the funds available in the contract. The calculation of funds that users receive is determined from various parameters and the maximum funds that can be claimed are 50% of the total funds available in the contract.

```solidity
uint256 maxAllowedClaimAmount = fund / maxAllowedClaimDivisor; // Define a maximum allowed claim amount as 50% of the current fund size
```

This way, there will always be funds remaining from each claim made by the user and cannot be withdrawn by the TraitForge team and are locked forever in the contract. 

This would be a problem if the `NukeFund.sol` contract had a vulnerability and the funds were required to be moved. Of course this cannot be done because the feature does not exist.

## Tools Used

Manual review

## Recommended Mitigation Steps

Consider add withdraw function which can only be done by the owner (preferably a multisig)


## Assessed type

Other
It is possible for users who has claimed rewards and contribute it back to DevFund.sol to be unable to claim their portion of the funds
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/DevFund/DevFund.sol#L69


# Vulnerability details

## Impact
Claimable assets can be lost for the claimer, if the claimer chooses to fund back the contract and redistribute the claimed amount.

## Proof of Concept
In `DevFund.sol`, in the `claim()` function, users will receive their claimable amount if they have any. Since this reward received is the native currency, the user is granted control at this point. The user can transfer whatever amount that is received back to the `DevFund.sol` contract at this point as a way of contributing back to the dev fund. 

`totalRewardDebt` is used to account for how much each user should receive in terms of rewards based on the weightage of each dev. However, because `claim()` overwrites `info.rewardDebt` at the end of the function, this particular sender will have their `totalRewardDebt` overwritten to the latest value, without the sender having claimed yet. This will result in the lost of assets based on how much they should be distributed.

For instance, if Alice has 10% weightage and Bob has 90% weightage, and total claimable fund is 1000 ETH, when Bob claims their share of asset, they receive 900 ETH. Bob decides to fund back the contract and redistribute them back right when the amount is received. The contract should compute Alice as now having 100 ETH (old) + 90 ETH (new) and Bob should have 810 ETH (new). 

However, due to the described issue of `totalRewardDebt` being overwritten, the 810 ETH is lost forever and can never be claimed.

Setting this issue at medium severity, as it is a loss of fund, but conditioned on the claimer funding back the contract immediately when ETH is received.


## Tools Used
Manual review

## Recommended Mitigation Steps
Consider updating `rewardDebt` before transferring rewards to `msg.sender`.



## Assessed type

ETH-Transfer
A dev will lose rewards if after claiming his rewards he mints an NFT
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/DevFund/DevFund.sol#L69
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/DevFund/DevFund.sol#L74


# Vulnerability details

## Summary

A developer may lose rewards if the `receive` function is triggered during the reward claim process from `DevFund`, resulting in their `info.rewardDebt` being higher than expected. This scenario can occur when a developer claims their rewards and then immediately mints an NFT with the funds, but the airdrop hasn’t yet started, they stand to lose those rewards. This happens because the system updates the user's `rewardDebt` after the callback.

## Vulnerability Details

When claiming the dev's rewards are calculated as follows: 
`uint256 pending = info.pendingRewards +
      (totalRewardDebt - info.rewardDebt) *
      info.weight;`

Then we do a call to that dev to transfer his funds and only after that call do we update his reward debt to the total reward debt.

This can create a flow in which the developer loses a portion of his rewards.
For example if the dev has a contract that receives his rewards and wants to use that money to then buy an NFT, as he believes in the protocol and wants to support it, he would have a `receive` function like this:

```solidity
 receive() external payable {
      traitForgeNFtContract.mintWithBudget{ value: address(this).balance }("");
 }
```

However in this scenario the `DevFund::receive` function will be triggered (if the airdrop hasn't started) which increases `totalRewardDebt` and only after that the dev's info is updated to the `totalRewardDebt`, which means that the user will later receive less rewards because `info.rewardDebt` will include the new rewards from the received minting fees but the dev didn't claim them.

## Proof of Concept

This is `DevFund::claim()`:

```solidity
  function claim() external whenNotPaused nonReentrant {
    DevInfo storage info = devInfo[msg.sender];

    uint256 pending = info.pendingRewards +
      (totalRewardDebt - info.rewardDebt) *
      info.weight;

    if (pending > 0) {
@->      uint256 claimedAmount = safeRewardTransfer(msg.sender, pending);
      info.pendingRewards = pending - claimedAmount;
      emit Claim(msg.sender, claimedAmount);
    }

@->    info.rewardDebt = totalRewardDebt;
  }
```

We can see that the `info.rewardDebt` happens after the call to the dev so now if the dev immediately calls the TraitForgeNft contract like that:

```solidity
 receive() external payable {
      traitForgeNFtContract.mintWithBudget{ value: address(this).balance }("");
 }
```

This will mint the developer some NFTs and for each minted NFT the `DevFund::receive()` will be triggered. The minting in `TraitForgeNft` distributes the fees to the `NukeFund` contract and if the airdrop hasn't started yet, the rewards are then redirected to the `DevFund` contract which triggers the `receive` function and the `totalRewardDebt` is updated:

```soli
NFT seller could `grief buyer` when they call `buyNFT` by making the call revert when receiving funds
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/EntityTrading/EntityTrading.sol#L77


# Vulnerability details

## Description
There is a weak spot in `EntityTrading::buyNFT` which allow the NFT seller to grief the buyer which seems to warrant `Medium`. 

The root cause is due to the fact that `buyNFT` is sending the `funds directly to the seller` (the NFT price) when selling which can be exploited by a malicious player as the seller could be a contract which always revert (or it could be dynamically adjusted depending on who is forging by the attacker) upon receiving funds. 

There would be no benefits really for the attacker beside annoying the players and the game owner as the players impacted will end up `wasting gas` and they might think there is actually `a bug in the game` instead of a malicious seller making the game owner looking bad. A competitor or a hater of the game could do this with multiple NFT minted at low prices, poluting the EntityTrading contract, and the owner will not be able todo much to mitigate this.

## Impact
NFT seller could grief any buyer which try to buy their NFT by forcing the transaction to revert.

## PoC
Add the following changes `EntityTrading.test.ts` and run `yarn test`
The test will pass prooving the impact validity.

Create this malicious seller contract inside `EntityTrading folder` and call it `MaliciousSeller.sol`
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./IEntityTrading.sol";
import "../TraitForgeNft/ITraitForgeNft.sol";

contract MaliciousSeller {
    receive() external payable {
        revert("Always revert");
    }

    function listNFT(
        uint256 tokenId,
        uint256 price,
        address entityTradingAddress,
        address nftAddress
    ) external {
        ITraitForgeNft(nftAddress).approve(entityTradingAddress, tokenId);
        IEntityTrading(entityTradingAddress).listNFTForSale(tokenId, price);
    }
}
```

```diff
import { ethers } from 'hardhat';
-import { EntityTrading, TestERC721 } from '../typechain-types';
+import { EntityTrading, TestERC721, MaliciousSeller } from '../typechain-types';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { expect } from 'chai';
```

```solidity
  it.only('allow seller to grief buyer by reverting when receiving funds', async function () {
    // 1) Deploy a malicious seller contract that will revert when receiving ETH
    let maliciousSeller: MaliciousSeller;
    const MaliciousSeller = await ethers.getContractFactory('MaliciousSeller');
    maliciousSeller = (await MaliciousSeller.deploy()) as MaliciousSeller;

    // 2) Transfer the NFT (could be Merger or Forger) to the malicious contract.
    await nft.transferFrom(owner.address, await maliciousSeller.getAddress(), TOKEN_ID);

    // 3) List the NFT
    await maliciousSeller.listNFT(TOKEN_ID, LISTING_PRICE, await entityTrading.getAddress(), await nft.getAddress());

    const lis
Malicious NFT Owner who lists his NFT for forge, can DoS forging requested from Mergers
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityForging/EntityForging.sol#L158-L159


# Vulnerability details

## Impact
When an NFT is bought via buyNFT in EntityTrading.sol, the seller gets a fee. However, this results in a contract call to an arbitrary address, i.e., the seller. The seller can permanently DoS the buy operation, as we require that the call is successful, so this would result in the NFT just being publicly listed on the TraitForge DAPP but never being possible to be bought.

## Proof of Concept
User story: A seller decides to put up his NFT for sale, and a buyer decides to buy it. However, the seller is malicious.

Step 1: The seller lists his NFT via `listNFTForSale()` in the EntityTrading contract.

Step 2: The buyer calls `buyNFT()` in EntityTrading.sol. However, due to the fact that we require the success status of the call, the seller can forcefully cause a revert in its receive() function, which would lead to the call failing, thus DoSing the operation.

```solidity
  function buyNFT(uint256 tokenId) external payable whenNotPaused nonReentrant {
    Listing memory listing = listings[listedTokenIds[tokenId]];
    require(
      msg.value == listing.price,
      'ETH sent does not match the listing price'
    );
    require(listing.seller != address(0), 'NFT is not listed for sale.');

    //transfer eth to seller (distribute to nukefund)
    uint256 nukeFundContribution = msg.value / taxCut;
    uint256 sellerProceeds = msg.value - nukeFundContribution;
    transferToNukeFund(nukeFundContribution); // transfer contribution to nukeFund

    // transfer NFT from contract to buyer
@>>  (bool success, ) = payable(listing.seller).call{ value: sellerProceeds }('');
@>> require(success, 'Failed to send to seller');
    nftContract.transferFrom(address(this), msg.sender, tokenId); // transfer NFT to the buyer

    delete listings[listedTokenIds[tokenId]]; // remove listing

    emit NFTSold(
      tokenId,
      listing.seller,
      msg.sender,
      msg.value,
      nukeFundContribution
    ); // emit an event for the sale
  }
```  

## Coded POC
Prerequisites:
- Setup foundry in hardhat project. You can use [this](https://hardhat.org/hardhat-runner/docs/advanced/hardhat-and-foundry) and then run forge init.

Remove onlyOwner modifier from addUserAmount and subUserAmount in Airdrop.sol as its OOS issue:
```diff
-  function addUserAmount(address user, uint256 amount) external whenNotPaused nonReentrant onlyOwner {
+  function addUserAmount(address user, uint256 amount) external whenNotPaused nonReentrant {  
```
```diff
- function subUserAmount(address user, uint256 amount) external whenNotPaused nonReentrant onlyOwner {
+ function subUserAmount(address user, uint256 amount) external whenNotPaused nonReentrant { 
```

Add this FoundryTest.t.sol into /test folder.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {Test} from "
Forger NFT owner could `grief merger` when they call `forgeWithListed` by making the call revert when receiving funds
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/EntityForging/EntityForging.sol#L158


# Vulnerability details

## Description
There is a weak spot in `EntityForging::forgeWithListed` which allow the Forger NFT owner to grief the Merger which seems to warrant `Medium`. 

The root cause is due to the fact that `forgeWithListed` is sending the `funds directly to the Forger` (the forge fees) when forging which can be exploited by a malicious player as the Forger owner could be a contract which always revert (or it could be dynamically adjusted depending on who is forging by the attacker) upon receiving funds. 

There would be no benefits really for the attacker beside annoying the players and the game owner as the players impacted will end up `wasting gas` and they might think there is actually `a bug in the game` instead of a malicious Forger making the game owner looking bad. A competitor or a hater of the game could do this with multiple Forger NFT token minted at low prices, poluting the EntityForging contract, and the owner will not be able todo much to mitigate this.

## Impact
Forger NFT owner could grief any Merger which try to forge using their NFT by forcing the transaction to revert.

## PoC
Add the following changes `EntityForging.test.ts` and run `yarn test`
The test will pass prooving the impact validity.

Create this malicious forger contract inside `EntityForging folder` and call it `MaliciousForger.sol`
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./IEntityForging.sol";

contract MaliciousForger {
    receive() external payable {
        revert("Always revert");
    }

    function listForger(
        uint256 tokenId,
        uint256 fee,
        address entityForgingAddress
    ) external {
        IEntityForging(entityForgingAddress).listForForging(tokenId, fee);
    }
}
```

```diff
import {
  Airdrop,
  DevFund,
  EntityForging,
  EntropyGenerator,
  EntityTrading,
  NukeFund,
  TraitForgeNft,
+ MaliciousForger,
} from '../typechain-types';
```

```solidity
    it.only('allow forger owner to grief merger by reverting when receiving funds', async () => {
      const forgerTokenId = FORGER_TOKEN_ID;
      const mergerTokenId = MERGER_TOKEN_ID;

      // 1) Deploy a malicious forger contract that will revert when receiving ETH
      let maliciousForger: MaliciousForger;
      const MaliciousForger = await ethers.getContractFactory('MaliciousForger');
      maliciousForger = (await MaliciousForger.deploy()) as MaliciousForger;

      // 2) Transfer the forger token to the malicious contract. It could be minted too, but this is just to make the test easier.
      await nft.connect(owner).transferFrom(owner.address, await maliciousForger.getAddress(), forgerTokenId);

      // 3) List the forger NFT
      await maliciousForger.listForger(forgerTokenId, FORGING_FEE, await entityForging.getAddress());
    
      // 4) Attempt to forge with the listed token
      awai