2021-06-realitycards-findings's People
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
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.
- It appears that the loop may be exited on the first cardAffiliateCut = 0 to optimize gas
- Alternatively a local variable may be assigned temporarily and then assigned to state: https://github.com/crytic/slither/wiki/Detector-Documentation#costly-operations-inside-a-loop
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.
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
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.
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
improve readability of 1000000
Handle
gpersoon
Vulnerability details
Impact
The number 1000000 is used in the constructor of RCTreasury.sol. This is difficult to read in a glance.
Solidity allows the use of an underscore ( _ ) to make numbers more readable.
Proof of Concept
https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L114
setMaxContractBalance(1000000 ether); // 1m
Tools Used
Recommended Mitigation Steps
Replace 1000000 with 1_000_000
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
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:
- 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))));
- 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)))
}
- 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
-- 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
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
- Use something like OpenZeppelinโs SafeERC20
- Set up an allow list for tokens, which are knowingly safe
- Consider a different approach to the
balancedBooks
modifier
_realitioAddress not used
Handle
gpersoon
Vulnerability details
Impact
The variable _realitioAddress of RCMarket.sol isn't used. The variable realitio seems to used instead.
Two variables with the same purpose is confusing.
Proof of Concept
https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L121
IRealitio public realitio;
address public _realitioAddress;
Tools Used
Recommended Mitigation Steps
Remove address public _realitioAddress;
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]);
}
}
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
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)
- Clone project repository
- Run project against hardhat network;
compile and run default test on contracts. - Installed sliither analyzer:
https://github.com/crytic/slither - 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)
- Clone Project Repository
- Run Project against Hardhat network;
compile and run default test on contracts. - Installed slither analyzer:
https://github.com/crytic/slither - 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)
- Clone project repository
- Run project against hardhat network;
compile and run default test on contracts. - Installed sliither analyzer:
https://github.com/crytic/slither - 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]
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google โค๏ธ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.