GithubHelp home page GithubHelp logo

2021-06-realitycards-findings's People

Contributors

c4-staff avatar code423n4 avatar joshuashort avatar ninek9 avatar

Watchers

 avatar  avatar  avatar

2021-06-realitycards-findings's Issues

RCFactory.setReferenceContractAddress() improperly uses assert() rather than require()

Handle

jvaqa

Vulnerability details

RCFactory.setReferenceContractAddress() improperly uses assert() rather than require()

Impact

According to Solidity documentation for v0.8.0:

"Properly functioning code should never [trigger an assert], not even on invalid external input. If this happens, then there is a bug in your contract which you should fix."[1]
[1] https://docs.soliditylang.org/en/v0.8.0/control-structures.html#error-handling-assert-require-revert-and-exceptions

The reason that asserts should never be triggered in bug-free code is that triggering an assert() causes the transaction to use up more space on the blockchain than is necessary, since the transaction uses all gas provided by the user in the transaction's gasLimit, whereas triggering a require() only uses up the gas used up to the point of the failed require() statement. Using up more gas than necessary hurts the public good of blockspace, by unnecessarily filling up the block with more gas than is strictly necessary for the failed transaction to use. It is useful in development environments to distinguish between allowable reverts and unallowable reverts in automated testing, but asserts should not be reached in live code.

Proof of Concept

The uberOwner can call RCFactory.setReferenceContractAddress(address(0)) to trigger an assert

Recommended Mitigation Steps

Change:

assert(newContractVariable.isMarket());

To:

require(newContractVariable.isMarket());

RCFactory.changeMarketApproval() improperly uses assert() rather than require()

Handle

jvaqa

Vulnerability details

RCFactory.changeMarketApproval() improperly uses assert() rather than require()

Impact

According to Solidity documentation for v0.8.0:

"Properly functioning code should never [trigger an assert], not even on invalid external input. If this happens, then there is a bug in your contract which you should fix."[1]
[1] https://docs.soliditylang.org/en/v0.8.0/control-structures.html#error-handling-assert-require-revert-and-exceptions

The reason that asserts should never be triggered in bug-free code is that triggering an assert() causes the transaction to use up more space on the blockchain than is necessary, since the transaction uses all gas provided by the user in the transaction's gasLimit, whereas triggering a require() only uses up the gas used up to the point of the failed require() statement. Using up more gas than necessary hurts the public good of blockspace, by unnecessarily filling up the block with more gas than is strictly necessary for the failed transaction to use. It is useful in development environments to distinguish between allowable reverts and unallowable reverts in automated testing, but asserts should not be reached in live code.

Proof of Concept

Any governor can call RCFactory.changeMarketApproval(address(0)) to trigger an assert

Recommended Mitigation Steps

Change:

assert(_marketToApprove.isMarket());

To:

require(_marketToApprove.isMarket());

exit can be done within 1 minute

Handle

gpersoon

Vulnerability details

Impact

Rentals of cards should be at least 1 minute.
The function withdrawDeposit has code to enforce this, however the function exit of RCMarket.sol also allow you to stop your ownership and doesn't seem to have a way to enforce the 1 minute requirement.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L784
function exit(uint256 _card) public override {
_checkState(States.OPEN);
address _msgSender = msgSender();
exitedTimestamp[_msgSender] = block.timestamp;
_collectRent(_card);
if (ownerOf(_card) == _msgSender) {
orderbook.findNewOwner(_card, block.timestamp);
assert(!orderbook.bidExists(_msgSender, address(this), _card));
} else {
if (orderbook.bidExists(_msgSender, address(this), _card)) {
orderbook.removeBidFromOrderbook(_msgSender, _card);
}
}
}

Tools Used

Recommended Mitigation Steps

Enforce the 1 minute requirement in the exit function

anyone can call function sponsor

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.

extra security for changeUberOwner

Handle

gpersoon

Vulnerability details

Impact

The function changeUberOwner of RCOrderbook.sol allow you to change the UberOwner.
If you accidentally make a mistake with the _newUberOwner parameter, you can shoot yourself in the foot and are never able to update the UberOwner again.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCOrderbook.sol#L117
function changeUberOwner(address _newUberOwner) external override {
require(msgSender() == uberOwner, "Extremely Verboten");
require(_newUberOwner != address(0));
uberOwner = _newUberOwner;
}

Tools Used

Recommended Mitigation Steps

Consider using a solution to set an UberOwner candidate, where the new UberOwner should claim ownership, while proving it exits.

For example with the following code:
address uberOwnercandidate;
function setUberOwnercandidate(address _newUberOwner) external {
require(msg.sender == uberOwner, "Extremely Verboten");
require(_newUberOwner != address(0));
uberOwnercandidate = _newUberOwner;
}

function confirmNewUberOwner() external  {
    require(msg.sender == uberOwnercandidate, "Extremely Verboten");
    uberOwner = uberOwnercandidate;
}

costly-loop

Handle

heiho1

Vulnerability details

Impact

RCMarket#initialize(uint256,uint32[],uint256,uint256,address,address,address[],address,string) has a potentially expensive loop that modifies state continually over an indeterminate number of cards.

Proof of Concept

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

Tools Used

Slither

Recommended Mitigation Steps

Potentially a gas-expensive loop because of arbitrary length of _cardAffiliateAddresses possibly assigning to state variable cardAffiliateCut multiple times.

test

Handle

0xRajeev

Vulnerability details

Impact

Detailed description of the impact of this finding.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Recommended Mitigation Steps

function _incrementState checks the boundaries before incrementing it

Handle

pauliax

Vulnerability details

Impact

_incrementState checks state boundaries before the increment, so in theory, it is possible to increment it to an invalid state (number 4). In my opinion, it should assert that after setting the new state. In practice, _incrementState is never called when the state is in WITHDRAW thus it cannot be exploited with the current code but it still needs to assume such a scenario in case you decide to change the code in the future.

Recommended Mitigation Steps

Move to the bottom of the _incrementState:
assert(uint256(state) < 4);

functions safeTransferFrom and transferFrom are too similar

Handle

pauliax

Vulnerability details

Impact

function safeTransferFrom is almost identical to function transferFrom. It would be better to reduce code duplication by re-using the code.

Recommended Mitigation Steps

function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory _data
) public override {
transferFrom(from, to, tokenId);
_data;
}

Camel case function name

Handle

heiho1

Vulnerability details

Impact

Detailed description of the impact of this finding.

Minimal code quality issue.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/interfaces/IRCFactory.sol#L26

The function setminimumPriceIncreasePercent does not follow the code standard of camel casing of function names.

Tools Used

Manual code review.

Recommended Mitigation Steps

Rename the function to have proper camel casing.

missing-zero-check

Handle

heiho1

Vulnerability details

Impact

RCMarket#initialize(..) accepts several contract state addresses but does not check them for zero address. This could result in markets where one or more participants are interacting with a useless zero address.

Proof of Concept

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

Tools Used

Slither

Recommended Mitigation Steps

The artistAddress, _affiliateAddress and _marketCreatorAddress variables should be required to not be address(0).

Can access cards of other markets

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

possible underflow in removeUserFromOrderbook

Handle

gpersoon

Vulnerability details

Impact

In the function removeUserFromOrderbook of RCMarket.sol, the length of the array user is taken and then 1 is subtraced (see proof of concept below).
If the length of the array would happen to be 0 then an underflow would occur and the function would revert.
Probably it is better just to return from the function if the length would be 0

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L854
function removeUserFromOrderbook(address _user) external override returns (bool _userForeclosed) {
.....
uint256 i = user[_user].length;
...
address _market = user[_user][i - 1].market;
uint256 _card = user[_user][i - 1].token;

Tools Used

Recommended Mitigation Steps

Add something like:
if (i == 0) return

Locked ether

Handle

heiho1

Vulnerability details

Impact

RCMarket overrides NativeMetaTransaction which declares payable executeMetaTransaction
---- This function accepts a functionSignature and calls the function from the userAddress
---- This appears to be a possible attack vector

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/lib/NativeMetaTransaction.sol#L31

This function is payable but the the encoded function invocation is arbitrary. This could potentially lead to locked ether as there is no clear withdrawal semantics.

Tools Used

Slither

Recommended Mitigation Steps

If there is no clear reason for the method to be payable then it should not be made payable. If there is a reason then a withdrawal mechanism should be supported, i.e.

https://docs.soliditylang.org/en/v0.5.3/solidity-by-example.html#safe-remote-purchase

transferFrom result not checked

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

reentrancy-no-eth

Handle

heiho1

Vulnerability details

Impact

Function RCMarket#lockMarket() is public and so can be invoked by anyone. It claims to be called within the context of the autoLock modifier but said modifier is not applied to the function call and so this function can be repeatedly called publicly by anyone. This could be quite expensive an operation as it collects all rent , change market state, and iterates over all cards for card transfers. In short it appears this function could cause market misbehavior simply by repeatedly invoking it within a block.

Proof of Concept

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

Tools Used

Slither

Recommended Mitigation Steps

It appears this market should either be restricted to admin/owner access or marked internal and be applied by the modifier.

function resetUser should emit LogUserForeclosed

Handle

pauliax

Vulnerability details

Impact

function resetUser sets isForeclosed to false so I expect to see LogUserForeclosed event afterward.

Recommended Mitigation Steps

emit LogUserForeclosed(_user, false);

underflow with _timeHeldToIncrement

Handle

gpersoon

Vulnerability details

Impact

In the function _processRentCollection of RCMarket.sol a variable _timeHeldToIncrement is calculated by subtracting two timestamps.
This could have more or less any value.
Later on this is subtracted from cardTimeLimit and timeHeldLimit (see code below). The values of cardTimeLimit and timeHeldLimit will get closer to 0,
However its possible that the subtraction will "overshoot", which will lead to an underflow.
As solidity 8.x is used, a revert will occur and the code will stop.
This is probably not what is desired.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L854
function _collectRentAction(uint256 _card)
uint256 _timeOfThisCollection = block.timestamp;
...
if .... _timeOfThisCollection = marketLockingTime;
if ..... _timeOfThisCollection = _cardTimeLimitTimestamp;
if .... _timeOfThisCollection = _timeUserForeclosed;
....
_processRentCollection(_user, _card, _timeOfThisCollection); // where the rent collection actually happens

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L1052
function _processRentCollection(address _user,uint256 _card,uint256 _timeOfCollection) {
....
uint256 _timeHeldToIncrement = (_timeOfCollection - timeLastCollected[_card]);
....
orderbook.reduceTimeHeldLimit(_user, _card, _timeHeldToIncrement);
cardTimeLimit[_card] -= _timeHeldToIncrement; // could underflow
}

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCOrderbook.sol#L850
function reduceTimeHeldLimit(address _user, uint256 _card, uint256 _timeToReduce ) external override onlyMarkets {
user[_user][index[_user][msgSender()][_card]].timeHeldLimit -= SafeCast.toUint64(_timeToReduce); // could underflow
}

Tools Used

Recommended Mitigation Steps

Make sure the subtraction doesn't overshoot and assign the value of 0 if it would be negative.
For example with the following code:
user[_user][index[_user][msgSender()][_card]].timeHeldLimit -= min ( SafeCast.toUint64(_timeToReduce), user[_user][index[_user][msgSender()][_card]].timeHeldLimit )
cardTimeLimit[_card] -= min ( _timeHeldToIncrement, cardTimeLimit[_card] )

Gas inefficiency with NativeMetaTransaction and calldata

Handle

axic

Vulnerability details

Impact

In lib/NativeMetaTransactions.sol there is a frequently used helper msgSender:

    function msgSender() internal view returns (address payable sender) {
        if (msg.sender == address(this)) {
            bytes memory array = msg.data;
            uint256 index = msg.data.length;
            assembly {
                // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those.
                sender := and(
                    mload(add(array, index)),
                    0xffffffffffffffffffffffffffffffffffffffff
                )
            }
        } else {
...

Even though only the last 20-bytes matter, the bytes memory array = msg.data; line causes the entire calldata to be copied to memory. This is exaggerated by the fact, that if msgSender() is called multiple times in a transaction, the calldata will be also copied multiple times as memory is not freed.

Proof of Concept

N/A

Tools Used

Manual review.

Recommended Mitigation Steps

There are multiple ways to avoid this:

  1. Make use of calldata slices and conversions

Something along the lines of (untested!):

            // Copy last 20 bytes
            bytes calldata data = msg.data[(msg.data.length - 20):];
            sender = payable(address(uint160(bytes20(data))));
  1. Implementing purely in assembly

The OpenZeppelin implementation (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/ERC2771Context.sol#L21-L30) is an example of an optimised assembly version:

            assembly {
                sender := shr(96, calldataload(sub(calldatasize(), 20)))
            }
  1. Combining slices and assembly

One must note that the pure assembly version is obviously the most gas efficient, at least today.

call updateTimeHeldLimit multiple times?

Handle

gpersoon

Vulnerability details

Impact

The function updateTimeHeldLimit of RCMarket.sol only does its function when _collectRent returns true (e.g. when the rent collection is done)
It seems you should call updateTimeHeldLimit multiple times to let it do its job.
However it's difficult to know how many times you should call this function, because it doesn't give a return value.
Perhaps it can be inferred from the emit of LogUpdateTimeHeldLimit however this can't be done from a smart contract and isn't shown in the comments.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L753
function updateTimeHeldLimit(uint256 _timeHeldLimit, uint256 _card) external {
..
if (_collectRent(_card)) {
// do stuff
emit LogUpdateTimeHeldLimit(_user, _timeHeldLimit, _card);
}
}

Tools Used

Recommended Mitigation Steps

Define how to determine when you are ready calling updateTimeHeldLimit
Perhaps return a boolean to indicate you should call the function again.

timestamp

Handle

heiho1

Vulnerability details

Impact

RCMarket has several functions that rely on block.timestamp and so may be potentially attacked by miners to affect market behavior.

Proof of Concept

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

-- RCMarket#initialize
--- marketOpeningTime <= block.timestamp (contracts/RCMarket.sol#274)
---- subject to miner timestamp manipulation
-- RCMarket#getWinnerFromOracle
--- (marketLockingTime <= block.timestamp,Market not finished) (contracts/RCMarket.sol#420)
---- subject to miner timestamp manipulation
-- RCMarket#lockMarket() (contracts/RCMarket.sol#441-460)
--- require(bool,string)(marketLockingTime <= block.timestamp,Market has not finished) (contracts/RCMarket.sol#443-446)
-- RCMarket#_payout(address,uint256) (contracts/RCMarket.sol#550-552) uses timestamp for comparisons\n\tDangerous comparisons:\n\t- assert(bool)(treasury.payout(_recipient,_amount)) (contracts/RCMarket.sol#551)
-- RCMarket#payMarketCreator() (contracts/RCMarket.sol#568-574) uses timestamp for comparisons\n\tDangerous comparisons:\n\t- require(bool,string)(totalTimeHeld[winningOutcome] > 0,No winner) (contracts/RCMarket.sol#570)
-- RCMarket#newRental(uint256,uint256,address,uint256) (contracts/RCMarket.sol#666-734) uses timestamp for comparisons\n\tDangerous comparisons:\n\t- require(bool,string)(exitedTimestamp[_user] != block.timestamp,Cannot lose and re-rent in same block) (contracts/RCMarket.sol#678-681)
-- RCMarket#_collectRentAction(uint256)
--- marketLockingTime <= block.timestamp (contracts/RCMarket.sol#862)
--- _limitHit = cardTimeLimit[_card] != 0 && _cardTimeLimitTimestamp < block.timestamp (contracts/RCMarket.sol#882-884)
-- RCMarket#circuitBreaker()
--- require(bool,string)(block.timestamp > (uint256(oracleResolutionTime) + (7257600)),Too early) (contracts/RCMarket.sol#1108-1111)

Tools Used

Slither

Recommended Mitigation Steps

Potentially use an oracle for real world UTC semantics.

RCTreasury.collectRentUser() improperly uses assert() rather than require()

Handle

jvaqa

Vulnerability details

Impact

According to Solidity documentation for v0.8.0:

"Properly functioning code should never [trigger an assert], not even on invalid external input. If this happens, then there is a bug in your contract which you should fix."[1]
[1] https://docs.soliditylang.org/en/v0.8.0/control-structures.html#error-handling-assert-require-revert-and-exceptions

The reason that asserts should never be triggered in bug-free code is that triggering an assert() causes the transaction to use up more space on the blockchain than is necessary, since the transaction uses all gas provided by the user in the transaction's gasLimit, whereas triggering a require() only uses up the gas used up to the point of the failed require() statement. Using up more gas than necessary hurts the public good of blockspace, by unnecessarily filling up the block with more gas than is strictly necessary for the failed transaction to use. It is useful in development environments to distinguish between allowable reverts and unallowable reverts in automated testing, but asserts should not be reached in live code.

Proof of Concept

Alice can call RCTreasury.collectRentUser(aliceAddress, 0) to trigger an assert

Recommended Mitigation Steps

Change:

assert(_timeToCollectTo != 0);

To:

require(_timeToCollectTo != 0);

Inclusive check of user deposit

Handle

pauliax

Vulnerability details

Impact

Here I think the check should be inclusive ('>=', not '>') to indicate the sufficient deposit balance:
// this deposit could cancel the users foreclosure
if (
(user[_user].deposit + _amount) >
(user[_user].bidRate / minRentalDayDivisor)
) {
isForeclosed[_user] = false;
emit LogUserForeclosed(_user, false);
}

Recommended Mitigation Steps

if (
(user[_user].deposit + _amount) >=
(user[_user].bidRate / minRentalDayDivisor)
) {
...
}

addToWhitelist doesn't check factoryAddress

Handle

gpersoon

Vulnerability details

Impact

The function addToWhitelist of RCTreasury.sol does a call to the factory contract, however the factoryAddress might not be initialized, because it is set via a different function
(setFactoryAddress).
The function addToWhitelist will revert when it calls a 0 address, but it might be more difficult to troubleshoot.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L233
function setFactoryAddress(address _newFactory) external override {
...
factoryAddress = _newFactory;
}

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L210
function addToWhitelist(address _user) public override {
IRCFactory factory = IRCFactory(factoryAddress);
require(factory.isGovernor(msgSender()), "Not authorised");
isAllowed[_user] = !isAllowed[_user];
}

Tools Used

Recommended Mitigation Steps

Verify that factoryAddress is set in the function addToWhitelist, for example using the following code.
require(factory != address(0), "Must have an address");

break loop if 0 card affiliate address is found

Handle

pauliax

Vulnerability details

Impact

Could break inside if as there is no point in iterating further once cardAffiliateCut is set to 0:
// check the validity of card affiliate array.
// if not valid, reduce payout to zero
if (_cardAffiliateAddresses.length == _numberOfCards) {
for (uint256 i = 0; i < _numberOfCards; i++) {
if (_cardAffiliateAddresses[i] == address(0)) {
cardAffiliateCut = 0;
}
}
} else {
cardAffiliateCut = 0;
}

Recommended Mitigation Steps

add 'break' inside inner if statement.

erc20 transfer and transferFrom functions

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.

minRentalDayDivisor can be different between markets and treasury

Handle

gpersoon

Vulnerability details

Impact

The minRentalDayDivisor is defined in RCTreasury.sol and copied to each market.
The minRentalDayDivisor can be updated via setMinRental, but then it isn't updated in the already created market.

To calculate the minimum rent time, in function withdrawDeposit of RCTreasury.sol, the latest version of minRentalDayDivisor is used, which could be different than the values in the market.
So the markets will calculate the minimum rent time different.
This could lead to unexpected results

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L191
function initialize(
...
minRentalDayDivisor = treasury.minRentalDayDivisor();

https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L322
function withdrawDeposit(uint256 _amount, bool _localWithdrawal)
...
require( user[_msgSender].bidRate == 0 || block.timestamp - (user[_msgSender].lastRentalTime) > uint256(1 days) / minRentalDayDivisor, "Too soon");
..
if ( user[_msgSender].bidRate != 0 && user[_msgSender].bidRate / (minRentalDayDivisor) > user[_msgSender].deposit ) {
..

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L169
function setMinRental(uint256 _newDivisor) public override onlyOwner {
minRentalDayDivisor = _newDivisor;
}

Tools Used

Recommended Mitigation Steps

Either accept and document the risk or change to code to prevent this from happening.

_amount added to deposit twice

Handle

gpersoon

Vulnerability details

Impact

In the function deposit of RCTreasury.sol the users deposit is increased with _amount.
Later in the function a check is done if the user have enough fund to prevent a foreclosure.
However at that moment the _amount is added to the users deposit again.
This doesn't seem logical and would lead to the fact that the foreclosure check doesn't work.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L279
function deposit(uint256 _amount, address _user) public override balancedBooks returns (bool) {
...
user[_user].deposit += SafeCast.toUint128(_amount); // first addition of _amount
....
// this deposit could cancel the users foreclosure
if (
(user[_user].deposit + _amount) > (user[_user].bidRate / minRentalDayDivisor) ) { // _amount is added to deposit again
...

Tools Used

Recommended Mitigation Steps

Remove the second addition of _amount

constable-states

Handle

heiho1

Vulnerability details

Impact

RCMarket#_realitioAddress is never initialized and is public. This is intended to be an oracle address but the realition oracle is assigned alternatively. As it is unused and public it should either be removed or made functional.

Proof of Concept

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

Tools Used

Slither

Recommended Mitigation Steps

Removed unused addresses.

updateRentalRate updates deposit but does not emit LogAdjustDeposit

Handle

pauliax

Vulnerability details

Impact

function updateRentalRate invokes:
_increaseMarketBalance(_additionalRentOwed, _newOwner);
but it does not emit LogAdjustDeposit event which should indicate the change in user's deposit.

Recommended Mitigation Steps

emit LogAdjustDeposit(_newOwner, _additionalRentOwed, false);

modifier balancedBooks missing in a few functions

Handle

gpersoon

Vulnerability details

Impact

Most of the functions of RCTreasury.sol, that manipulate totalDeposits, marketBalance or totalMarketPots use the modifier balancedBooks.
However the functions refundUser and topupMarketBalance don't use the modifier.
It doesn't hurt to add the extra safeguard.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L124
/// @notice check that funds haven't gone missing during this function call
modifier balancedBooks {
_;
// using >= not == in case anyone sends tokens direct to contract
require(erc20.balanceOf(address(this)) >= totalDeposits + marketBalance + totalMarketPots,"Books are unbalanced!");
}

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L447
function refundUser(address _user, uint256 _refund) external override onlyMarkets {
marketBalance -= _refund;
...
totalDeposits += _refund;

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L372
function topupMarketBalance(uint256 _amount) external override {
....
marketBalance += _amount;
}

Tools Used

Recommended Mitigation Steps

Add the modifier balancedBooks to the functions refundUser and topupMarketBalance

Unchecked ERC20 transfers can cause lock up

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

RCTreasury.addToWhitelist() will erroneously remove user from whitelist if user is already whitelisted

Handle

jvaqa

Vulnerability details

RCTreasury.addToWhitelist() will erroneously remove user from whitelist if user is already whitelisted

Impact

The comments state that calling addToWhitelist() should add a user to the whitelist. [1]

However, since the implementation simply flips the user's whitelist bool, if the user is already on the whitelist, then calling addToWhitelist() will actually remove them from the whitelist. [2]

Since batchAddToWhitelist() will repeatedly call addToWhitelist() with an entire array of users, it is very possible that someone could inadvertently call addToWhitelist twice for a particular user, thereby leaving them off of the whitelist. [3]

Proof of Concept

If a governor calls addToWhitelist() with the same user twice, the user will not be added to the whitelist, even though the comments state that they should.

Recommended Mitigation Steps

Change addToWhitelist to only ever flip a user's bool to true. To clarify the governor's intention, create a corresponding removeFromWhitelist and batchRemoveFromWhitelist which flip a user's bool to false, so that the governor does not accidently remove a user when intending to add them.

Change this:

isAllowed[_user] = !isAllowed[_user]; // [4]

To this:

isAllowed[_user] = true; // [4]

And add this:

/// @notice Remove a user to the whitelist
function removeFromWhitelist(address _user) public override {
    IRCFactory factory = IRCFactory(factoryAddress);
    require(factory.isGovernor(msgSender()), "Not authorised");
    isAllowed[_user] = false;
}

/// @notice Remove multiple users from the whitelist
function batchRemoveFromWhitelist(address[] calldata _users) public override {
    for (uint256 index = 0; index < _users.length; index++) {
        removeFromWhitelist(_users[index]);
    }
}

[1] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L209

[2] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L213

[3] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L217

[4] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L213

external-function

Handle

heiho1

Vulnerability details

Impact

RCMarket#tokenURI(uint256) is declared external in the IRCMarket interface but is declared public in the RCMarket implementation. This is inconsistent and affect the gas behavior of the function: https://gus-tavo-guim.medium.com/public-vs-external-functions-in-solidity-b46bcf0ba3ac

Proof of Concept

https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/interfaces/IRCMarket.sol#L27

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

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

Tools Used

Slither

Recommended Mitigation Steps

Mark the implementation method as external.

payout doesn't fix isForeclosed state

Handle

gpersoon

Vulnerability details

Impact

The function payout of RCTreasury.sol doesn't undo the isForeclosed state of a user.
This would be possible because with a payout a user will receive funds so he can lose his isForeclosed status.

For example the function refundUser does check and update the isForeclosed status.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L429
function payout(address _user, uint256 _amount) external override balancedBooks onlyMarkets returns (bool) {
require(!globalPause, "Payouts are disabled");
assert(marketPot[msgSender()] >= _amount);
user[_user].deposit += SafeCast.toUint128(_amount);
marketPot[msgSender()] -= _amount;
totalMarketPots -= _amount;
totalDeposits += _amount;
emit LogAdjustDeposit(_user, _amount, true);
return true;
}

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L447
function refundUser(address _user, uint256 _refund) external override onlyMarkets {
...
if ( isForeclosed[_user] && user[_user].deposit > user[_user].bidRate / minRentalDayDivisor ) {
isForeclosed[_user] = false;
emit LogUserForeclosed(_user, false);
}

Tools Used

Recommended Mitigation Steps

Check and update the isForeclosed state in the payout function

circuitBreaker overrides the state

Handle

pauliax

Vulnerability details

Impact

function circuitBreaker calls _incrementState but later sets the state itself again:
function _incrementState() internal {
assert(uint256(state) < 4);
state = States(uint256(state) + (1));
emit LogStateChange(uint256(state));
}

function circuitBreaker() external {
    require(
        block.timestamp > (uint256(oracleResolutionTime) + (12 weeks)),
        "Too early"
    );
    _incrementState();
    orderbook.closeMarket();
    state = States.WITHDRAW;
}

Recommended Mitigation Steps

state = States.WITHDRAW; shouldn't be there, or another solution would be to put it before orderbook.closeMarket(); and remove _incrementState(); instead but then LogStateChange event will also need to be emitted manually.

Multiple calls necessary for getWinnerFromOracle

Handle

gpersoon

Vulnerability details

Impact

Sometimes multiple calls necessary to getWinnerFromOracle are necessary to get the _winningOutcome to be processed:

  • getWinnerFromOracle calls setWinner
  • setWinner calls lockMarket
  • lockMarket calls collectRentAllCards
  • collectRentAllCards can return false, which means is has to be called again. In that case the _winningOutcome isn't processed and getWinnerFromOracle has to be called again.

It's not easy to determine how many times getWinnerFromOracle has to be called.
(it can be seen via emit LogWinnerKnown(winningOutcome), however this cannot be read from a smart contract)

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L464
function setWinner(uint256 _winningOutcome) internal {
if (state == States.OPEN) {
// change the locking time to allow lockMarket to lock
marketLockingTime = SafeCast.toUint32(block.timestamp);
lockMarket();
}
if (state == States.LOCKED) {
// get the winner. This will revert if answer is not resolved.
winningOutcome = _winningOutcome;
_incrementState();
emit LogWinnerKnown(winningOutcome);
}
}

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L441
function lockMarket() public {
...
if (collectRentAllCards()) {
....
_incrementState();
...
}
}

Tools Used

Recommended Mitigation Steps

Let the function getWinnerFromOracle return a boolean to indicate it has to be called again.

Add comment to not obvious code in withdrawDeposit

Handle

gpersoon

Vulnerability details

Impact

In the function withdrawDeposit of RCTreasury.sol, the value of isForeclosed[_msgSender] is set to true.
In the next statement it is overwritten with a new value. So the first statement seem redundant.
However this is not the case because it is retrieved from the function removeUserFromOrderbook
(see proof of concept below)

As this is not obvious it is probably useful to add a comment so future developers can understand this.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L322
function withdrawDeposit(uint256 _amount, bool _localWithdrawal) external override balancedBooks {
...
isForeclosed[_msgSender] = true; // this seems to be redundant
isForeclosed[_msgSender] = orderbook.removeUserFromOrderbook( _msgSender );

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCOrderbook.sol#L575
function removeUserFromOrderbook(address _user) external override returns (bool _userForeclosed) {
require(treasury.isForeclosed(_user), "User must be foreclosed"); // this checks the isForeclosed value from the treasury contract

Tools Used

Recommended Mitigation Steps

Add a comment to isForeclosed[_msgSender] = true; explaining this line is important.

Checks for enum bounds

Handle

gpersoon

Vulnerability details

Impact

For the enums Mode and State, checks are made that the variables are within bounds. Here specific size are used, e.g. 2 and 4.
If the size of the enums would be changed in the future, those numbers don't change automatically.
Also solidity provides in-built check to check that variables are within bounds, which could be used instead. This also make the code more readable.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L202
enum Mode {CLASSIC, WINNER_TAKES_ALL, SAFE_MODE}
function initialize(
...
assert(_mode <= 2); // can be removed
...
mode = Mode(_mode); // this makes sure: 0<=mode<=2 // move to top

https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/interfaces/IRCMarket.sol#L7
enum States {CLOSED, OPEN, LOCKED, WITHDRAW}

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L1094
function _incrementState() internal {
assert(uint256(state) < 4); // can be removed
state = States(uint256(state) + (1)); // this makes sure: 0<=state<=3
emit LogStateChange(uint256(state));
}

Tools Used

Recommended Mitigation Steps

For function initialize:
Remove the "assert(_mode <= 2);" and move the statement "mode = Mode(_mode);" to the top of the function and add a comment

For function _incrementState:
Remove "assert(uint256(state) < 4);" and add a comment at "state = States(uint256(state) + (1));"

Possible Reentrency not-involving-eth Issues [RCOrderbook.sol]

Handle

maplesyrup

Vulnerability details

Impact

2 - Medium Risk

  • Possible reentrancy found in the contract, possible loss of funds due to code manipulation

Proof of Concept

According to the Slither-analyzer documentation (https://github.com/crytic/slither/wiki/Detector-Documentation#configuration-32): Detection of reentrancy was found in the following functions as there are external calls made before state variables are changed. This can lead to a possible attack on the functions and contract.

Reentrancy found in:

contracts/RCOrderbook.sol

line(s) 280-340

RCOrderbook._newBidInOrderbook(address, address, uint256, uint256, uint256, RCOrderbook.Bid)

External calls:

treasury.increaseBidRate(_user,_price) 
(contracts/RCOrderbook.sol line(s)#328)

transferCard(_market,_card,_oldOwner,_user,_price) 
(contracts/RCOrderbook.sol line(s)#331)

_rcmarket.transferCard(_oldOwner,_newOwner,_card,_price,_timeLimit) 
(contracts/RCOrderbook.sol line(s)#870)

State variables written after the call(s):

transferCard(_market,_card,_oldOwner,_user,_price) 
(contracts/RCOrderbook.sol line(s)#331)

ownerOf[_market][_card] = _newOwner 
(contracts/RCOrderbook.so line(s)#866)

RCOrderbook._removeBidFromOrderbookIgnoreOwner(address,uint256)
(contracts/RCOrderbook.sol line(s)#492-527):

External calls:

treasury.decreaseBidRate(_user,_currUser.price) 
(contracts/RCOrderbook.sol line(s)#499)

State variables written after the call(s):

index[_user][_market][_card] = 0 
(contracts/RCOrderbook.sol line(s)#520)

index[_user][user[_user][_index].market][user[_user][_index].token] = _index 
(contracts/RCOrderbook.sol line(s)#522-524)

user[_tempNext][index[_tempNext][_market][_card]].prev = _tempPrev 
(contracts/RCOrderbook.sol line(s)#504)

user[_tempPrev][index[_tempPrev][_market][_card]].next = _tempNext 
(contracts/RCOrderbook.sol line(s)#505)

user[_user][_index] = user[_user][_lastRecord] 
(contracts/RCOrderbook.sol line(s)#515)

user[_user].pop() 
(contracts/RCOrderbook.sol line(s)#517)

RCOrderbook.closeMarket()
(contracts/RCOrderbook.sol line(s)#633-669):

External calls:
treasury.updateRentalRate(_owner,_market,_price,0,block.timestamp)
(contracts/RCOrderbook.sol line(s)#641-647)

State variables written after the call(s):
user[_market][index[_market][_market][i]].prev = _market
(contracts/RCOrderbook.sol line(s)#654)

user[_market][index[_market][_market][i]].next = _market 
(contracts/RCOrderbook.sol line(s)#655) 

user[_firstBid][index[_market][_firstBid][i]].prev = address(this) 
(contracts/RCOrderbook.sol line(s)#656)

user[_lastBid][index[_market][_lastBid][i]].next = address(this)
(contracts/RCOrderbook.sol line(s)#657) 

user[address(this)].push(_newBid) 
(contracts/RCOrderbook.sol line(s)#667)

RCOrderbook.removeBidFromOrderbook(address,uint256)

(contracts/RCOrderbook.sol line(s)#442-489):

External calls:

treasury.decreaseBidRate(_user,_currUser.price) 
(contracts/RCOrderbook.sol line(s)#450)

transferCard(_market,_card,_user,_currUser.next,_price) 
(contracts/RCOrderbook.sol line(s)#456)

_rcmarket.transferCard(_oldOwner,_newOwner,_card,_price,_timeLimit)
(contracts/RCOrderbook.sol line(s)#870)

treasury.updateRentalRate(_user,_currUser.next,_currUser.price,_price,block.timestamp) 
(contracts/RCOrderbook.sol line(s)#457-463)

State variables written after the call(s):

index[_user][_market][_card] = 0 
(contracts/RCOrderbook.sol line(s)#482)

index[_user][user[_user][_index].market][user[_user][_index].token] = _index 
(contracts/RCOrderbook.sol line(s)#484-486)

user[_tempNext][index[_tempNext][_market][_card]].prev = _tempPrev 
(contracts/RCOrderbook.sol line(s)#468)

user[_tempPrev][index[_tempPrev][_market][_card]].next = _tempNext
(contracts/RCOrderbook.sol line(s)#469)

user[_user][_index] = user[_user][_lastRecord] 
(contracts/RCOrderbook.sol line(s)#477) 

user[_user].pop() 
(contracts/RCOrderbook.sol line(s)#479)

RCOrderbook.removeOldBids(address)

(contracts/RCOrderbook.sol line(s)#674-713):

External calls:

treasury.decreaseBidRate(_user,_price) 
(contracts/RCOrderbook.sol line(s)#690)

State variables written after the call(s):

index[_user][_market][i] = 0 
(contracts/RCOrderbook.sol line(s)#705)

user[_tempNext][index[_tempNext][_market][i]].prev = _tempPrev
(contracts/RCOrderbook.sol line(s)#698-699)

user[_tempPrev][index[_tempPrev][_market][i]].next = _tempNext 
(contracts/RCOrderbook.sol line(s)#700-701)

user[_user].pop() 
(contracts/RCOrderbook.sol line(s)#704)

RCOrderbook.removeUserFromOrderbook(address)

(contracts/RCOrderbook.sol line(s)#575-629):

External calls:

treasury.updateRentalRate(_user,_tempNext,user[_user][i].price,_price, block.timestamp) 
(contracts/RCOrderbook.sol  line(s)#601-607) 

transferCard(_market,_card,_user,_tempNext,_price) 
(contracts/RCOrderbook.sol line(s)#608)

_rcmarket.transferCard(_oldOwner,_newOwner,_card,_price,_timeLimit) 
(contracts/RCOrderbook.sol line(s)#870)

treasury.decreaseBidRate(_user,user[_user][i].price) 
(contracts/RCOrderbook.sol line(s)#611)

State variables written after the call(s):

user[_tempNext][index[_tempNext][user[_user][i].market][user[_user][i].token]].prev = _tempPrev 
(contracts/RCOrderbook.sol  line(s)#613-616)

user[_tempPrev][index[_tempPrev][user[_user][i].market][user[_user][i].token]].next = _tempNext 
(contracts/RCOrderbook.sol line(s)#617-620)

user[_user].pop() 
(contracts/RCOrderbook.sol line(s)#621)

As the document mentions, the best way to solve the issue(s) found on reentrancy is to apply the Checks-Effects-Interactions pattern to the following functions.


Console output(Slither log):

INFO:Detectors:
Reentrancy in RCMarket._collectRentAction(uint256) (contracts/RCMarket.sol#854-1034):
External calls:
- _timeUserForeclosed = treasury.collectRentUser(_user,block.timestamp) (contracts/RCMarket.sol#873-874)
- treasury.refundUser(_user,_refundAmount) (contracts/RCMarket.sol#1020)
- _processRentCollection(_user,_card,_timeOfThisCollection) (contracts/RCMarket.sol#1022)
- treasury.payRent(_rentOwed) (contracts/RCMarket.sol#1060)
- orderbook.reduceTimeHeldLimit(_user,_card,_timeHeldToIncrement) (contracts/RCMarket.sol#1066)
State variables written after the call(s):
- _processRentCollection(_user,_card,_timeOfThisCollection) (contracts/RCMarket.sol#1022)
- cardTimeLimit[_card] -= _timeHeldToIncrement (contracts/RCMarket.sol#1067)
- _processRentCollection(_user,_card,_timeOfThisCollection) (contracts/RCMarket.sol#1022)
- timeLastCollected[_card] = _timeOfCollection (contracts/RCMarket.sol#1075)
Reentrancy in RCOrderbook._newBidInOrderbook(address,address,uint256,uint256,uint256,RCOrderbook.Bid) (contracts/RCOrderbook.sol#280-340):
External calls:
- treasury.increaseBidRate(_user,_price) (contracts/RCOrderbook.sol#328)
- transferCard(_market,_card,_oldOwner,_user,_price) (contracts/RCOrderbook.sol#331)
- _rcmarket.transferCard(_oldOwner,_newOwner,_card,_price,_timeLimit) (contracts/RCOrderbook.sol#870)
State variables written after the call(s):
- transferCard(_market,_card,_oldOwner,_user,_price) (contracts/RCOrderbook.sol#331)
- ownerOf[_market][_card] = _newOwner (contracts/RCOrderbook.sol#866)
Reentrancy in RCMarket._processRentCollection(address,uint256,uint256) (contracts/RCMarket.sol#1052-1083):
External calls:
- treasury.payRent(_rentOwed) (contracts/RCMarket.sol#1060)
- orderbook.reduceTimeHeldLimit(_user,_card,_timeHeldToIncrement) (contracts/RCMarket.sol#1066)
State variables written after the call(s):
- cardTimeLimit[_card] -= _timeHeldToIncrement (contracts/RCMarket.sol#1067)
- timeLastCollected[_card] = _timeOfCollection (contracts/RCMarket.sol#1075)
Reentrancy in RCOrderbook._removeBidFromOrderbookIgnoreOwner(address,uint256) (contracts/RCOrderbook.sol#492-527):
External calls:
- treasury.decreaseBidRate(_user,_currUser.price) (contracts/RCOrderbook.sol#499)
State variables written after the call(s):
- index[_user][_market][_card] = 0 (contracts/RCOrderbook.sol#520)
- index[_user][user[_user][_index].market][user[_user][_index].token] = _index (contracts/RCOrderbook.sol#522-524)
- user[_tempNext][index[_tempNext][_market][_card]].prev = _tempPrev (contracts/RCOrderbook.sol#504)
- user[_tempPrev][index[_tempPrev][_market][_card]].next = _tempNext (contracts/RCOrderbook.sol#505)
- user[_user][_index] = user[_user][_lastRecord] (contracts/RCOrderbook.sol#515)
- user[_user].pop() (contracts/RCOrderbook.sol#517)
Reentrancy in RCMarket.circuitBreaker() (contracts/RCMarket.sol#1107-1115):
External calls:
- orderbook.closeMarket() (contracts/RCMarket.sol#1113)
State variables written after the call(s):
- state = States.WITHDRAW (contracts/RCMarket.sol#1114)
Reentrancy in RCOrderbook.closeMarket() (contracts/RCOrderbook.sol#633-669):
External calls:
- treasury.updateRentalRate(_owner,_market,_price,0,block.timestamp) (contracts/RCOrderbook.sol#641-647)
State variables written after the call(s):
- user[_market][index[_market][_market][i]].prev = _market (contracts/RCOrderbook.sol#654)
- user[_market][index[_market][_market][i]].next = _market (contracts/RCOrderbook.sol#655)
- user[_firstBid][index[_market][_firstBid][i]].prev = address(this) (contracts/RCOrderbook.sol#656)
- user[_lastBid][index[_market][_lastBid][i]].next = address(this) (contracts/RCOrderbook.sol#657)
- user[address(this)].push(_newBid) (contracts/RCOrderbook.sol#667)
Reentrancy in RCFactory.createMarket(uint32,string,uint32[],string[],address,address,address[],string,uint256) (contracts/RCFactory.sol#465-613):
External calls:
- treasury.addMarket(_newAddress) (contracts/RCFactory.sol#569)
- nfthub.addMarket(_newAddress) (contracts/RCFactory.sol#570)
- orderbook.addMarket(_newAddress,_tokenURIs.length,minimumPriceIncreasePercent) (contracts/RCFactory.sol#571-575)
- IRCMarket(_newAddress).initialize(_mode,_timestamps,_tokenURIs.length,totalNftMintCount,_artistAddress,_affiliateAddress,_cardAffiliateAddresses,_creator,_realitioQuestion) (contracts/RCFactory.sol#582-592)
State variables written after the call(s):
- totalNftMintCount = totalNftMintCount + _tokenURIs.length (contracts/RCFactory.sol#605)
Reentrancy in RCTreasury.deposit(uint256,address) (contracts/RCTreasury.sol#279-316):
External calls:
- erc20.transferFrom(msgSender(),address(this),_amount) (contracts/RCTreasury.sol#298)
- orderbook.removeOldBids(_user) (contracts/RCTreasury.sol#301)
State variables written after the call(s):
- totalDeposits += _amount (contracts/RCTreasury.sol#304)
Reentrancy in RCMarket.lockMarket() (contracts/RCMarket.sol#441-460):
External calls:
- collectRentAllCards() (contracts/RCMarket.sol#449)
- treasury.payRent(_rentOwed) (contracts/RCMarket.sol#1060)
- orderbook.reduceTimeHeldLimit(_user,_card,_timeHeldToIncrement) (contracts/RCMarket.sol#1066)
- _timeUserForeclosed = treasury.collectRentUser(_user,block.timestamp) (contracts/RCMarket.sol#873-874)
- treasury.refundUser(_user,_refundAmount) (contracts/RCMarket.sol#1020)
- orderbook.findNewOwner(_card,_timeOfThisCollection) (contracts/RCMarket.sol#1025)
- orderbook.closeMarket() (contracts/RCMarket.sol#450)
State variables written after the call(s):
- _incrementState() (contracts/RCMarket.sol#451)
- state = IRCMarket.States(uint256(state) + (1)) (contracts/RCMarket.sol#1096)
Reentrancy in RCMarket.newRental(uint256,uint256,address,uint256) (contracts/RCMarket.sol#666-734):
External calls:
- _userStillForeclosed = orderbook.removeUserFromOrderbook(_user) (contracts/RCMarket.sol#689)
- orderbook.removeOldBids(_user) (contracts/RCMarket.sol#705)
- _collectRent(_card) (contracts/RCMarket.sol#707)
- treasury.payRent(_rentOwed) (contracts/RCMarket.sol#1060)
- orderbook.reduceTimeHeldLimit(_user,_card,_timeHeldToIncrement) (contracts/RCMarket.sol#1066)
- _timeUserForeclosed = treasury.collectRentUser(_user,block.timestamp) (contracts/RCMarket.sol#873-874)
- treasury.refundUser(_user,_refundAmount) (contracts/RCMarket.sol#1020)
- orderbook.findNewOwner(_card,_timeOfThisCollection) (contracts/RCMarket.sol#1025)
- autoLock() (contracts/RCMarket.sol#671)
- treasury.payRent(_rentOwed) (contracts/RCMarket.sol#1060)
- assert(bool)(nfthub.transferNft(_from,_to,_tokenId)) (contracts/RCMarket.sol#367)
- orderbook.closeMarket() (contracts/RCMarket.sol#450)
- orderbook.reduceTimeHeldLimit(_user,_card,_timeHeldToIncrement) (contracts/RCMarket.sol#1066)
- _timeUserForeclosed = treasury.collectRentUser(_user,block.timestamp) (contracts/RCMarket.sol#873-874)
- treasury.refundUser(_user,_refundAmount) (contracts/RCMarket.sol#1020)
- orderbook.findNewOwner(_card,_timeOfThisCollection) (contracts/RCMarket.sol#1025)
State variables written after the call(s):
- _collectRent(_card) (contracts/RCMarket.sol#707)
- cardTimeLimit[_card] -= _timeHeldToIncrement (contracts/RCMarket.sol#1067)
- _collectRent(_card) (contracts/RCMarket.sol#707)
- longestOwner[_card] = _user (contracts/RCMarket.sol#1080)
- _collectRent(_card) (contracts/RCMarket.sol#707)
- longestTimeHeld[_card] = timeHeld[_card][_user] (contracts/RCMarket.sol#1079)
- _collectRent(_card) (contracts/RCMarket.sol#707)
- rentCollectedPerCard[_card] += _rentOwed (contracts/RCMarket.sol#1072)
- _collectRent(_card) (contracts/RCMarket.sol#707)
- rentCollectedPerUser[_user] += _rentOwed (contracts/RCMarket.sol#1071)
- _collectRent(_card) (contracts/RCMarket.sol#707)
- rentCollectedPerUserPerCard[_user][_card] += _rentOwed (contracts/RCMarket.sol#1073)
- _collectRent(_card) (contracts/RCMarket.sol#707)
- timeHeld[_card][_user] += _timeHeldToIncrement (contracts/RCMarket.sol#1069)
- _collectRent(_card) (contracts/RCMarket.sol#707)
- timeLastCollected[_card] = _timeOfCollection (contracts/RCMarket.sol#1075)
- timeLastCollected[_card] = _timeOfThisCollection (contracts/RCMarket.sol#1031)
- _collectRent(_card) (contracts/RCMarket.sol#707)
- totalRentCollected += _rentOwed (contracts/RCMarket.sol#1074)
- _collectRent(_card) (contracts/RCMarket.sol#707)
- totalTimeHeld[_card] += _timeHeldToIncrement (contracts/RCMarket.sol#1070)
Reentrancy in RCOrderbook.removeBidFromOrderbook(address,uint256) (contracts/RCOrderbook.sol#442-489):
External calls:
- treasury.decreaseBidRate(_user,_currUser.price) (contracts/RCOrderbook.sol#450)
- transferCard(_market,_card,_user,_currUser.next,_price) (contracts/RCOrderbook.sol#456)
- _rcmarket.transferCard(_oldOwner,_newOwner,_card,_price,_timeLimit) (contracts/RCOrderbook.sol#870)
- treasury.updateRentalRate(_user,_currUser.next,_currUser.price,_price,block.timestamp) (contracts/RCOrderbook.sol#457-463)
State variables written after the call(s):
- index[_user][_market][_card] = 0 (contracts/RCOrderbook.sol#482)
- index[_user][user[_user][_index].market][user[_user][_index].token] = _index (contracts/RCOrderbook.sol#484-486)
- user[_tempNext][index[_tempNext][_market][_card]].prev = _tempPrev (contracts/RCOrderbook.sol#468)
- user[_tempPrev][index[_tempPrev][_market][_card]].next = _tempNext (contracts/RCOrderbook.sol#469)
- user[_user][_index] = user[_user][_lastRecord] (contracts/RCOrderbook.sol#477)
- user[_user].pop() (contracts/RCOrderbook.sol#479)
Reentrancy in RCOrderbook.removeOldBids(address) (contracts/RCOrderbook.sol#674-713):
External calls:
- treasury.decreaseBidRate(_user,_price) (contracts/RCOrderbook.sol#690)
State variables written after the call(s):
- index[_user][_market][i] = 0 (contracts/RCOrderbook.sol#705)
- user[_tempNext][index[_tempNext][_market][i]].prev = _tempPrev (contracts/RCOrderbook.sol#698-699)
- user[_tempPrev][index[_tempPrev][_market][i]].next = _tempNext (contracts/RCOrderbook.sol#700-701)
- user[_user].pop() (contracts/RCOrderbook.sol#704)
Reentrancy in RCOrderbook.removeUserFromOrderbook(address) (contracts/RCOrderbook.sol#575-629):
External calls:
- treasury.updateRentalRate(_user,_tempNext,user[_user][i].price,_price,block.timestamp) (contracts/RCOrderbook.sol#601-607)
- transferCard(_market,_card,_user,_tempNext,_price) (contracts/RCOrderbook.sol#608)
- _rcmarket.transferCard(_oldOwner,_newOwner,_card,_price,_timeLimit) (contracts/RCOrderbook.sol#870)
- treasury.decreaseBidRate(_user,user[_user][i].price) (contracts/RCOrderbook.sol#611)
State variables written after the call(s):
- user[_tempNext][index[_tempNext][user[_user][i].market][user[_user][i].token]].prev = _tempPrev (contracts/RCOrderbook.sol#613-616)
- user[_tempPrev][index[_tempPrev][user[_user][i].market][user[_user][i].token]].next = _tempNext (contracts/RCOrderbook.sol#617-620)
- user[_user].pop() (contracts/RCOrderbook.sol#621)
Reentrancy in RCMarket.setWinner(uint256) (contracts/RCMarket.sol#464-476):
External calls:
- lockMarket() (contracts/RCMarket.sol#468)
- treasury.payRent(_rentOwed) (contracts/RCMarket.sol#1060)
- assert(bool)(nfthub.transferNft(_from,_to,_tokenId)) (contracts/RCMarket.sol#367)
- orderbook.closeMarket() (contracts/RCMarket.sol#450)
- orderbook.reduceTimeHeldLimit(_user,_card,_timeHeldToIncrement) (contracts/RCMarket.sol#1066)
- _timeUserForeclosed = treasury.collectRentUser(_user,block.timestamp) (contracts/RCMarket.sol#873-874)
- treasury.refundUser(_user,_refundAmount) (contracts/RCMarket.sol#1020)
- orderbook.findNewOwner(_card,_timeOfThisCollection) (contracts/RCMarket.sol#1025)
State variables written after the call(s):
- _incrementState() (contracts/RCMarket.sol#473)
- state = IRCMarket.States(uint256(state) + (1)) (contracts/RCMarket.sol#1096)
Reentrancy in RCTreasury.sponsor(address,uint256) (contracts/RCTreasury.sol#466-482):
External calls:
- erc20.transferFrom(_sponsor,address(this),_amount) (contracts/RCTreasury.sol#478)
State variables written after the call(s):
- totalMarketPots += _amount (contracts/RCTreasury.sol#480)
Reentrancy in RCMarket.updateTimeHeldLimit(uint256,uint256) (contracts/RCMarket.sol#753-770):
External calls:
- _collectRent(_card) (contracts/RCMarket.sol#759)
- treasury.payRent(_rentOwed) (contracts/RCMarket.sol#1060)
- orderbook.reduceTimeHeldLimit(_user,_card,_timeHeldToIncrement) (contracts/RCMarket.sol#1066)
- _timeUserForeclosed = treasury.collectRentUser(_user,block.timestamp) (contracts/RCMarket.sol#873-874)
- treasury.refundUser(_user,_refundAmount) (contracts/RCMarket.sol#1020)
- orderbook.findNewOwner(_card,_timeOfThisCollection) (contracts/RCMarket.sol#1025)
- orderbook.setTimeHeldlimit(_user,_card,_timeHeldLimit) (contracts/RCMarket.sol#762)
State variables written after the call(s):
- cardTimeLimit[_card] = _timeHeldLimit (contracts/RCMarket.sol#765)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities-1

Tools Used

Solidity Compiler 0.8.4
Hardhat v2.3.3
Slither v.0.8.0

Compiled, Tested, and Deployed contracts on a local Hardhat network.

Ran Slither-analyzer for further detecting and testing

Recommended Mitigation Steps

(Worked best under a python virtual environment)

  1. Clone project repository
  2. Run project against hardhat network;
    compile and run default test on contracts.
  3. Installed sliither analyzer:
    https://github.com/crytic/slither
  4. Ran [$ slither .] against all contracts

Possible locked-ether (funds) Issue in RCOrderbook.sol

Handle

maplesyrup

Vulnerability details

Impact

2 - Medium Risk
- Possible loss or lock of funds found in a function in the contract

Proof of Concept

When running the analyzer code, the following functions were found in RCOrderbook.sol to possibly lock funds due to it being a payable function with no withdraw function associated.


Contract locking ether found:

// contracts/RCOrderbook.sol
// line(s) 15-876

Contract RCOrderbook

has payable functions:

// contracts/lib/NativeMetaTransaction.sol
// line(s) 31-67

NativeMetaTransaction.executeMetaTransaction(address,bytes,bytes32,bytes32,uint8)

But does not have a function to withdraw the funds

According to Slither analyzer detector documentation (https://github.com/crytic/slither/wiki/Detector-Documentation#contracts-that-lock-ether)

Possible functions that receive funds with the payable attribute must have a withdraw function to secure that funds can be sent out from the function or remove payable attribute.

Although the function may not receive funds directly, there should be a withdraw function added to ensure that information needed from the function can be withdrawn safely or do not include payable attribute.

Console Output (Slither log):

INFO:Detectors:
Contract locking ether found:
Contract RCOrderbook (contracts/RCOrderbook.sol#15-876) has payable functions:
- NativeMetaTransaction.executeMetaTransaction(address,bytes,bytes32,bytes32,uint8) (contracts/lib/NativeMetaTransaction.sol#31-67)
But does not have a function to withdraw the ether
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#contracts-that-lock-ether

Tools Used

Solidity Compiler 0.8.4
Hardhat v2.3.3
Slither v0.8.0

Compiled, Tested, Deployed contracts on a local hardhat network.

Ran Slither-analyzer for further detecting and testing.

Recommended Mitigation Steps

(Worked best under python venv)

  1. Clone Project Repository
  2. Run Project against Hardhat network;
    compile and run default test on contracts.
  3. Installed slither analyzer:
    https://github.com/crytic/slither
  4. Ran [$ slither .] against RCOrderbook.sol and all contracts to verify results

unnecessary emit of LogUserForeclosed

Handle

gpersoon

Vulnerability details

Impact

The function deposit of RCTreasury.sol resets the isForeclosed state and emits LogUserForeclosed, if the use have enough funds.
However this also happens if the user is not Foreclosed and so the emit is redundant and confusing.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L279
function deposit(uint256 _amount, address _user) public override balancedBooks returns (bool) {
....
// this deposit could cancel the users foreclosure
if ( (user[_user].deposit + _amount) > (user[_user].bidRate / minRentalDayDivisor) ) {
isForeclosed[_user] = false;
emit LogUserForeclosed(_user, false);
}
return true;
}

Tools Used

Recommended Mitigation Steps

Only do the emit when isForeclosed was true

1000 as a constant

Handle

gpersoon

Vulnerability details

Impact

A value of 1000 is used to indicate 100%. This value is hardcoded on several places.
It's saver to use a constant, to prevent mistakes in future updates.

Proof of Concept

.\RCFactory.sol: /// @dev in basis points (so 1000 = 100%)
.\RCFactory.sol: 1000,
.\RCMarket.sol: (((uint256(1000) - artistCut) - creatorCut) - affiliateCut) -
.\RCMarket.sol: ((((uint256(1000) - artistCut) - affiliateCut) - cardAffiliateCut) -
.\RCMarket.sol: _winningsToTransfer = (totalRentCollected * winnerCut) / (1000);
.\RCMarket.sol: (1000);
.\RCMarket.sol: _remainingPot = (totalRentCollected * _remainingCut) / (1000);
.\RCMarket.sol: ((uint256(1000) - artistCut) - affiliateCut) - cardAffiliateCut;
.\RCMarket.sol: (_rentCollected * _remainingCut) / (1000);
.\RCMarket.sol: (rentCollectedPerCard[_card] * cardAffiliateCut) / (1000);
.\RCMarket.sol: uint256 _payment = (totalRentCollected * _cut) / (1000);

Tools Used

grep

Recommended Mitigation Steps

Replace 1000 with a constant.

event WithdrawnBatch is not used

Handle

pauliax

Vulnerability details

Impact

event WithdrawnBatch in contract RCNftHubL2 is not used anywhere.

Recommended Mitigation Steps

Remove or use it where intended.

Use immutable keyword

Handle

gpersoon

Vulnerability details

Impact

Several variables are only set once. So it might be useful to make them immutable to reduced the change of accidental updates
See the "proof of concept" for examples.

Proof of Concept

// https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol
uint256 public numberOfCards;
uint256 public totalNftMintCount;
IRCTreasury public treasury;
IRCFactory public factory;
IRCNftHubL2 public nfthub;
IRCOrderbook public orderbook;
uint256 public minimumPriceIncreasePercent;
uint256 public minRentalDayDivisor;
uint256 public maxRentIterations;
uint32 public marketOpeningTime;
uint32 public override marketLockingTime;
uint32 public oracleResolutionTime;
address public artistAddress;
address public affiliateAddress;
address public marketCreatorAddress;
uint256 public creatorCut;
address[] public cardAffiliateAddresses;
address public arbitrator;
IRealitio public realitio;

//https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCFactory.sol#L113
IRCTreasury public override treasury;

Tools Used

Recommended Mitigation Steps

Use the immutable keyword where possible

contract RCTreasury does not use nfthub and setNftHubAddress

Handle

pauliax

Vulnerability details

Impact

contract RCTreasury has an unused storage variable nfthub and setNftHubAddress function. This variable was moved to the Factory contract so it is useless here.

Recommended Mitigation Steps

Remove nfthub variable and function setNftHubAddress.

Uninitialized Local Variables found in RCOrderbook.sol

Handle

maplesyrup

Vulnerability details

Impact

2 - Medium Risk

  • Possible accidental loss of funds if variables do not contain the right information such as correct addresses in this specific scenario.

Proof of Concept

According to Slither documentation (https://github.com/crytic/slither/wiki/Detector-Documentation#configuration-32), uninitialized local variables can cause the risk of loss of funds due to inappropriate usage of these variables while using the contract. All variables must be initialized to insure they do not run the risk of incorrect calculations or sending funds to a 0x0 address.

It is recommended that all variables need to be initialized. If the variable needs to be 0, then it is best to explicitly assign 0 to the variable.

The following local variables in RCOrderbook are not initialized:

RCOrderbook.getBid(address,address,uint256)._newBid <--- is a local variable never initialized

(contracts/RCOrderbook.sol line(s)#818)


RCOrderbook.removeOldBids(address)._cardCount <--- is a local variable never initialized

(contracts/RCOrderbook.sol line(s)#676)


RCOrderbook._newBidInOrderbook(address,address,uint256,uint256,uint256,RCOrderbook.Bid)._newBid <--- is a local variable never initialized

(contracts/RCOrderbook.sol line(s)#301)


RCOrderbook.addMarket(address,uint256,uint256).i <--- is a local variable never initialized

(contracts/RCOrderbook.sol line(s)#166)


RCOrderbook.removeOldBids(address)._loopCounter <--- is a local variable never initialized

(contracts/RCOrderbook.sol line(s)#677)


RCOrderbook.cleanWastePile().i <--- is a local variable never initialized

(contracts/RCOrderbook.sol line(s)#717)


RCOrderbook.closeMarket()._newBid <--- is a local variable never initialized

(contracts/RCOrderbook.sol line(s)#660)


RCOrderbook.addMarket(address,uint256,uint256)._newBid <--- is a local variable never initialized

(contracts/RCOrderbook.sol line(s)#168)


Console output (Slither log):

INFO:Detectors:
RCOrderbook.removeOldBids(address)._loopCounter (contracts/RCOrderbook.sol#677) is a local variable never initialized
RCMarket.lockMarket().i (contracts/RCMarket.sol#453) is a local variable never initialized
RCOrderbook.cleanWastePile().i (contracts/RCOrderbook.sol#717) is a local variable never initialized
RCOrderbook.getBid(address,address,uint256)._newBid (contracts/RCOrderbook.sol#818) is a local variable never initialized
RCOrderbook.addMarket(address,uint256,uint256)._newBid (contracts/RCOrderbook.sol#168) is a local variable never initialized
RCOrderbook.addMarket(address,uint256,uint256).i (contracts/RCOrderbook.sol#166) is a local variable never initialized
RCNftHubL2.deposit(address,bytes).i (contracts/nfthubs/RCNftHubL2.sol#150) is a local variable never initialized
RCOrderbook._newBidInOrderbook(address,address,uint256,uint256,uint256,RCOrderbook.Bid)._newBid (contracts/RCOrderbook.sol#301) is a local variable never initialized
RCOrderbook.removeOldBids(address)._cardCount (contracts/RCOrderbook.sol#676) is a local variable never initialized
RCOrderbook.closeMarket()._newBid (contracts/RCOrderbook.sol#660) is a local variable never initialized
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#uninitialized-local-variables

Tools Used

Solidity Compiler 0.8.4
Hardhat v2.3.3
Slither v0.8.0

Compiled, Tested, and Deployed contracts on a local Hardhat network

Ran Slither-analyzer for further detecting and testing.

Recommended Mitigation Steps

(Worked best under a python virtual environment)

  1. Clone project repository
  2. Run project against hardhat network;
    compile and run default test on contracts.
  3. Installed sliither analyzer:
    https://github.com/crytic/slither
  4. Ran [$ slither .] against all contracts

RCTreasury.updateRentalRate()._increaseMarketBalance() adjusts user's deposit in storage but does not trigger corresponding event LogAdjustDeposit()

Handle

jvaqa

Vulnerability details

RCTreasury.updateRentalRate()._increaseMarketBalance() adjusts user's deposit in storage but does not trigger corresponding event LogAdjustDeposit()

Impact

The event LogAdjustDeposit() is supposed to be triggered every time that a user's deposit is either increased or decreased. [1]

However, RCTreasury.updateRentalRate() calls _increaseMarketBalance(), but does not then trigger a corresponding LogAdjustDeposit() event. [2]

This means that any event listeners will calculate incorrect deposit balances when summing all emitted events.

Proof of Concept

Alice can have RCOrderbook call RCTreasury.updateRentalRate() when it is true that:
_timeOwnershipChanged < user[_newOwner].lastRentCalc

When this is the case, RCTreasury.updateRentalRate() will call _increaseMarketBalance(), but will not emit an LogAdjustDeposit() event.

Recommended Mitigation Steps

Solution 1:

Change this:

_increaseMarketBalance(_additionalRentOwed, _newOwner); // [3]

To this:

_increaseMarketBalance(_additionalRentOwed, _newOwner);
emit LogAdjustDeposit(_newOwner, _additionalRentOwed, false);

Solution 2:

Add a LogAdjustDeposit event inside of _increaseMarketBalance() function while also removing the LogAdjustDeposit in the other place that _increaseMarketBalance is called, which is RCTreasury.collectRentUser() // [4]

[1] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L93

[2] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L562

[3] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L562

[4] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L743

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.