Platform: Code4rena
Start Date: 10/06/2021
Pot Size: $45,000 USDC
Total HM: 21
Participants: 12
Period: 7 days
Judge: LSDan
Total Solo HM: 13
Id: 13
League: ETH
Rank: 4/12
Findings: 6
Award: $6,506.06
๐ Selected for report: 5
๐ Solo Findings: 2
a_delamo
Even though the uberOwner
controls what ERC20 to use on RCTreasury
, it is highly recommendable to use TransferHelper or SafeERC20 in order to interact safely with the ERC20.
Editor
#0 - Splidge
2021-06-17T14:48:05Z
Duplicate of #2
#1 - dmvt
2021-07-11T12:37:53Z
duplicate of #2
๐ Selected for report: a_delamo
4340.5261 USDC - $4,340.53
a_delamo
On RCTreasury
, we have the method collectRentUser
. This method is public, so anyone can call it using whatever user and whatever timestamp.
So, calling this method using user = XXXXX
and _timeToCollectTo = type(uint256).max)
, would make isForeclosed[user] = true
.
function collectRentUser(address _user, uint256 _timeToCollectTo) public override returns ( uint256 newTimeLastCollectedOnForeclosure ) { require(!globalPause, "Global pause is enabled"); assert(_timeToCollectTo != 0); if (user[_user].lastRentCalc < _timeToCollectTo) { uint256 rentOwedByUser = rentOwedUser(_user, _timeToCollectTo); if (rentOwedByUser > 0 && rentOwedByUser > user[_user].deposit) { // The User has run out of deposit already. uint256 previousCollectionTime = user[_user].lastRentCalc; /* timeTheirDepsitLasted = timeSinceLastUpdate * (usersDeposit/rentOwed) = (now - previousCollectionTime) * (usersDeposit/rentOwed) */ uint256 timeUsersDepositLasts = ((_timeToCollectTo - previousCollectionTime) * uint256(user[_user].deposit)) / rentOwedByUser; /* Users last collection time = previousCollectionTime + timeTheirDepsitLasted */ rentOwedByUser = uint256(user[_user].deposit); newTimeLastCollectedOnForeclosure = previousCollectionTime + timeUsersDepositLasts; _increaseMarketBalance(rentOwedByUser, _user); user[_user].lastRentCalc = SafeCast.toUint64( newTimeLastCollectedOnForeclosure ); assert(user[_user].deposit == 0); isForeclosed[_user] = true; emit LogUserForeclosed(_user, true); } else { // User has enough deposit to pay rent. _increaseMarketBalance(rentOwedByUser, _user); user[_user].lastRentCalc = SafeCast.toUint64(_timeToCollectTo); } emit LogAdjustDeposit(_user, rentOwedByUser, false); } }
Now, we can do the same for all the users bidding for a specific token.
Finally, I can become the owner of the token by just calling newRental
and using a small price. newRental
will iterate over all the previous bid and will remove them because there are foreclosed.
Editor
collectRentUser
should be private and create a new public method with onlyOrderbook
modifier
#0 - Splidge
2021-06-18T10:42:50Z
I like this.
Although I might change the mitigation steps. I like keeping collectRentUser
available to use, we can call it from our bot and it'll help keep user deposits updated in a timely manner for the frontend. I think I'll just add in
require(_timeToCollectTo <= block.timestamp, "Can't collect future rent")
#1 - mcplums
2021-06-18T13:18:39Z
Yeah this is a real doozie, very happy this one was spotted!! Thanks @a_delamo :)
#2 - Splidge
2021-06-21T13:19:11Z
Fix implemented here
a_delamo
In all three contracts (RCFactory
, RCOrderbook
and RCTreasury
) in order to apply or execute important changes/functionalities, you need to be the uberOwner
.
In order to change the uberOwner
, you would execute the following method:
function changeUberOwner(address _newUberOwner) external { require(msgSender() == uberOwner, "Extremely Verboten"); require(_newUberOwner != address(0)); uberOwner = _newUberOwner; }
If the wrong address would be introduced, it would make the contract unusable.
I would recommend using a two steps process. This way we would protect for human errors.
address public uberOwner; address public proposedUberOwner; function proposeNewUberOwner(address _newUberOwner) external { require(msgSender() == uberOwner, "Extremely Verboten"); require(_newUberOwner != address(0)); proposedUberOwner= _newUberOwner; } function becomeUberOwner() external { require(msgSender() == proposedUberOwner, "Extremely Verboten"); uberOwner = proposedUberOwner; proposedUberOwner = address(0); }
#0 - Splidge
2021-06-17T08:45:14Z
Duplicate of #5
#1 - dmvt
2021-07-11T10:53:59Z
duplicate of #105
๐ Selected for report: a_delamo
1302.1578 USDC - $1,302.16
a_delamo
The method _collectRentAction
contains the following code:
... } else if (!_foreclosed && _limitHit && _marketLocked) { // CASE 4 // didn't foreclose AND // did hit time limit AND // did lock market // THEN refund rent between the earliest event and now if (_cardTimeLimitTimestamp < marketLockingTime) { // time limit hit before market locked _timeOfThisCollection = _cardTimeLimitTimestamp; _newOwner = true; _refundTime = block.timestamp - _cardTimeLimitTimestamp; } else { // market locked before time limit hit _timeOfThisCollection = marketLockingTime; _newOwner = false; _refundTime = block.timestamp - marketLockingTime; } } else if (_foreclosed && !_limitHit && !_marketLocked) { // CASE 5 // did foreclose AND // didn't hit time limit AND // didn't lock market // THEN rent OK, find new owner _timeOfThisCollection = _timeUserForeclosed; _newOwner = true; _refundTime = 0; } else if (_foreclosed && !_limitHit && _marketLocked) { // CASE 6 // did foreclose AND // didn't hit time limit AND // did lock market // THEN if foreclosed first rent ok, otherwise refund after locking if (_timeUserForeclosed < marketLockingTime) { // user foreclosed before market locked _timeOfThisCollection = _timeUserForeclosed; _newOwner = true; _refundTime = 0; } else { // market locked before user foreclosed _timeOfThisCollection = marketLockingTime; _newOwner = false; _refundTime = block.timestamp - marketLockingTime; } } else if (_foreclosed && _limitHit && !_marketLocked) { // CASE 7 // did foreclose AND // did hit time limit AND // didn't lock market // THEN if foreclosed first rent ok, otherwise refund after limit if (_timeUserForeclosed < _cardTimeLimitTimestamp) { // user foreclosed before time limit _timeOfThisCollection = _timeUserForeclosed; _newOwner = true; _refundTime = 0; } else { // time limit hit before user foreclosed _timeOfThisCollection = _cardTimeLimitTimestamp; _newOwner = true; _refundTime = _timeUserForeclosed - _cardTimeLimitTimestamp; } } else { // CASE 8 // did foreclose AND // did hit time limit AND // did lock market // THEN (โฏยฐ็ยฐ)โฏๅฝกโปโโป if ( _timeUserForeclosed <= _cardTimeLimitTimestamp && _timeUserForeclosed < marketLockingTime ) { // user foreclosed first (or at same time as time limit) _timeOfThisCollection = _timeUserForeclosed; _newOwner = true; _refundTime = 0; } else if ( _cardTimeLimitTimestamp < _timeUserForeclosed && _cardTimeLimitTimestamp < marketLockingTime ) { // time limit hit first _timeOfThisCollection = _cardTimeLimitTimestamp; _newOwner = true; _refundTime = _timeUserForeclosed - _cardTimeLimitTimestamp; } else { // market locked first _timeOfThisCollection = marketLockingTime; _newOwner = false; _refundTime = _timeUserForeclosed - marketLockingTime; } ...
On the case 6, instead of doing _refundTime = _timeUserForeclosed - marketLockingTime;
like the following cases, is doing _refundTime = block.timestamp - marketLockingTime;
.
This could lead to funds being drained by the miscalculation.
#0 - mcplums
2021-06-18T13:17:51Z
This is a really great find!!
#1 - Splidge
2021-06-21T13:18:45Z
Fix implemented here
๐ Selected for report: JMukesh
Also found by: 0xRajeev, a_delamo, cmichel, maplesyrup
a_delamo
Some methods are missing validators to verify that address != 0
.
A human error can produce that an invalid address is introduced and this check would help a bit to reduce issues.
// RCOrderbook.sol constructor(address _factoryAddress, address _treasuryAddress) { factoryAddress = _factoryAddress; treasuryAddress = _treasuryAddress; treasury = IRCTreasury(treasuryAddress); uberOwner = msgSender(); }
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
#0 - Splidge
2021-06-17T11:55:27Z
Duplicate of #56
#1 - dmvt
2021-07-11T10:36:31Z
duplicate of #56
a_delamo
NativeMetaTransaction
is using ecrecover directly in order to verify the signature.
I would recommend using ECDSA.sol from openzeppelin as it does some more verifications in order to validate the signature is valid.
Being this such an important thing, any extra verification is needed.
function verify( address signer, MetaTransaction memory metaTx, bytes32 sigR, bytes32 sigS, uint8 sigV ) internal view returns (bool) { require(signer != address(0), "NativeMetaTransaction: INVALID_SIGNER"); //FIXME: Recommend to use ECDSA return signer == ecrecover( toTypedMessageHash(hashMetaTransaction(metaTx)), sigV, sigR, sigS ); }
#0 - Splidge
2021-06-18T10:31:32Z
Duplicate of #66
#1 - dmvt
2021-07-10T14:31:29Z
duplicate of #66
๐ Selected for report: a_delamo
0 USDC - $0.00
a_delamo
On RCOrderbook
, there are duplicated the state variable treasuryAddress
and treasury
address public treasuryAddress; IRCTreasury public treasury;
constructor(address _factoryAddress, address _treasuryAddress) { factoryAddress = _factoryAddress; treasuryAddress = _treasuryAddress; treasury = IRCTreasury(treasuryAddress); uberOwner = msgSender(); }
#0 - Splidge
2021-06-17T12:49:50Z
Fixed alongside other issues here
๐ Selected for report: a_delamo
0 USDC - $0.00
a_delamo
Contracts perform a lot of reads causing to increase the gas consumption. I would recommend reducing the reads and just use storage or memory in order to keep an instance of the value.
Example
function cleanWastePile() internal { uint256 i; while (i < cleaningLoops && user[address(this)].length > 0) { uint256 _pileHeight = user[address(this)].length - 1; if (user[address(this)][_pileHeight].next == address(this)) { user[address(this)].pop(); } else { address _market = user[address(this)][_pileHeight].market; uint256 _card = user[address(this)][_pileHeight].token; address _user = user[address(this)][index[address(this)][_market][_card]] .next; Bid storage _currUser = user[_user][index[_user][_market][_card]]; // extract from linked list address _tempNext = _currUser.next; address _tempPrev = _currUser.prev; user[_tempNext][index[_tempNext][_market][_card]] .prev = _tempPrev; user[_tempPrev][index[_tempPrev][_market][_card]] .next = _tempNext; // overwrite array element uint256 _index = index[_user][_market][_card]; uint256 _lastRecord = user[_user].length - (1); // no point overwriting itself if (_index != _lastRecord) { user[_user][_index] = user[_user][_lastRecord]; } user[_user].pop(); // update the index to help find the record later index[_user][_market][_card] = 0; if (user[_user].length != 0 && _index != _lastRecord) { index[_user][user[_user][_index].market][ user[_user][_index].token ] = _index; } } i++; } }
๐ Selected for report: a_delamo
0 USDC - $0.00
a_delamo
RCMarket
contains the constant variable isMarket
to indicate it is a Market bool public constant override isMarket = true;
.
This is after used in RCFactory
function changeMarketApproval(address _market) external onlyGovernors { require(_market != address(0)); // check it's an RC contract IRCMarket _marketToApprove = IRCMarket(_market); assert(_marketToApprove.isMarket()); isMarketApproved[_market] = !isMarketApproved[_market]; emit LogMarketApproved(_market, isMarketApproved[_market]); }
Why not use mappingOfMarkets
to verify the address is a Market? This would reduce the state space used.
#0 - Splidge
2021-06-17T10:59:51Z
That certainly would be easier ๐
#1 - Splidge
2021-06-21T15:54:56Z
In implementing this I discovered the problem.
The check isMarket()
is mainly used when we deploy a new reference market contract. This checks that it is a market but doesn't add it to the mappingOfMarkets
because it's not actually an active market.
I've changed changeMarketApproval
to use the mapping in this commit but I'm going to leave the constant isMarket in the market mainly for the check in setReferenceContractAddress()