xanoutas/solidity-security-findings huggingface dataset:
Superposition's implementation of `SafeTransferFrom()` is broken
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L139-L158
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L73-L96


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L139-L158

```solidity
    function safeTransferFrom(
        address _from,
        address _to,
        uint256 _tokenId
    ) external payable {
        _transfer(_from, _to, _tokenId);
        _onTransferReceived(msg.sender, _from, _to, _tokenId);
    }

    /// @inheritdoc IERC721Metadata
    function safeTransferFrom(
        address _from,
        address _to,
        uint256 _tokenId,
        bytes calldata /* _data */
    ) external payable {
        _transfer(_from, _to, _tokenId);
        _onTransferReceived(msg.sender, _from, _to, _tokenId);
    }

```

Both functions query the callback after the transfer: https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L73-L96

```solidity
    function _onTransferReceived(

        address _sender,
        address _from,
        address _to,
        uint256 _tokenId
    ) internal {
        // only call the callback if the receiver is a contract
        if (_to.code.length == 0) return;

        bytes4 data = IERC721TokenReceiver(_to).onERC721Received(
            _sender,
            _from,
            _tokenId,

            // this is empty byte data that can be optionally passed to
            // the contract we're confirming is able to receive NFTs
            ""
        );

        require(
            data != IERC721TokenReceiver.onERC721Received.selector,
            "bad nft transfer received data"
        );
    }
```

This is a classic implementation of ensuring safe transfers for NFT's and in this case it's used by calling the callback `onERC721Received`in the recipient if they have `codesize > 0`. Per the [documentation](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L63-L71) and as expected, if the callback
doesn't return the selector, the function should revert.

Issue however is that the opposite is being checked for in the function, see https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L91-L95

```solidity

        require(
            data != IERC721TokenReceiver.onERC721Received.selector,
            "bad nft transfer received data"
        );
```

## Impact

Intended functionality is broken, considering any function/contracts that needs to query this to execute it's flow would always revert [here](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg
Anyone can acquire unlimited Cred
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/main/src/Cred.sol#L685
https://github.com/code-423n4/2024-08-phi/blob/main/src/Cred.sol#L695


# Vulnerability details


https://github.com/code-423n4/2024-08-phi/blob/main/src/Cred.sol#L685
https://github.com/code-423n4/2024-08-phi/blob/main/src/Cred.sol#L695

## Impact
`_addCredIdPerAddress` is public and it has no access restrictions. The function adds a new `credId` to the list of credentials associated with a user's address. This is crucial for keeping track of which credentials each user has interacted with, especially in a system where users can own or trade multiple credentials.

## Proof of Concept
This function is called during transactions where a user acquires a new credential, ensuring that their association with the credential is recorded and easily accessible for future operations and it should be internal.

## Tools Used
Manual reveiw 

## Recommended Mitigation Steps
Make the function internal

```diff
[src/Cred.sol]
-       function _addCredIdPerAddress(uint256 credId_, address sender_) public {
+       function _addCredIdPerAddress(uint256 credId_, address sender_) internal {    
686         // Add the new credId to the array
687         _credIdsPerAddress[sender_].push(credId_);
688         // Store the index of the new credId
689         _credIdsPerAddressCredIdIndex[sender_][credId_] = _credIdsPerAddressArrLength[sender_];
690         // Increment the array length counter
691         _credIdsPerAddressArrLength[sender_]++;
692     }
```

> Note:
> The same applies for `_removeCredIdPerAddress` function.






## Assessed type

Access Control
Curators may receive rewards after a delay because of an inefficient function that consumes a ton of gas to distribute rewards
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/main/src/reward/CuratorRewardsDistributor.sol#L77
https://github.com/code-423n4/2024-08-phi/blob/main/src/reward/CuratorRewardsDistributor.sol#L87


# Vulnerability details

## Impact
The `CuratorRewardsDistributor.sol:distribute` is in charge of distributing the curators' rewards and it should be called by someone. 
The problem is that it is using the push pattern, which makes it so expensive to distribute rewards to all curators because the gas cost increases with the increase of curators. 
This will delay the distribution of rewards when there are many curators because they may experience losses instead of profit if they
distribute the rewards

Curators will receive their rewards with a delay or even lose their profit

## Proof of Concept
Paste the following test in `Cred.t.sol` to see the gas consumption, which is about `32825780` gas
when there are 999 curators:

```javascript
function testLargeGasConsuptionOnDistribution() public {
    uint256 credId = 1;
    bytes32 merkleRoot = 0xdc4c272ceaa7b5ba9e0f3f9972f735f14e1b4ed191e15d0d17afc62792226428;

    // Create the initial cred
    _createCred("ADVANCED", "MERKLE", merkleRoot);

    address[] memory buyers = new address[](998);

    vm.warp(START_TIME + 1);
    // Buy one by one up to 998
    for (uint256 i = 0; i < 998; i++) {
        address buyer = makeAddr(string(abi.encodePacked("buyer", i)));
        buyers[i] = buyer;
        vm.deal(buyer, 11_000 ether);

        vm.startPrank(buyer);
        uint256 currentSupply = cred.credInfo(credId).currentSupply;
        uint256 buyAmount = 1;
        uint256 price = bondingCurve.getBuyPriceAfterFee(credId, currentSupply, buyAmount);

        cred.buyShareCred{ value: price }(credId, buyAmount, 0);

        assertEq(cred.getShareNumber(credId, buyer), buyAmount);
        assertEq(cred.credInfo(credId).currentSupply, currentSupply + buyAmount);
        vm.stopPrank();
    }

    assertEq(cred.credInfo(credId).currentSupply, 999); // 1 initial + 998 purchases
    assertEq(cred.getCuratorAddresses(credId, 0, 0).length, 999);

    // Distribute rewards
    vm.startPrank(owner);
    vm.deal(owner, 1_000_000 ether);
    uint256 depositAmount = 1 ether;

    // Deposit some ETH to the curatorRewardsDistributor
    curatorRewardsDistributor.deposit{ value: depositAmount }(credId, depositAmount);
    uint256 gasBefore = gasleft();
    console2.log("Gas before distribute: ", gasBefore);
    curatorRewardsDistributor.distribute(credId);
    console2.log("Gas after distribute: ", gasleft());
    console2.log("Gas used: ", gasBefore - gasleft());
}
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Replace the push pattern with the pull to give curators the ability to get their rewards











## Assessed type

Loop
The `CuratorRewardsDistributor::distribute()` function contains a lot of loops and may fail to execute if the number of `Curator Addresses` is large.
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/reward/CuratorRewardsDistributor.sol#L77-L130
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/Cred.sol#L440-L457
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/Cred.sol#L897-L935
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/abstract/RewardControl.sol#L54-L90


# Vulnerability details

The `CuratorRewardsDistributor::distribute()` function contains multiple nested loops, which can lead to excessive gas consumption, especially as the number of `Curator Addresses` grows. As marked by the `@>` annotations, the function involves extensive iteration through curator addresses. In extreme cases, the gas consumption can exceed `30,000,000`, the typical gas limit per block on the Ethereum Virtual Machine (EVM). Since each block on the EVM has a gas limit that usually hovers around `30,000,000 Gas` (depending on the network state), any transaction consuming more than this limit will not be included in a block, leading to transaction failure. Consequently, if the gas consumption for this function surpasses the block limit, it may result in the transaction never successfully executing on the Ethereum mainnet.
```js
    // CuratorRewardsDistributor::distribute()
    function distribute(uint256 credId) external {

        // SNIP...

@>        address[] memory distributeAddresses = credContract.getCuratorAddresses(credId, 0, 0);
        uint256 totalNum;


@>        for (uint256 i = 0; i < distributeAddresses.length; i++) {
            totalNum += credContract.getShareNumber(credId, distributeAddresses[i]);
        }

        // SNIP...

        // actualDistributeAmount is used to avoid rounding errors
        // amount[0] = 333 333 333 333 333 333
        // amount[1] = 333 333 333 333 333 333
        // amount[2] = 333 333 333 333 333 333
        uint256 actualDistributeAmount = 0;
@>        for (uint256 i = 0; i < distributeAddresses.length; i++) {
            // SNIP...
        }

        // SNIP...

        //slither-disable-next-line arbitrary-send-eth
@>        phiRewardsContract.depositBatch{ value: actualDistributeAmount }(
            distributeAddresses, amounts, reasons, "deposit from curator rewards distributor"
        );

        // SNIP...
    }
    // Cred::getCuratorAddresses()
    function getCuratorAddresses(
        uint256 credId_,
        uint256 start_,
        uint256 stop_
    )
        public
        view
        returns (address[] memory)
    {
        CuratorData[] memory curatorData = _getCuratorData(credId_, start_, stop_);
        address[] memory result = new address[](curatorData.length);


@>        for (uint256 i = 0; i < curatorData.length; i++) {
            result[i] = curatorData[i].curator;
        }


        return result;
    }
    // Cred::_getCuratorData()
    function _getC
Signature replay in `signatureClaim` results in unauthorized claiming of rewards
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L336-L340


# Vulnerability details

## Impact

Loss of funds due to unauthorized and multiple claiming of art rewards: signature obtained from the Phi signer on one chain can be employed to claim NFT rewards on any other chain.

## Summary

Contract `PhiFactory` includes the function [signatureClaim](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L327-L346), which is intended for claiming art rewards using a signature obtained from the Phi signing authority. Unfortunately, the signature validation in this function is faulty, as it doesn't validate the chain id for which the signature has been issued:

```solidity
(uint256 expiresIn_, address minter_, address ref_, address verifier_, uint256 artId_,, bytes32 data_) =
    abi.decode(encodeData_, (uint256, address, address, address, uint256, uint256, bytes32));


if (expiresIn_ <= block.timestamp) revert SignatureExpired();
if (_recoverSigner(keccak256(encodeData_), signature_) != phiSignerAddress) revert AddressNotSigned();
```
Notice the fragmen  `uint256 artId_,, bytes32 data_`: in between of `artId` and `data_` there is the encoded `chainId`, which is ignored. 

There is a correct path for signature claims, which goes via `PhiNFT1155` -> [Claimable::signatureClaim](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/abstract/Claimable.sol#L22-L44), which unpacks the supplied data, but submits to `PhiFactory::signatureClaim` the data with the chain id substituted with `block.chainid`. Unfortunately, `PhiFactory::signatureClaim` can be called directly, thus bypassing this crucial step. As a result the signature obtained for one chain can be reused for multiple chains. As `PhiFactory` and art contracts on multiple chains are independent, the `artId` referring to some cheap art work on one chain, may refer on another chain to a very valuable art work; claiming this art work without proper authorization leads to direct loss of funds. Here is the scenario:

1. An attacker observes that a valuable art piece exists on chain `A` with `artId`
2. They find a chain `B` where such `artId` is not yet used, and create a bunch of arts till they reach this `artId`
3. They correctly claim `artId` on chain `B`
4. They reuse the obtained signature to claim the art piece with `artId` on chain `A`, thus effectively stealing it.

## Proof of Concept

Drop this test to [Claimable.t.sol](hhttps://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/test/Claimable.t.sol#L172) and execute via `forge test --match-test Kuprum`

```solidity
    function testKuprum_Claim_with_Signature_Replay() public {
        uint256 artId = 1;
        uint256 tokenId = 1;
        uint256 quantity = 1;

        // The signature is obtained on some other chain
        uint256 otherChainId = 999
`CuratorRewardsDistributor.sol#distribute()` may DOS.
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/main/src/reward/CuratorRewardsDistributor.sol#L106


# Vulnerability details

## Impact
`CuratorRewardsDistributor.sol#distribute()` may DOS.

## Proof of Concept
`CuratorRewardsDistributor.sol#distribute()` function is as follows.
```
    function distribute(uint256 credId) external {
        if (!credContract.isExist(credId)) revert InvalidCredId();
        uint256 totalBalance = balanceOf[credId];
        if (totalBalance == 0) {
            revert NoBalanceToDistribute();
        }

        address[] memory distributeAddresses = credContract.getCuratorAddresses(credId, 0, 0);
        uint256 totalNum;

        for (uint256 i = 0; i < distributeAddresses.length; i++) {
            totalNum += credContract.getShareNumber(credId, distributeAddresses[i]);
        }

        if (totalNum == 0) {
            revert NoSharesToDistribute();
        }

        uint256[] memory amounts = new uint256[](distributeAddresses.length);
        bytes4[] memory reasons = new bytes4[](distributeAddresses.length);

        uint256 royaltyfee = (totalBalance * withdrawRoyalty) / RATIO_BASE;
        uint256 distributeAmount = totalBalance - royaltyfee;

        // actualDistributeAmount is used to avoid rounding errors
        // amount[0] = 333 333 333 333 333 333
        // amount[1] = 333 333 333 333 333 333
        // amount[2] = 333 333 333 333 333 333
        uint256 actualDistributeAmount = 0;
        for (uint256 i = 0; i < distributeAddresses.length; i++) {
            address user = distributeAddresses[i];

            uint256 userAmounts = credContract.getShareNumber(credId, user);
            uint256 userRewards = (distributeAmount * userAmounts) / totalNum;

            if (userRewards > 0) {
                amounts[i] = userRewards;
                actualDistributeAmount += userRewards;
            }
        }

        balanceOf[credId] -= totalBalance;

        _msgSender().safeTransferETH(royaltyfee + distributeAmount - actualDistributeAmount);

        //slither-disable-next-line arbitrary-send-eth
        phiRewardsContract.depositBatch{ value: actualDistributeAmount }(
            distributeAddresses, amounts, reasons, "deposit from curator rewards distributor"
        );

        emit RewardsDistributed(
            credId, _msgSender(), royaltyfee + distributeAmount - actualDistributeAmount, distributeAmount, totalBalance
        );
    }
```
If the number of curators of a `credId` is big, this transaction may dos because of gas limit.    
On the other hand, there is no upper limit of the number of curators for a `credId`.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Mitigation steps for this vulnerability are as follows.
1. Set maximum count for the number of curators for a `credId` in `Cred.sol`.
2. Set minimum amount for result share of `curator` when buying and selling cred.


## Assessed type

DoS
Unbounded loops in distribute Function in CuratorRewardsDistributor.sol contract
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/reward/CuratorRewardsDistributor.sol#L87
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/reward/CuratorRewardsDistributor.sol#L88
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/reward/CuratorRewardsDistributor.sol#L106
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/reward/CuratorRewardsDistributor.sol#L107


# Vulnerability details

This loop iterates over the distributeAddresses array and calls credContract.getShareNumber for each address. If the credContract.getShareNumber function is expensive or can be manipulated to consume a lot of gas, a malicious actor could provide a large number of addresses in the distributeAddresses array to intentionally increase the gas cost of the transaction. This could make it prohibitively expensive for honest users to execute the distribute function, effectively preventing them from using the contract.

Vulnerability Details
Unbounded Loop in distributeAddresses: The code iterates over the distributeAddresses array twice:

First Loop: Aggregates the total number of shares each address holds.
Second Loop: Calculates individual rewards and prepares the data for distribution.
Since the distributeAddresses array is populated from an external call to credContract.getCuratorAddresses(), the size of this array can be arbitrarily large. If an attacker (or a natural scenario with many curators) causes this array to be extremely large, the gas required to execute this function can exceed the block gas limit.

Exploit Scenario
An attacker adds a large number of curator addresses to a specific credId by exploiting the credContract.
The attacker or any user tries to call the distribute function.
Due to the large number of addresses in the distributeAddresses array, the loop exceeds the block gas limit, causing the transaction to revert.
This results in a denial of service, where rewards cannot be distributed, effectively locking the funds in the contract.

To mitigate this vulnerability, you can introduce a maximum loop iteration limit:


uint256 maxIterations = 100; // Adjust this value as needed

for (uint256 i = 0; i < distributeAddresses.length && i < maxIterations; i++) {
    // ...
}

This will ensure that the loop terminates after a certain number of iterations, preventing unbounded gas consumption. However, it's important to set the maxIterations value carefully to avoid limiting legitimate use cases.


## Assessed type

Other
Uncapped `mintFee` allows frontrunning attacks on large buy orders
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/main/src/PhiFactory.sol#L248


# Vulnerability details

## Impact
A malicious art creator could monitor the mempool for incoming large buy orders. Just before such an order is processed, they could call `updateArtSettings` to dramatically increase the `mintFee`. This would result in the buyer paying much more than they intended, with the excess fees going to the creator.

## Proof of Concept

An attacker could exploit this by:

1. Creating art with a low initial `mintFee`
2. Monitoring the mempool for large buy orders
3. Quickly calling `updateArtSettings` to increase the `mintFee` just before the buy order is processed
4. The buyer's transaction would then execute with the new, higher fee

```javascript
function updateArtSettings(
    uint256 artId_,
    string memory url_,
    address receiver_,
    uint256 maxSupply_,
    uint256 mintFee_,
    uint256 startTime_,
    uint256 endTime_,
    bool soulBounded_,
    IPhiNFT1155Ownable.RoyaltyConfiguration memory configuration
)
    external
    onlyArtCreator(artId_)
{
    if (receiver_ == address(0)) {
        revert InvalidAddressZero();
    }

    if (endTime_ < startTime_) {
        revert InvalidTimeRange();
    }
    if (endTime_ < block.timestamp) {
        revert EndTimeInPast();
    }

    PhiArt storage art = arts[artId_];

    if (art.numberMinted > maxSupply_) {
        revert ExceedMaxSupply();
    }

    art.receiver = receiver_;
    art.maxSupply = maxSupply_;
@>  art.mintFee = mintFee_;

    // existing code...
}
```

## Tools Used
Manual review

## Recommended Mitigation Steps

Implement a time delay for mint fee changes to take effect. This gives users time to react to fee updates and prevents sudden, unexpected fee increases.

Here's a suggested implementation:

```diff
contract PhiFactory is IPhiFactory, Ownable2StepUpgradeable, PausableUpgradeable, UUPSUpgradeable {
+    uint256 public constant FEE_UPDATE_DELAY = 1 days;
+    mapping(uint256 => PendingFeeUpdate) public pendingFeeUpdates;
+
+    struct PendingFeeUpdate {
+        uint256 newFee;
+        uint256 effectiveTime;
+    }
+

    // existing code...

    function updateArtSettings(
        uint256 artId_,
        string memory url_,
        address receiver_,
        uint256 maxSupply_,
        uint256 mintFee_,
        uint256 startTime_,
        uint256 endTime_,
        bool soulBounded_,
        IPhiNFT1155Ownable.RoyaltyConfiguration memory configuration
    )
        external
        onlyArtCreator(artId_)
    {
        // ... existing checks ...

        PhiArt storage art = arts[artId_];

        // ... other updates ...

-       art.mintFee = mintFee_;
+       pendingFeeUpdates[artId_] = PendingFeeUpdate({
+           newFee: mintFee_,
+           effectiveTime: block.timestamp + FEE_UPDATE_DELAY
+       });
+       emit PendingFeeUpdateCreated(artId_, mintFee_, block.timestamp + FEE_UPDATE_DELAY);

        // ... rest of the function ...
    }