xanoutas/solidity-security-findings huggingface dataset:
Potential Signature Replay Attack in `withdrawWithSig` Function
Audit: 2024-08-phi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/abstract/RewardControl.sol#L146
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/abstract/RewardControl.sol#L100


# Vulnerability details

## Impact

The `withdrawWithSig` function is vulnerable to a Signature Replay Attack due to the absence of EIP-155 replay protection. This flaw allows an attacker to reuse a valid signature across different chains, leading to unauthorized withdrawals and potential financial loss.

Specifically, the function `_verifySignature` does not include chain ID in the signature, which is crucial for preventing replay attacks across different chains. Without this protection, a valid signature on one chain can be reused on another, and an attacker can exploit this vulnerability to perform unauthorized withdrawals on different chains using the same signature, potentially causing significant financial damage.

## Recommended Mitigation Steps

To fix the issue, include the chain ID in the signature by modifying the `structHash`:

```
function _verifySignature(
    address from,
    address to,
    uint256 amount,
    uint256 nonce,
    uint256 deadline,
    bytes calldata sig
)
    internal
    view
    returns (bool)
{
    uint256 chainId;
    assembly {
        chainId := chainid()
    }
    bytes32 structHash = keccak256(abi.encode(WITHDRAW_TYPEHASH, from, to, amount, nonce, deadline, chainId));
    bytes32 digest = _hashTypedData(structHash);
    return SignatureCheckerLib.isValidSignatureNowCalldata(from, digest, sig);
}
```


## Assessed type

Other
Excess ether sent while claiming art rewards is not returned to the caller, instead goes to protocol fee address
Audit: 2024-08-phi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L264-L304
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L352-L383
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L327-L346
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L723-L758


# Vulnerability details

## Impact
While claiming art rewards, `PhiFactory::claim(...)` logic identifies the claim verification type and process the claiming of art rewards. Any excess reward if received is returned to the caller of the claim function.

But, due to change in context of the call, the excess ether will not be returned to the caller, instead it will be retained in the `PhiFactory` contract. The issue arises due to the calling convention used in the `PhiFactory::claim(...)` to trigger the `merkleClaim(...)` and `signatureClaim(...)` external function using `this`.

By doing this, the calling context, that is msg.sender switched from original caller to `PhiFactory`. As a result, any excess ether sent will remain in the `PhiFactory` contract. The protocol owner can withdraw such funds using `withdraw(...)`.

Excess funds should be returned back to the caller.

## Proof of Concept
When the `PhiFactory::claim(...)` is initiated by the caller, the function logic identifies the verification flow calls `merkleClaim(...)` or `signatureClaim(...)` function.

The issue arises in how the `merkleClaim(...)` or `signatureClaim(...)` are triggered. Note the below code snippet where one of the two functions will be triggered.  Here, since the trigger is via `this` and both of these functions are external, the `msg.sender` is switched from original caller to 'PhiFactory` contract instance. Hence, from this point onwards, the msg.sender is 'PhiFactory` contract.

```solidity
    if (art.verificationType.eq("MERKLE")) {
        ...
===>        this.merkleClaim{ value: mintFee }(proof, claimData, mintArgs, leafPart_);
        } else if (art.verificationType.eq("SIGNATURE")) {
        ...
===>        this.signatureClaim{ value: mintFee }(signature_, claimData, mintArgs);
        } 
```
`merkleClaim(...)` or `signatureClaim(...)` function do their specific logic and calls `_processClaim(...)` internal function which returns any excess ether to the `msg.sender`, which in this case would be `PhiFactory` contract. 

Hence, the excess ether received will not be returned to the original caller. The ether will remain in `PhiFactory` contract.

```
   function _processClaim(
        uint256 artId_,
        address minter_,
        address ref_,
        address verifier_,
        uint256 quantity_,
        bytes32 data_,
        string memory imageURI_,
        uint256 etherValue_
    )
        private
    {
       ...
        // Handle refund
        uint256 mintFee = getArtMintFee(a
Missing refund of excess ether in `claim()`
Audit: 2024-08-phi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users will lose their excess ether when calling `claim()` function.

## Proof of Concept
Both [`merkleClaim()`](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L378) and [`signatureClaim()`](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L343) are `payable` function that pass `msg.value` with the call. Within these functions [`_processClaim()`](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L737-L741) is called internally with the passed `msg.value` as `etherValue_`.

The function then `refunds` any `excess ether` as follows:
```solidity
        // Handle refund
        uint256 mintFee = getArtMintFee(artId_, quantity_);
        if ((etherValue_ - mintFee) > 0) {
            _msgSender().safeTransferETH(etherValue_ - mintFee);
        }
```
However, in `claim()`, `excess ether` will not be refunded.
This is because, [`merkleClaim()`](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L282-L283) and [`signatureClaim()`](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L299-L300) are called within it but with already calculated `mintFee`.
```solidity
    uint256 mintFee = getArtMintFee(artId, quantity_);
>>  this.merkleClaim{ value: mintFee }(proof, claimData, mintArgs, leafPart_);
```
```solidity
    uint256 mintFee = getArtMintFee(artId, quantity_);
>>  this.signatureClaim{ value: mintFee }(signature_, claimData, mintArgs);
```
So, the `excess ether` is not refunded as it is not passed to these functions. These functions will receive `exact` amount of ether required meaning that the `refund logic` in `_processClaim()` will not be triggered as there will be nothing to refund.

Also, the `claim()` function itself does not implement this refund logic and as such, users will have to forfeit these funds.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Update `claim()` to refund any excess payment.
```diff
    uint256 mintFee = getArtMintFee(artId, quantity_);
+   // @audit Handle refund
+   if ((msg.value - mintFee) > 0) {
+       _msgSender().safeTransferETH(msg.value - mintFee);
+   }
    this.merkleClaim{ value: mintFee }(proof, claimData, mintArgs, leafPart_);
```
```diff
    uint256 mintFee = getArtMintFee(artId, quantity_);
+   // @audit Handle refund
+   if ((msg.value - mintFee) > 0) {
+       _msgSender().safeTransferETH(msg.value - mintFee);
+   }
    this.signatureClaim{ value: mintFee }(signature_, claimData, mintArgs);
```


## Assessed type

ETH-Transfer
PhiFactory::batchClaim() switches the caller context from original caller to PhiFactory
Audit: 2024-08-phi-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L308-L321
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L264-L304
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L352-L383
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L327-L346
https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L723-L758


# Vulnerability details

## Impact
`PhiFactory::batchClaim(...)` switches the caller context from original caller to `PhiFactory` instance due to how the calling reference is made to `claim(...)` external function via `this` reference.

```
   for (uint256 i = 0; i < encodeDatas_.length; i++) {
===>       this.claim{ value: ethValue_[i] }(encodeDatas_[i]);
   }
```

Due to this convention, the caller of `claim()` function is switch from original msg.sender to `PhiFactory` instance. Any excess Ether sent in the `batchClaim(...)` will be deposited into `PhiFactory` contract itself.

It will not be returned to the original caller.

## Proof of Concept
Due to how the `claim(...)` function is invoked in the `batchClaim(...)`, the caller context is switched from original sender to `PhiFactory` instance.
So, Incase there is any excess ether sent in the `batchClaim(...)` function, such ether will be retained in the `PhiFactory` instance.

```
   function batchClaim(bytes[] calldata encodeDatas_, uint256[] calldata ethValue_) external payable {
        if (encodeDatas_.length != ethValue_.length) revert ArrayLengthMismatch();

        // calc claim fee
        uint256 totalEthValue;
        for (uint256 i = 0; i < encodeDatas_.length; i++) {
            totalEthValue = totalEthValue + ethValue_[i];
        }
        if (msg.value != totalEthValue) revert InvalidEthValue();

        for (uint256 i = 0; i < encodeDatas_.length; i++) {
            this.claim{ value: ethValue_[i] }(encodeDatas_[i]);
        }
    }
```

This happens because in `_processClaim(...)` function, the excess ether is returned to `_msgSender()` which is `PhiFactory` instance.

```
   if ((etherValue_ - mintFee) > 0) {
==>         _msgSender().safeTransferETH(etherValue_ - mintFee);
   }
```

Hence, all the excess ether will be deposited in `PhiFactory` instance and owner can withdraw this ether using the `withdraw()` function. 

```
   function withdraw() external onlyOwner {
        protocolFeeDestination.safeTransferETH(address(this).balance);
   }
```


## Tools Used
Manual review

## Recommended Mitigation Steps
Invoke the `claim(...)` function in the context of the original caller. This is not advise to switch context like this.

When a function needs to be called externally and internally, it is recommended to declare such function as public. 



## Assessed type

ETH-Transfer
`createArt()` has the `whenNotPaused` modifier, but `updateArtSettings()` does not.
Audit: 2024-08-phi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Allowing updates to existing credentials even when the contract is paused could lead to unexpected behavior and potential security risks.

## Proof of Concept
The `PhiFactory` contract contains two key functions: `createArt()` for creating new credentials and `updateArtSettings()` for updating existing ones. These functions have different pause controls, which could lead to inconsistent behavior.

The [`createArt()`](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L204) function includes the `whenNotPaused` modifier, preventing the creation of new credentials when the contract is `paused`.
```solidity
    function createArt(
        ---SNIP---
    )
        external
        payable
        nonReentrant
>>      whenNotPaused
        returns (address)
    {
``` 
However, the [`updateArtSettings()`](https://github.com/code-423n4/2024-08-phi/blob/8c0985f7a10b231f916a51af5d506dd6b0c54120/src/PhiFactory.sol#L215-L257) function lacks this modifier, allowing updates to existing credentials even when the contract is paused. This inconsistency could lead to unexpected behavior and potential security risks.

## Tools Used
Manual Review

## Recommended Mitigation Steps
To ensure consistent behavior and improve security, consider applying the `whenNotPaused` modifier to the `updateArtSettings()` function as well.


## Assessed type

Other
Excess ETH is not refunded when calling `PhiFactory::createArt`
Audit: 2024-08-phi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

When a user calls `PhiFactory::createArt` with more ETH than the `artFee` required, either by mistake or by assuming that any excess ETH would be refunded to his account, this excess ETH will instead be refunded to the `PhiFactory` contract itself.

Although the `PhiFactory` defines a `withdraw` function allowing the owner to withdraw ETH held by this contract ot the `protocolFeeDestination` contract, we still consider this a HIGH severity issue as funds could be misused/misdistributed once in the `protocolFeeDestination` contract.

## Proof of Concept

Let's have a look at the [`PhiFactory::createArt`](https://github.com/code-423n4/2024-08-phi/blob/main/src/PhiFactory.sol#L196-L213) function below

```javascript
    function createArt(
        bytes calldata signedData_,
        bytes calldata signature_,
        CreateConfig memory createConfig_
    )
        external
        payable
        nonReentrant
        whenNotPaused
        returns (address)
    {
        _validateArtCreationSignature(signedData_, signature_);
        (, string memory uri_, bytes memory credData) = abi.decode(signedData_, (uint256, string, bytes));
        ERC1155Data memory erc1155Data = _createERC1155Data(artIdCounter, createConfig_, uri_, credData);
        address artAddress = createERC1155Internal(artIdCounter, erc1155Data);
        artIdCounter++;
        return artAddress;
    }
```

this function in turns calls [`createERC1155Internal`](https://github.com/code-423n4/2024-08-phi/blob/main/src/PhiFactory.sol#L551-L568)

```javascript
    function createERC1155Internal(uint256 newArtId, ERC1155Data memory createData_) internal returns (address) {
        // Todo check if artid exists, if artid exists, reverts
        _validateArtCreation(createData_);
        PhiArt storage currentArt = arts[newArtId];
        _initializePhiArt(currentArt, createData_);

        (uint256 credId,, string memory verificationType, uint256 credChainId,) =
            abi.decode(createData_.credData, (uint256, address, string, uint256, bytes32));

        address artAddress;
        if (credNFTContracts[credChainId][credId] == address(0)) {
            artAddress = _createNewNFTContract(currentArt, newArtId, createData_, credId, credChainId, verificationType);
        } else {
            artAddress = _useExistingNFTContract(currentArt, newArtId, createData_, credId, credChainId);
        }

        return artAddress;
    }
```

let's assume `credNFTContracts[credChainId][credId] == address(0)` for the sake of this proof of concept, we then call the `_createNewNFTContract` function below

```javascript
    function _createNewNFTContract(
        PhiArt storage art,
        uint256 newArtId,
        ERC1155Data memory createData_,
        uint256 credId,
        uint256 credChainId,
        string memory verificationType
    )
        private
        r
When msg.value to be set greater than artFee and call createArt function, it always reverts.
Audit: 2024-08-phi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

# When msg.value to be set greater than artFee and call createArt function, it always reverts.
## Impact
The createArt function is a function that is called when registering a specific art.
When calling this function, you must pay the artFee to register the art.

When msg.value is set to a value greater than artFee, it is always reverted, so art cannot be created for this condition.

## Proof of Concept
https://github.com/code-423n4/2024-08-phi/blob/main/src/PhiFactory.sol#L640-L643
PhiFactory.sol
```
    function _createNewNFTContract(
        PhiArt storage art,
        uint256 newArtId,
        ERC1155Data memory createData_,
        uint256 credId,
        uint256 credChainId,
        string memory verificationType
    )
        private
        returns (address)
    {
        
        ''' skip '''

        (bool success_, bytes memory response) =
            newArt.call{ value: msg.value }(abi.encodeWithSignature("createArtFromFactory(uint256)", newArtId));

        ''' skip '''
    }
```

When the createArt function is called, the _createNewNFTContract function is called, which calls the createArtFromFactory function of the PhiNTF1155.sol contract with msg.value to create art.

https://github.com/code-423n4/2024-08-phi/blob/main/src/art/PhiNFT1155.sol#L138-L156
PhiNFT1155.sol
```
    function createArtFromFactory(uint256 artId_) external payable onlyPhiFactory whenNotPaused returns (uint256) {
        _artIdToTokenId[artId_] = tokenIdCounter;
        _tokenIdToArtId[tokenIdCounter] = artId_;

        uint256 artFee = phiFactoryContract.artCreateFee();

        protocolFeeDestination.safeTransferETH(artFee);
        emit ArtCreated(artId_, tokenIdCounter);
        uint256 createdTokenId = tokenIdCounter;

        unchecked {
            tokenIdCounter += 1;
        }
        if ((msg.value - artFee) > 0) {
            _msgSender().safeTransferETH(msg.value - artFee);
        }

        return createdTokenId;
    }
```

In this function, there is an if statement that creates and registers art and sends the remaining msg.value to msg.sender.

At this time, msg.sender is the address of the PhiFactory contract, not the user.

Since the PhiFactory contract does not have a receive function or a fallback function, the result of the _msgSender().safeTransferETH(msg.value - artFee) code always becomes revert . Even if there is a receive function or a fallback function, the msg.value that should be returned does not go to the user but to the PhiFactory contract, which is also a problem.

### Test Code
https://github.com/code-423n4/2024-08-phi/blob/main/src/PhiFactory.sol#L620
Add console.log to _createNewNFTCon
'claim' function of the PhiFactory contract, if msg.value is greater than mintFee, the remaining ether after subtracting mintFee will not be returned.
Audit: 2024-08-phi-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
If the user calls the 'claim' function of the PhiFactory contract and msg.value is greater than mintFee, the remaining ether minus mintFee will not be returned.

## Proof of Concept
https://github.com/code-423n4/2024-08-phi/blob/main/src/PhiFactory.sol#L264-L304
PhiFactory.sol
claim function
```
    function claim(bytes calldata encodeData_) external payable {
        bytes memory encodedata_ = LibZip.cdDecompress(encodeData_);
        (uint256 artId) = abi.decode(encodedata_, (uint256));
        PhiArt storage art = arts[artId];
        uint256 tokenId = IPhiNFT1155Ownable(art.artAddress).getTokenIdFromFactoryArtId(artId);
        if (art.verificationType.eq("MERKLE")) {
            (
                ,
                address minter_,
                bytes32[] memory proof,
                address ref_,
                uint256 quantity_,
                bytes32 leafPart_,
                string memory imageURI_
            ) = abi.decode(encodedata_, (uint256, address, bytes32[], address, uint256, bytes32, string));

            bytes memory claimData = abi.encode(minter_, ref_, artId);
            MintArgs memory mintArgs = MintArgs({ tokenId: tokenId, quantity: quantity_, imageURI: imageURI_ });
            uint256 mintFee = getArtMintFee(artId, quantity_);
            this.merkleClaim{ value: mintFee }(proof, claimData, mintArgs, leafPart_);
        } else if (art.verificationType.eq("SIGNATURE")) {
            (
                ,
                address minter_,
                address ref_,
                address verifier_,
                uint256 expiresIn_,
                uint256 quantity_,
                bytes32 data_,
                string memory imageURI_,
                bytes memory signature_
            ) = abi.decode(encodedata_, (uint256, address, address, address, uint256, uint256, bytes32, string, bytes));

            bytes memory claimData = abi.encode(expiresIn_, minter_, ref_, verifier_, artId, block.chainid, data_);
            MintArgs memory mintArgs = MintArgs({ tokenId: tokenId, quantity: quantity_, imageURI: imageURI_ });
            uint256 mintFee = getArtMintFee(artId, quantity_);
            this.signatureClaim{ value: mintFee }(signature_, claimData, mintArgs);
        } else {
            revert InvalidVerificationType();
        }
    }
```

When you call the 'claim' function, it calls either merkleClaim or signatureClaim depending on the verificationType.

When calling each claim function, send a value equal to mintFee.


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

exmaple signatureClaim)
```
    function signatureClaim(
        bytes calldata signature_,
        bytes calldata encodeData_,