xanoutas/solidity-security-findings huggingface dataset:
Problems with non-standard compliant ERC20 tokens
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

cmichel


# Vulnerability details

## Vulnerability Details

Some tokens (like USDT) don't correctly implement the EIP20 standard and their `transfer`/`transferFrom` function return `void` instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert.

## Impact

Tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value.

## Recommended Mitigation Steps

We recommend using OpenZeppelin’s `SafeERC20` versions with the `safeTransfer` and `safeTransferFrom` functions that handles the return value check as well as non-standard-compliant tokens.
Check if the token on the deployed network has these issues.
Unchecked return value from ERC20
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

cmichel


# Vulnerability details

## Vulnerability Details

The `ERC20.transfer()` and `ERC20.transferFrom()` functions return a boolean value indicating success. This parameter needs to be checked for success.
According to the standard the return value must be checked for `true`, otherwise the transfer will have failed.

The treasury contract calls `ERC20.transferFrom/transfer` four times without checking for the `success` return value.

## Impact

ERC20-compliant tokens that don't actually perform the transfer and return `false` are still counted as a correct transfer.
Users can "deposit" and increase their balance this way without actually transfering tokens, and then withdraw the treasury.

## Recommended Mitigation Steps

We recommend using OpenZeppelin’s `SafeERC20` versions with the `safeTransfer` and `safeTransferFrom` functions that handles the return value check as well as non-standard-compliant tokens.
User deposits can be turned into sponsors and then be stolen
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

cmichel


# Vulnerability details

## Vulnerability Details

When a user deposits to the treasury they first approve the contract and then call its `deposit` action which performs an `ERC20.transferFrom`.
It's possible for an attacker to frontrun the final `deposit` transaction after the user approval and turn the deposit into a `sponsor` this way, which is basically a free donation to a market outcome (card).
The attacker can create their own market where they are the only participant betting on a card that is guaranteed to win.

Attack:
1. Attacker creates custom market `M` with a guaranteed winning card. (They can time the market close in a way such that no other user can rent the card longer then them.)
2. user A sends a transaction which approve the treasury for a deposit amount.
3. user A sends a `Treasury.deposit(amount)` action instead.
4. An attacker observes the `deposit` from 2 and frontruns it with a sponsor action from their custom market by sending a `M.sponsor(A, amount)` transaction.
5. This increases the payout of the card in the attacker market but takes the funds from the user in the `treasury.sponsor(_sponsorAddress=A, _amount)` treasury call: `erc20.transferFrom(_sponsor=A, address(this), _amount);`
6. The attacker wins, and can withdraw their profit at some point, essentially stealing the user's deposit

## Impact

User deposits can be stolen.

## Recommended Mitigation Steps

The sponsor function should not allow specifying a custom `sponsorAddress`.
The market should pull in the amounts from `msg.sender` using `ERC20.transferFrom(msg.sender, amount)` and then forward this amount to the treasury using a simple `ERC20.transfer(treasury, amount)` followed by the `treasury.sponsor` call that does the remaining book-keeping.
User could deposit for free
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

s1m0


# Vulnerability details

## Impact
[deposit()](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L298) function doesn't check the return value of transferFrom() that means if the erc20 token return false instead of reverting the user could deposit for free.

## Tools Used
Manual analysis

## Recommended Mitigation Steps
Use openzeppelin's SafeERC20 library.
Use SafeERC20/TransferHelper on RCTreasury
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

a_delamo


# Vulnerability details

## Impact

Even though the `uberOwner` controls what ERC20 to use on `RCTreasury`, it is highly recommendable to use TransferHelper or SafeERC20 in order to interact safely with the ERC20.

## Tools Used

Editor
Anyone can affect deposits of any user and turn the owner of the token
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

a_delamo


# Vulnerability details

## Impact

On `RCTreasury`, we have the method `collectRentUser`. This method is public, so anyone can call it using whatever user and whatever timestamp. 
So, calling this method using `user = XXXXX` and `_timeToCollectTo = type(uint256).max)`, would make `isForeclosed[user] = true`. 
```
    function collectRentUser(address _user, uint256 _timeToCollectTo)
        public
        override
        returns (
            uint256 newTimeLastCollectedOnForeclosure
        )
    {
        require(!globalPause, "Global pause is enabled");
        assert(_timeToCollectTo != 0);
        if (user[_user].lastRentCalc < _timeToCollectTo) {
            uint256 rentOwedByUser = rentOwedUser(_user, _timeToCollectTo);

            if (rentOwedByUser > 0 && rentOwedByUser > user[_user].deposit) {
                // The User has run out of deposit already.
                uint256 previousCollectionTime = user[_user].lastRentCalc;

                /*
            timeTheirDepsitLasted = timeSinceLastUpdate * (usersDeposit/rentOwed)
                                  = (now - previousCollectionTime) * (usersDeposit/rentOwed)
            */
                uint256 timeUsersDepositLasts =
                    ((_timeToCollectTo - previousCollectionTime) *
                        uint256(user[_user].deposit)) / rentOwedByUser;
                /*
            Users last collection time = previousCollectionTime + timeTheirDepsitLasted
            */
                rentOwedByUser = uint256(user[_user].deposit);
                newTimeLastCollectedOnForeclosure =
                    previousCollectionTime +
                    timeUsersDepositLasts;
                _increaseMarketBalance(rentOwedByUser, _user);
                user[_user].lastRentCalc = SafeCast.toUint64(
                    newTimeLastCollectedOnForeclosure
                );
                assert(user[_user].deposit == 0);
                isForeclosed[_user] = true;
                emit LogUserForeclosed(_user, true);
            } else {
                // User has enough deposit to pay rent.
                _increaseMarketBalance(rentOwedByUser, _user);
                user[_user].lastRentCalc = SafeCast.toUint64(_timeToCollectTo);
            }
            emit LogAdjustDeposit(_user, rentOwedByUser, false);
        }
    }
```

Now, we can do the same for all the users bidding for a specific token. 
Finally, I can become the owner of the token by just calling `newRental` and using a small price. `newRental` will iterate over all the previous bid and will remove them because there are foreclosed.


## Tools Used
Editor

## Recommended Mitigation Steps

`collectRentUser` should be private and create a new public method with `onlyOrderbook` modifier
Missing Factory-only access check leads to loss of funds
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

0xRajeev


# Vulnerability details

## Impact

The Market sponsor(address _sponsorAddress, uint256 _amount) function is an externally callable function that is specified to be callable only by the Factory contract during market creation, as documented in the Natspec @dev comment: "called by Factory during market creation.” However, there is no access control on the caller address as done in other functions.

Impact: Anyone can call with any _sponsorAddress that has given approval to treasury contract (i.e. all users across all markets who have made deposits) and make them lose that amount towards the market pot. It is not clear if an attacker could selectively do this for markets when they are winning to benefit from such funds sponsored towards the market.

## Proof of Concept

Exploit Scenario: User Alice has deposited 10000 tokens to be used for different prediction markets and thereby has given Treasury contract the required allowance to spend this deposit. Evil Eve calls Market::sponsor(Alice’s address, 10,000) and contributes Alice’s deposit amount towards the market pot liquidity which cannot be won back. Alice loses all her deposit to the market which may possibly benefit Eve.

https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L814-L822

## Tools Used

Manual Analysis

## Recommended Mitigation Steps

Add require(msgSender() == factory.owner(), "Not authorised”); to Market::sponsor().
Unused return value from  erc20.transfer()/ erc20.transferFrom()
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

JMukesh


# Vulnerability details

## Impact

It is usually good to add a require-statement that checks the return value or to use something like safeTransfer; unless one is sure the given token reverts in case of a failure

## Proof of Concept

https://consensys.net/diligence/audits/2021/01/fei-protocol/#unchecked-return-value-for-iweth-transfer-call

In RcTreasury.sol

https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L298

https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L350

## Tools Used

manual review

## Recommended Mitigation Steps
Consider adding a require-statement or using safeTransfer()
erc20 transfer and transferFrom functions
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

pauliax


# Vulnerability details

## Impact
When transfering erc20 tokens, functions transfer and transferFrom are used. These functions return boolean to indicate if the action was successful, however, none of the usages check the returned value:
   erc20.transferFrom(msgSender(), address(this), _amount);
   erc20.transfer(_msgSender, _amount);
This erc20 token may only be set by the admin upon initialization or function setTokenAddress, so we can assume it will be ensured that it behaves as expected, however, I wanted you to be aware of this potential issue and consider using SafeERC20: https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#SafeERC20

## Recommended Mitigation Steps
Check that these functions return true or implement SafeERC20 pattern.
anyone can call function sponsor
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

pauliax


# Vulnerability details

## Impact
This function sponsor should only be called by the factory, however, it does not have any auth checks, so that means anyone can call it with an arbitrary _sponsorAddress address and transfer tokens from them if the allowance is > 0:
    /// @notice ability to add liqudity to the pot without being able to win.
    /// @dev called by Factory during market creation
    /// @param _sponsorAddress the msgSender of createMarket in the Factory
    function sponsor(address _sponsorAddress, uint256 _amount)
        external
        override
    {
        _sponsor(_sponsorAddress, _amount);
    }

## Recommended Mitigation Steps
Check that the sender is a factory contract.
transferFrom result not checked
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

gpersoon


# Vulnerability details

## Impact
The function deposit of SafeERC20.sol relies on the fact that transferFrom will revert if it can't transfer the erc20 tokens.
However, depending on the ERC20 token, this doesn't happen and you have to check the result of transferFrom.
With the wrong ERC20 token the treasury would assume it received the erc20 tokens, while in reality it didn't.

This could especially be a risk if the code is deployed later (or forked) with a different ERC20 contract.

## Proof of Concept
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol
 function deposit(uint256 _amount, address _user)  public  override  balancedBooks returns (bool) {
 ....
    erc20.transferFrom(msgSender(), address(this), _amount);
...


## Tools Used

## Recommended Mitigation Steps
Check the result of transferFrom or use SafeERC20
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol
Can access cards of other markets
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

gpersoon


# Vulnerability details

## Impact
Within RCMarket.sol the functions ownerOf and onlyTokenOwner do not check if the _cardId/_token is smaller than numberOfCards.
So it's possible to supply a larger number and access cards of other markets.
The most problematic seems to be upgradeCard. Here the check for isMarketApproved can be circumvented by trying to move the card via another market.

You can still only move cards you own.

## Proof of Concept
// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L338
    function ownerOf(uint256 _cardId) public view override returns (address) {
        uint256 _tokenId = _cardId + totalNftMintCount; // doesn't check if _cardId < numberOfCards
        return nfthub.ownerOf(_tokenId);
    }

https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L313
  modifier onlyTokenOwner(uint256 _token) {
        require(msgSender() == ownerOf(_token), "Not owner"); // _token could be higher than numberOfCards,
        _;
    }

  function upgradeCard(uint256 _card) external onlyTokenOwner(_card) {   // _card  could be higher than numberOfCards,
        _checkState(States.WITHDRAW);
        require(
            !factory.trapIfUnapproved() ||
                factory.isMarketApproved(address(this)),   // this can be circumvented by calling the function via another market
            "Upgrade blocked"
        );
        uint256 _tokenId = _card + totalNftMintCount;    // _card  could be higher than numberOfCards, thus accessing a card in another market
        _transferCard(ownerOf(_card), address(this), _card); // contract becomes final resting place
        nfthub.withdrawWithMetadata(_tokenId);
        emit LogNftUpgraded(_card, _tokenId);
    }
 

## Tools Used

## Recommended Mitigation Steps
Add the following to ownerOf:
require(_card < numberOfCards, "Card does not exist");
Unchecked ERC20 transfers can cause lock up
Audit: 2021-06-realitycards-findings
Severity: HIGH

# Handle

axic


# Vulnerability details

## Impact

Some major tokens went live before ERC20 was finalised, resulting in a discrepancy whether the transfer functions a) should return a boolean or b) revert/fail on error. The current best practice is that they should revert, but return “true” on success. However, not every token claiming ERC20-compatibility is doing this — some only return true/false; some revert, but do not return anything on success. This is a well known issue, heavily discussed since mid-2018.

Today many tools, including OpenZeppelin, offer a wrapper for “safe ERC20 transfer”: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol

RealityCards is not using such a wrapper, but instead tries to ensure successful transfers via the `balancedBooks` modifier:

```
    modifier balancedBooks {
        _;
        // using >= not == in case anyone sends tokens direct to contract
        require(
            erc20.balanceOf(address(this)) >=
                totalDeposits + marketBalance + totalMarketPots,
            "Books are unbalanced!"
        );
    }
```

This modifier is present on most functions, but is missing on `topupMarketBalance`:
```
    function topupMarketBalance(uint256 _amount) external override {
        erc20.transferFrom(msgSender(), address(this), _amount);
        if (_amount > marketBalanceDiscrepancy) {
            marketBalanceDiscrepancy = 0;
        } else {
            marketBalanceDiscrepancy -= _amount;
        }
        marketBalance += _amount;
    }
```

In the case an ERC20 token which is not reverting on failures is used, a malicious actor could call `topupMarketBalance` with a failing transfer, but also move the value of `marketBalance` above the actual holdings. After this, `deposit`, `withdrawDeposit`, `payRent`, `payout`, `sponsor`, etc. could be locked up and always failing with “Books are unbalanced”.

## Proof of Concept

Anyone can call `topupMarketBalance` with some unrealistically large number, so that `marketBalance` does not overflow, but is above the actually helping balances. This is only possible if the underlying ERC20 used is not reverting on failures, but return “false” instead.

## Tools Used

Manual review

## Recommended Mitigation Steps

1. Use something like OpenZeppelin’s SafeERC20
2. Set up an allow list for tokens, which are knowingly safe
3. Consider a different approach to the `balancedBooks` modifier
Function `foreclosureTimeUser` returns a shorter user's foreclosure time than expected
Audit: 2021-06-realitycards-findings
Severity: MEDIUM

# Handle

shw


# Vulnerability details

## Impact

The function `foreclosureTimeUser` of `RCTreasury` underestimates the user's foreclosure time if the current time is not the user's last rent calculation time. The underestimation of the foreclosure time could cause wrong results when determining the new owner of the card.

## Proof of Concept

The variable `timeLeftOfDeposit` at line 668 is calculated based on `depositAbleToWithdraw(_user)`, the user's deposit minus the rent from the last rent calculation to the current time. Thus, the variable `timeLeftOfDeposit` indicates the time left of deposit, starting from now. However, at line 672, the `foreclosureTimeWithoutNewCard` is calculated by `timeLeftOfDeposit` plus the user's last rent calculation time instead of the current time. As a result, the user's foreclosure time is reduced. From another perspective, the rent between the last rent calculation time and the current time is counted twice.

Referenced code:
[RCTreasury.sol#L642-L653](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L642-L653)
[RCTreasury.sol#L669](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L669)
[RCTreasury.sol#L672](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L672)
[RCTreasury.sol#L678](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L678)
[RCOrderbook.sol#L553-L557](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCOrderbook.sol#L553-L557)

## Recommended Mitigation Steps

Change `depositAbleToWithdraw(_user)` at line 669 to `user[_user].deposit`. Or, change `user[_user].lastRentCalc` at both line 672 and 678 to `block.timestamp`.
Market opening time can be before the locking time
Audit: 2021-06-realitycards-findings
Severity: MEDIUM

# Handle

shw


# Vulnerability details

## Impact

In the function `createMarket` of `RCFactory`, the market opening time is not checked before the market locking time, and thus markets can be created with invalid time configurations accidentally. Besides, if the `advancedWarning` is non-zero, there are two checks applied, and the first seems unnecessary (which is a requirement of the second one).

## Proof of Concept

Referenced code:
[RCFactory.sol#L520-L527](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCFactory.sol#L520-L527)

## Recommended Mitigation Steps

Check that the market opening time is before both the market locking time. In cases where `advancedWarning` is non-zero, if the purpose of the first check is to prevent subtraction overflow on the second check, consider removing the first one and re-writing the second one as `_timestamps[0] > block.timestamp + advancedWarning`.
`RCNftHubL2.safeTransferFrom` not accoring to spec
Audit: 2021-06-realitycards-findings
Severity: MEDIUM

# Handle

cmichel


# Vulnerability details

## Vulnerability Details

The `RCNftHubL2.safeTransferFrom` function does not correctly implement the ERC721 spec:

> When using safeTransferFrom, the token contract checks to see that the receiver is an IERC721Receiver, which implies that it knows how to handle ERC721 tokens. [ERC721](https://docs.openzeppelin.com/contracts/2.x/api/token/erc721#IERC721-safeTransferFrom)

This check is not implemented, it just drops the `_data` argument.

## Impact

Contracts that don't know how to handle ERC721 tokens (are not an `IERC721Receiver`) can accept them but they should not when using `safeTransferFrom` according to spec.

## Recommended Mitigation Steps

Implement the `IERC721Receiver` check in `safeTransferFrom`.
Pot distribution does not need to add up to 100%
Audit: 2021-06-realitycards-findings
Severity: MEDIUM

# Handle

cmichel


# Vulnerability details

## Vulnerability Details

The `Factory.setPotDistribution` allows specifying values that add up to less than 100% because of the inequality `<= 1000` instead of an equality `== 1000`.

## Impact

If using less than 100%, funds could become stuck in the market for certain modes.

## Recommended Mitigation Steps

Ensure it adds up to 100%.
Deposits can be denied by abusing `maxContractBalance`
Audit: 2021-06-realitycards-findings
Severity: MEDIUM

# Handle

cmichel


# Vulnerability details

## Vulnerability Details

The treasury implements a max contract balance check in the `deposit` function:

```solidity
require(
    (erc20.balanceOf(address(this)) + _amount) <= maxContractBalance,
    "Limit hit"
);
```

A whale can stop anyone from depositing by front-running a user's deposit with a deposit that pushes the contract balance to the `maxContractBalance` limit first.
The user's deposit will then fail in the check.
Afterwards, the whale can withdraw again.

> This is not only restricted to whales, miners/users can do the same using same-block cross-transaction flashloans and submitting a `(attacker deposit, user deposit, attacker withdraw)` flashbundle to a miner. Possibilities like this will only become more prevalent in the future.

## Impact

Any users can be blocked from depositing which prevents them from renting cards.
This allows an attacker to manipulate the outcome of a market in their favor by strategically preventing other competitors to bid on their cards (causing forfeiture due to a low deposit balance).

## Recommended Mitigation Steps

Remove the contract limit or at least set the limit very high if it keeps happening.
Deposits don't work with fee-on transfer tokens
Audit: 2021-06-realitycards-findings
Severity: MEDIUM

# Handle

cmichel


# Vulnerability details

## Vulnerability Details

There are ERC20 tokens that may make certain customizations to their ERC20 contracts.
One type of these tokens is deflationary tokens that charge a certain fee for every `transfer()` or `transferFrom()`.


## Impact

The `deposit()` function will introduce unexpected balance inconsistencies when comparing internal asset records with external ERC20 token contracts.

## Recommended Mitigation Steps

 One possible mitigation is to measure the asset change right before and after the asset-transferring routines
Card affiliate payouts are skipped if a single card does not have an affiliate
Audit: 2021-06-realitycards-findings
Severity: MEDIUM

# Handle

cmichel


# Vulnerability details

## Vulnerability Details

The `Market.initialize` function sets the `cardAffiliateCut` to zero if a single `_cardAffiliateAddresses` is the zero address.

```solidity
for (uint256 i = 0; i < _numberOfCards; i++) {
    if (_cardAffiliateAddresses[i] == address(0)) {
        cardAffiliateCut = 0;
    }
}
```

## Impact

Even if all other cards have a valid affiliate, no affiliate cuts are paid out to these.

## Recommended Mitigation Steps

Distribute the specified `cardAffiliateCut` equally among all non-zero card affiliates.