xanoutas/solidity-security-findings huggingface dataset:
Broken Upgradable Logic in Pool.sol
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Pool.sol#L5
https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Pool.sol#L15


# Vulnerability details

## Impact
The `Pool` smart contract allows a user to predeposit ETH so that it can be used when a seller takes their bid. It uses an `ERC1967` proxy pattern and only the exchange contract is permitted to make transfers.
The smart contract inherits the [`OwnableUpgradeable.sol` smart contract](https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Pool.sol#L5)

The smart contract inherits the `OwnableUpgradeable.sol` but never initialize it, with a call to the internal `__Ownable_init();` function. Due to the fact that the upgradable smart contract initial logic cannot be executed in the constructor, this efficiently leaves the smart contract without an owner, and so, no-one can upgrade it(https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Pool.sol#L15)

## Tools Used
Manual Code Review

## Recommended Mitigation Steps
It is recommended to create an `initialize()` function, similar to the one that the [`Exchange.sol`](https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L112) ,  that makes a call to the `__Ownable_init();` internal function. This will make the `msg.sender` as the original owner, that is required in order to upgrade the smart contract.
Admin can drain user funds from the Pool or buy assets for free
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-non-fungible/blob/main/contracts/PolicyManager.sol#L25-L29
https://github.com/code-423n4/2022-11-non-fungible/blob/main/contracts/Exchange.sol#L251
https://github.com/code-423n4/2022-11-non-fungible/blob/main/contracts/Exchange.sol#L565-L581
https://github.com/code-423n4/2022-11-non-fungible/blob/main/contracts/Pool.sol#L70-L74


# Vulnerability details

## Impact
We assume that the admin is honest, however there is still possibility of exploiting asset policy contract to and set price to 0 in oder to buy an asset for free - or even worse - drain user funds by setting the price really high in StandardPolicyERC721::canMatchMakerAsk, thus draining the user balance fron the Pool.

## Proof of Concept
Steps to execute:
0. (prerequisite) - change asset matching policy to maliciously set high price and manipulate asset amount, id and type

```diff
index 2e24cce..1eb7b98 100644
--- a/contracts/matchingPolicies/StandardPolicyERC721.sol
+++ b/contracts/matchingPolicies/StandardPolicyERC721.sol
@@ -30,7 +30,7 @@ contract StandardPolicyERC721 is IMatchingPolicy {
             (takerBid.amount == 1) &&
             (makerAsk.matchingPolicy == takerBid.matchingPolicy) &&
             (makerAsk.price == takerBid.price),
-            makerAsk.price,
+            1 ether,
             makerAsk.tokenId,
             1,
             AssetType.ERC721
@@ -58,16 +58,12 @@ contract StandardPolicyERC721 is IMatchingPolicy {
             (takerAsk.amount == 1) &&
             (makerBid.matchingPolicy == takerAsk.matchingPolicy) &&
             (makerBid.price == takerAsk.price),
-            makerBid.price,
+            1 ether,
             makerBid.tokenId,
             1,
             AssetType.ERC721
         );
```
1. create an asset order with really compelling price as a honeypot, and set payment token to the Pool contract address
2. When a victim accepts an offer, take signed order params (frontrun the transaction) and invoke Exchange::execute()
3. Actual price to pay is returned from malicious policy https://github.com/code-423n4/2022-11-non-fungible/blob/main/contracts/Exchange.sol#L251 . It's already set to arbitrarily high amount
4. Exchange::_executeFundsTransfer() is called with the price read from the Policy contract. No additional checks for validity are made, so Exchange::_transferTo https://github.com/code-423n4/2022-11-non-fungible/blob/main/contracts/Exchange.sol#L581 , and later Pool:transferFrom https://github.com/code-423n4/2022-11-non-fungible/blob/main/contracts/Pool.sol#L70-L74 is called, which transfers funds to the attacker

## Tools Used
VSCode

## Recommended Mitigation Steps
Mitigation steps:
The best option is to take the price from buy and sell input, under condition that they do match, as it is not succeptible to this attack. In case that this is not possible, I see two possible options:
1. Introduce grace period before new policy will be active, to at least give users some time to
Trader can still execute the order even after cancelling the order
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/constantlearner2000/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L253-L255


# Vulnerability details

## Impact
There are  no checks to verify whether the order has been cancelled or not in _execute function. This will enable traders to place the order even after cancellation

## Proof of Concept
Execute cancelOrder function with Order data
Include the Order in  input of execute function


## Tools Used
Manual check
## Recommended Mitigation Steps
Verify for cancelled orders before following lines
        /* Mark orders as filled. */
        cancelledOrFilled[sellHash] = true;
        cancelledOrFilled[buyHash] = true;
Signature Malleability in case of using EVM built-in ecrecover()
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

 https://github.com/code-423n4/2022-11-non-fungible/blob/main/contracts/Exchange.sol#L524


# Vulnerability details

## Impact
The ecrecover() function which is used verify a signature. The built-in EVM precompile ecrecover
 is susceptible to signature malleability (because of non-unique s and v values) which could lead to replay attacks.

## Proof of Concept
 https://github.com/code-423n4/2022-11-non-fungible/blob/main/contracts/Exchange.sol#L524

## Tools Used
Manual Analysis

## Recommended Mitigation Steps
Use OpenZeppelin's ECDSA library for address recovery
On `_returnDust`, `call` opcode's return value not checked
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L212-L227


# Vulnerability details

## Impact
The call opcode's return value not checked, which could leads to the originator lose funds.


## Proof of Concept
The caller of Exchange.sol::execute or Exchange.sol::bulkExecute could be a contract who may not implement the fallback or receive function, when a call to it with value sent, it will revert, thus failed to receive the ETH.


Lets imagine that we call `execute` or `bulkExecute` and there is some remaining ETH, the contract orginator doesn't implement the fallback/receive function and the call opcode's return value not checked, thus the ETH value will be trapped in the Exchange contract. The originator lose funds.

## Tools Used
manual revision

## Recommended Mitigation Steps
Check return value

```diff
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol
index ec27b1d..7e56de0 100644
--- a/contracts/Exchange.sol
+++ b/contracts/Exchange.sol
@@ -210,10 +210,11 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U
     }
 
     function _returnDust() private {
+        bool callStatus;
         uint256 _remainingETH = remainingETH;
         assembly {
             if gt(_remainingETH, 0) {
-                let callStatus := call(
+                callStatus := call(
                     gas(),
                     caller(),
                     selfbalance(),
@@ -224,6 +225,7 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U
                 )
             }
         }
+        require(callStatus, "Fail ETH transfer");
     }
 
     /**
```
Tokens transfers do not verify that the tokens were successfully transferred
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L639


# Vulnerability details

## Impact
Some tokens (like [`zrx`](https://etherscan.io/token/0xe41d2489571d322189246dafa5ebde1f4699f498#code)) do not revert the transaction when the transfer/transferfrom fails and return false instead, which requires us to check the return value after calling the `transfer`/`transferfrom` function. Even though, the exchange seems to only support WETH for the moment, which reverts on a failed transaction, this behavior could change in the future.


## Proof of Concept
The `_transferTo` function:
```
function _transferTo(
        address paymentToken,
        address from,
        address to,
        uint256 amount
    ) internal {
        if (amount == 0) {
            return;
        }

        if (paymentToken == address(0)) {
            /* Transfer funds in ETH. */
            require(to != address(0), "Transfer to zero address");
            (bool success,) = payable(to).call{value: amount}("");
            require(success, "ETH transfer failed");
        } else if (paymentToken == POOL) {
            /* Transfer Pool funds. */
            bool success = IPool(POOL).transferFrom(from, to, amount);
            require(success, "Pool transfer failed");
        } else if (paymentToken == WETH) {
            /* Transfer funds in WETH. */
            executionDelegate.transferERC20(WETH, from, to, amount);
        } else {
            revert("Invalid payment token");
        }
    }
```
The `transferERC20` function:
```
   function transferERC20(address token, address from, address to, uint256 amount)
        approvedContract
        external
    {
        require(revokedApproval[from] == false, "User has revoked approval");
        bytes memory data = abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount);
        bytes memory returndata = token.functionCall(data);
        if (returndata.length > 0) {
          require(abi.decode(returndata, (bool)), "ERC20 transfer failed");
        }
    }
```

## Tools Used
Manual Code Review

## Recommended Mitigation Steps
It is recommended to use OZ [`SafeERC20.sol`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/b2970b96e5e2be297421cd7690e3502e49f7deff/contracts/token/ERC20/utils/SafeERC20.sol#L30) which checks the return value of the transfer.
Pool designed to be upgradeable but does not set owner, making it unupgradeable
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Pool.sol#L13


# Vulnerability details

## Description

The docs state:
"*The pool allows user to predeposit ETH so that it can be used when a seller takes their bid. It uses an ERC1967 proxy pattern and only the exchange contract is permitted to make transfers.*"

Pool is designed as an ERC1967 upgradeable proxy which handles balances of users in Not Fungible. Users may interact via deposit and withdraw with the pool, and use the funds in it to pay for orders in the Exchange.

Pool is declared like so:
```
contract Pool is IPool, OwnableUpgradeable, UUPSUpgradeable {
	function _authorizeUpgrade(address) internal override onlyOwner {}
	...
```

Importantly, it has no constructor and no initializers. The issue is that when using upgradeable contracts, it is important to implement an initializer which will call the base contract's initializers in turn. See how this is done correctly in Exchange.sol:

```
/* Constructor (for ERC1967) */
function initialize(
    IExecutionDelegate _executionDelegate,
    IPolicyManager _policyManager,
    address _oracle,
    uint _blockRange
) external initializer {
    __Ownable_init();
    isOpen = 1;
	  ...
}
```

Since Pool skips the \_\_Ownable_init initialization call, this logic is skipped:
```
function __Ownable_init() internal onlyInitializing {
    __Ownable_init_unchained();
}
function __Ownable_init_unchained() internal onlyInitializing {
    _transferOwnership(_msgSender());
}
```

Therefore, the contract owner stays zero initialized, and this means any use of onlyOwner will always revert.

The only use of onlyOwner in Pool is here:
```
function _authorizeUpgrade(address) internal override onlyOwner {}
```

The impact is that when the upgrade mechanism will check caller is authorized, it will revert. Therefore, the contract is unexpectedly unupgradeable. Whenever the EXCHANGE or SWAP address, or some functionality needs to be changed, it would not be possible.

## Impact

The Pool contract is designed to be upgradeable but is actually not upgradeable

## Proof of Concept

In the 'pool' test in execution.test.ts, add the following lines:
```
it('owner configured correctly', async () => {
  expect(await pool.owner()).to.be.equal(admin.address);
});
```

It shows that the pool after deployment has owner as 0x0000...00

## Tools Used

Manual audit, hardhat

## Recommended Mitigation Steps

Implement an initializer for Pool similarly to the Exchange.sol contract.
Potential ETH leak of value when execute() is called from a contract 
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L216


# Vulnerability details

## Description

\_returnDust function is used to return unused ETH from the trade executions back to the sender. It is implemented like so:
```
function _returnDust() private {
    uint256 _remainingETH = remainingETH;
    assembly {
        if gt(_remainingETH, 0) {
            let callStatus := call(
                gas(),
                caller(),
                selfbalance(),
                0,
                0,
                0,
                0
            )
        }
    }
}
```

remainingETH keeps track of how many of user's ETH remain after trade executions. The issue is that the caller of execute() or bulkExecute() may be a contract that does not accept no-calldata calls (does not have a fallback function). In that event, the call() will fail and return 0 to the callStatus variable. However, since it is never checked, \_returnDust will complete successfully and therefore leak the remaining ETH value.

## Impact

ETH is leaked when execute() is called from a contract that doesn't have a fallback function.

## Tools Used

Manual audit

## Recommended Mitigation Steps

It is recommended to require that callStatus is true.
It makes sense to add a flag dontForceRefund, which will override the default revert behavior.
listingTime can be abitrary number less than block.timestamp
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L537-L555


# Vulnerability details

## Proof of Concept

`_canMatchOrders` branch on condition `sell.listingTime <= buy.listingTime`. However, at least one of this `listingTime` is set by user. In case of `canMatchMakerAsk` and `canMatchMakerBid` behave differently, malicious user can exploit this to make a better trade. Since this issue is based on assumption that `MatchingPolicy` is implemented incorrectly. I believe this is a medium risk issue.

https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L537-L555

```solidity
    function _canMatchOrders(Order calldata sell, Order calldata buy)
        internal
        view
        returns (uint256 price, uint256 tokenId, uint256 amount, AssetType assetType)
    {
        bool canMatch;
        if (sell.listingTime <= buy.listingTime) {
            /* Seller is maker. */
            require(policyManager.isPolicyWhitelisted(sell.matchingPolicy), "Policy is not whitelisted");
            (canMatch, price, tokenId, amount, assetType) = IMatchingPolicy(sell.matchingPolicy).canMatchMakerAsk(sell, buy);
        } else {
            /* Buyer is maker. */
            require(policyManager.isPolicyWhitelisted(buy.matchingPolicy), "Policy is not whitelisted");
            (canMatch, price, tokenId, amount, assetType) = IMatchingPolicy(buy.matchingPolicy).canMatchMakerBid(buy, sell);
        }
        require(canMatch, "Orders cannot be matched");

        return (price, tokenId, amount, assetType);
    }

```

## Recommended Mitigation Steps

If the fact that who is maker or taker makes no difference on how orders are matched (like in StandardPolicyERC721). We should no split `canMatch` logic into two.
All orders which use expirationTime == 0 to support oracle cancellation are not executable.
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L378


# Vulnerability details

## Description

The Non Fungible supplied docs state:
**Off-chain methods**
"Oracle cancellations - if the order is signed with an expirationTime of 0, a user can request an oracle to stop producing authorization signatures; without a recent signature, the order will not be able to be matched"

From the docs, we can expect that when expirationTime is 0 , trader wishes to enable dynamic oracle cancellations. However, the recent code refactoring broke not only this functionality but all orders with expirationTime = 0.

Previous \_validateOrderParameters:
```
function _validateOrderParameters(Order calldata order, bytes32 orderHash)
    internal
    view
    returns (bool)
{
    return (
        /* Order must have a trader. */
        (order.trader != address(0)) &&
        /* Order must not be cancelled or filled. */
        (cancelledOrFilled[orderHash] == false) &&
        /* Order must be settleable. */
        _canSettleOrder(order.listingTime, order.expirationTime)
    );
}
/**
 * @dev Check if the order can be settled at the current timestamp
 * @param listingTime order listing time
 * @param expirationTime order expiration time
 */
function _canSettleOrder(uint256 listingTime, uint256 expirationTime)
    view
    internal
    returns (bool)
{
    return (listingTime < block.timestamp) && (expirationTime == 0 || block.timestamp < expirationTime);
```

New \_validateOrderParameters:
```
function _validateOrderParameters(Order calldata order, bytes32 orderHash)
    internal
    view
    returns (bool)
{
    return (
        /* Order must have a trader. */
        (order.trader != address(0)) &&
        /* Order must not be cancelled or filled. */
        (!cancelledOrFilled[orderHash]) &&
        /* Order must be settleable. */
        (order.listingTime < block.timestamp) &&
        (block.timestamp < order.expirationTime)
    );
}
```

Note the requirements on expirationTime in \_canSettleOrder (old) and \_validateOrderParameters (new).
If expirationTime == 0, the condition was satisfied without looking at `block.timestamp < expirationTime`.
In the new code, `block.timestamp < expirationTime` is *always* required in order for the order to be valid. Clearly, block.timestamp < 0 will always be false, so all orders that wish to make use of off-chain cancellation will never execute.

## Impact

All orders which use expirationTime == 0 to support oracle cancellation are not executable.

## Tools Used

Manual audit, diffing tool

## Recommended Mitigation Steps

Implement the checks the same way as they were in the previous version of Exchange.
Hacked owner or malicious owner can immediately steal all assets on the platform
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L639
https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L30


# Vulnerability details

## Description

In Non-Fungible's security model, users approve their ERC20 / ERC721 / ERC1155 tokens to the ExecutionDelegate contract, which accepts transfer requests from Exchange. 

The requests are made here:
```
function _transferTo(
    address paymentToken,
    address from,
    address to,
    uint256 amount
) internal {
    if (amount == 0) {
        return;
    }
    if (paymentToken == address(0)) {
        /* Transfer funds in ETH. */
        require(to != address(0), "Transfer to zero address");
        (bool success,) = payable(to).call{value: amount}("");
        require(success, "ETH transfer failed");
    } else if (paymentToken == POOL) {
        /* Transfer Pool funds. */
        bool success = IPool(POOL).transferFrom(from, to, amount);
        require(success, "Pool transfer failed");
    } else if (paymentToken == WETH) {
        /* Transfer funds in WETH. */
        executionDelegate.transferERC20(WETH, from, to, amount);
    } else {
        revert("Invalid payment token");
    }
}
```

```
function _executeTokenTransfer(
    address collection,
    address from,
    address to,
    uint256 tokenId,
    uint256 amount,
    AssetType assetType
) internal {
    /* Call execution delegate. */
    if (assetType == AssetType.ERC721) {
        executionDelegate.transferERC721(collection, from, to, tokenId);
    } else if (assetType == AssetType.ERC1155) {
        executionDelegate.transferERC1155(collection, from, to, tokenId, amount);
    }
}
```

The issue is that there is a significant centralization risk trusting Exchange.sol contract to behave well, because it is an immediately upgradeable ERC1967Proxy. All it takes for a malicious owner or hacked owner to upgrade to the following contract:

```
function _stealTokens(
    address token,
    address from,
    address to,
    uint256 tokenId,
    uint256 amount,
    AssetType assetType
) external onlyOwner {
    /* Call execution delegate. */
    if (assetType == AssetType.ERC721) {
        executionDelegate.transferERC721(token, from, to, tokenId);
    } else if (assetType == AssetType.ERC1155) {
        executionDelegate.transferERC1155(token, from, to, tokenId, amount);
    } else if (assetType == AssetType.ERC20) {
        executionDelegate.transferERC20(token, from, to, amount);
}
```

At this point hacker or owner can steal all the assets approved to Non-Fungible. 

## Impact

Hacked owner or malicious owner can immediately steal all assets on the platform

## Tools Used

Manual audit

## Recommended Mitigation Steps

Exchange contract proxy should implement a timelock, to give users enough time to withdraw their approvals before some malicious action becomes possible.

## Judging
The transferFrom() method is used instead of safeTransferFrom()
Audit: 2022-11-non-fungible-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L635
https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/ExecutionDelegate.sol#L80


# Vulnerability details

# Vulnerability details

Details & Impact
The transferFrom() method is used instead of safeTransferFrom(), presumably to save gas. I however argue that this isn’t recommended because:

1)OpenZeppelin’s documentation discourages the use of transferFrom(), use safeTransferFrom() whenever possible
2)Given that any NFT can be used for the call option, there are a few NFTs (here’s an example) that have logic in the onERC721Received() function, which is only triggered in the safeTransferFrom() function and not in transferFrom()

Recommended Mitigation Steps
Call the safeTransferFrom() method instead of transferFrom()

File: 2022-11-non-fungible\contracts\Exchange.sol
  635,40:             bool success = IPool(POOL).transferFrom(from, to, amount);
  
File: 2022-11-non-fungible\contracts\ExecutionDelegate.sol
  
  80,29:         IERC721(collection).transferFrom(from, to, tokenId);