Reality Cards contest - a_delamo's results

The world's first 'outcome ownership' prediction market.

General Information

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

Reality Cards

Findings Distribution

Researcher Performance

Rank: 4/12

Findings: 6

Award: $6,506.06

๐ŸŒŸ Selected for report: 5

๐Ÿš€ Solo Findings: 2

Findings Information

๐ŸŒŸ Selected for report: axic

Also found by: JMukesh, a_delamo, cmichel, gpersoon, pauliax, s1m0, shw

Labels

bug
duplicate
3 (High Risk)

Awards

259.5075 USDC - $259.51

External Links

Handle

a_delamo

Vulnerability details

Impact

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

Tools Used

Editor

#0 - Splidge

2021-06-17T14:48:05Z

Duplicate of #2

#1 - dmvt

2021-07-11T12:37:53Z

duplicate of #2

Findings Information

๐ŸŒŸ Selected for report: a_delamo

Labels

bug
3 (High Risk)
sponsor confirmed
resolved

Awards

4340.5261 USDC - $4,340.53

External Links

Handle

a_delamo

Vulnerability details

Impact

On RCTreasury, we have the method collectRentUser. This method is public, so anyone can call it using whatever user and whatever timestamp. So, calling this method using user = XXXXX and _timeToCollectTo = type(uint256).max), would make isForeclosed[user] = true.

function collectRentUser(address _user, uint256 _timeToCollectTo) public override returns ( uint256 newTimeLastCollectedOnForeclosure ) { require(!globalPause, "Global pause is enabled"); assert(_timeToCollectTo != 0); if (user[_user].lastRentCalc < _timeToCollectTo) { uint256 rentOwedByUser = rentOwedUser(_user, _timeToCollectTo); if (rentOwedByUser > 0 && rentOwedByUser > user[_user].deposit) { // The User has run out of deposit already. uint256 previousCollectionTime = user[_user].lastRentCalc; /* timeTheirDepsitLasted = timeSinceLastUpdate * (usersDeposit/rentOwed) = (now - previousCollectionTime) * (usersDeposit/rentOwed) */ uint256 timeUsersDepositLasts = ((_timeToCollectTo - previousCollectionTime) * uint256(user[_user].deposit)) / rentOwedByUser; /* Users last collection time = previousCollectionTime + timeTheirDepsitLasted */ rentOwedByUser = uint256(user[_user].deposit); newTimeLastCollectedOnForeclosure = previousCollectionTime + timeUsersDepositLasts; _increaseMarketBalance(rentOwedByUser, _user); user[_user].lastRentCalc = SafeCast.toUint64( newTimeLastCollectedOnForeclosure ); assert(user[_user].deposit == 0); isForeclosed[_user] = true; emit LogUserForeclosed(_user, true); } else { // User has enough deposit to pay rent. _increaseMarketBalance(rentOwedByUser, _user); user[_user].lastRentCalc = SafeCast.toUint64(_timeToCollectTo); } emit LogAdjustDeposit(_user, rentOwedByUser, false); } }

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

Tools Used

Editor

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

Findings Information

๐ŸŒŸ Selected for report: 0xRajeev

Also found by: a_delamo, gpersoon

Labels

bug
duplicate
2 (Med Risk)

Awards

351.5826 USDC - $351.58

External Links

Handle

a_delamo

Vulnerability details

Impact

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

Findings Information

๐ŸŒŸ Selected for report: a_delamo

Labels

bug
2 (Med Risk)
sponsor confirmed
resolved

Awards

1302.1578 USDC - $1,302.16

External Links

Handle

a_delamo

Vulnerability details

Impact

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

Findings Information

๐ŸŒŸ Selected for report: JMukesh

Also found by: 0xRajeev, a_delamo, cmichel, maplesyrup

Labels

bug
duplicate
1 (Low Risk)

Awards

56.9564 USDC - $56.96

External Links

Handle

a_delamo

Vulnerability details

Impact

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(); }

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

#0 - Splidge

2021-06-17T11:55:27Z

Duplicate of #56

#1 - dmvt

2021-07-11T10:36:31Z

duplicate of #56

Findings Information

๐ŸŒŸ Selected for report: 0xRajeev

Also found by: a_delamo

Labels

bug
duplicate
1 (Low Risk)

Awards

195.3237 USDC - $195.32

External Links

Handle

a_delamo

Vulnerability details

Impact

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

Findings Information

๐ŸŒŸ Selected for report: a_delamo

Labels

bug
G (Gas Optimization)
sponsor confirmed
resolved

Awards

0 USDC - $0.00

External Links

Handle

a_delamo

Vulnerability details

Impact

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

Findings Information

๐ŸŒŸ Selected for report: a_delamo

Labels

bug
G (Gas Optimization)
sponsor acknowledged

Awards

0 USDC - $0.00

External Links

Handle

a_delamo

Vulnerability details

Impact

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++; } }

Findings Information

๐ŸŒŸ Selected for report: a_delamo

Labels

bug
G (Gas Optimization)
sponsor confirmed
resolved

Awards

0 USDC - $0.00

External Links

Handle

a_delamo

Vulnerability details

Impact

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()

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax ยฉ 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter