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: 3/12
Findings: 7
Award: $6,981.24
π Selected for report: 7
π Solo Findings: 3
cmichel
The ERC20.transfer()
and ERC20.transferFrom()
functions return a boolean value indicating success. This parameter needs to be checked for success.
According to the standard the return value must be checked for true
, otherwise the transfer will have failed.
The treasury contract calls ERC20.transferFrom/transfer
four times without checking for the success
return value.
ERC20-compliant tokens that don't actually perform the transfer and return false
are still counted as a correct transfer.
Users can "deposit" and increase their balance this way without actually transfering tokens, and then withdraw the treasury.
We recommend using OpenZeppelinβs SafeERC20
versions with the safeTransfer
and safeTransferFrom
functions that handles the return value check as well as non-standard-compliant tokens.
#0 - Splidge
2021-06-17T11:05:15Z
Duplicate of #2
#1 - dmvt
2021-07-11T12:38:03Z
duplicate of #2
cmichel
When a user deposits to the treasury they first approve the contract and then call its deposit
action which performs an ERC20.transferFrom
.
It's possible for an attacker to frontrun the final deposit
transaction after the user approval and turn the deposit into a sponsor
this way, which is basically a free donation to a market outcome (card).
The attacker can create their own market where they are the only participant betting on a card that is guaranteed to win.
Attack:
M
with a guaranteed winning card. (They can time the market close in a way such that no other user can rent the card longer then them.)Treasury.deposit(amount)
action instead.deposit
from 2 and frontruns it with a sponsor action from their custom market by sending a M.sponsor(A, amount)
transaction.treasury.sponsor(_sponsorAddress=A, _amount)
treasury call: erc20.transferFrom(_sponsor=A, address(this), _amount);
User deposits can be stolen.
The sponsor function should not allow specifying a custom sponsorAddress
.
The market should pull in the amounts from msg.sender
using ERC20.transferFrom(msg.sender, amount)
and then forward this amount to the treasury using a simple ERC20.transfer(treasury, amount)
followed by the treasury.sponsor
call that does the remaining book-keeping.
#0 - Splidge
2021-06-17T11:06:13Z
Duplicate of #40
#1 - dmvt
2021-07-11T12:39:50Z
duplicate of #40
585.971 USDC - $585.97
cmichel
The RCNftHubL2.safeTransferFrom
function does not correctly implement the ERC721 spec:
When using safeTransferFrom, the token contract checks to see that the receiver is an IERC721Receiver, which implies that it knows how to handle ERC721 tokens. ERC721
This check is not implemented, it just drops the _data
argument.
Contracts that don't know how to handle ERC721 tokens (are not an IERC721Receiver
) can accept them but they should not when using safeTransferFrom
according to spec.
Implement the IERC721Receiver
check in safeTransferFrom
.
#0 - Splidge
2021-06-21T15:44:57Z
This has been fixed while working on issue #118 commit here
π Selected for report: cmichel
1302.1578 USDC - $1,302.16
cmichel
The treasury only checks its globalPause
field but does not check its market-specific marketPaused
field for Treasury.sponsor
.
A paused market contract can therefore still deposit as a sponsor using Market.sponsor
The market-specific pause does not work correctly.
Add checks for marketPaused
in the Treasury for sponsor
.
#0 - mcplums
2021-06-17T06:54:32Z
I don't think this is important but I guess it can't hurt to block sponsorship if paused
#1 - Splidge
2021-06-17T12:00:04Z
I'm not sure why this is a severity 2? Maybe it should be lower. Sponsoring a market, whether paused or not, doesn't come with an expectation to receive the funds back. So assets are not at risk here.
#2 - Splidge
2021-06-21T14:57:52Z
There have been changes made to marketPaused
and how markets are created due to other issues that have been found. By default markets are now created in a paused state and it'd be useful to be able to sponsor them before the governors approve them. It's a nice thing for the sponsorship to be in place before anybody interacts with the contract.
I have however made changes such that is the market pause is ever turned on by the Treasury owner then the sponsor function will revert.
Changes here
π Selected for report: cmichel
1302.1578 USDC - $1,302.16
cmichel
There are ERC20 tokens that may make certain customizations to their ERC20 contracts.
One type of these tokens is deflationary tokens that charge a certain fee for every transfer()
or transferFrom()
.
The deposit()
function will introduce unexpected balance inconsistencies when comparing internal asset records with external ERC20 token contracts.
One possible mitigation is to measure the asset change right before and after the asset-transferring routines
#0 - mcplums
2021-06-17T06:38:17Z
I think balancedBooks modifier should handle this?
Of course it means we are unable to use such tokens, but that is ok
#1 - Splidge
2021-06-17T11:49:45Z
oh, trying the same one again..? π https://github.com/code-423n4/2021-05-88mph-findings/issues/16
I'll fight this one though, I'd argue that we are using ERC20 tokens and according to the ERC20 spec for transferFrom:
Transfers
_value
amount of tokens from address_from
to address_to
A deflationary token therefore isn't compliant to ERC20 as it doesn't transfer the full _value
and so it isn't what we are planning to use and not relevant here.
#2 - dmvt
2021-07-10T17:50:57Z
If you plan not to support these tokens it should be very clearly documented. Keep in mind that "we don't support that" still has massive impact on the users involved. See: imBTC / ERC777 on Uniswap v1. The issue is valid and should stand in the audit report, in part so that future users see it.
π Selected for report: cmichel
1302.1578 USDC - $1,302.16
cmichel
The treasury implements a max contract balance check in the deposit
function:
require( (erc20.balanceOf(address(this)) + _amount) <= maxContractBalance, "Limit hit" );
A whale can stop anyone from depositing by front-running a user's deposit with a deposit that pushes the contract balance to the maxContractBalance
limit first.
The user's deposit will then fail in the check.
Afterwards, the whale can withdraw again.
This is not only restricted to whales, miners/users can do the same using same-block cross-transaction flashloans and submitting a
(attacker deposit, user deposit, attacker withdraw)
flashbundle to a miner. Possibilities like this will only become more prevalent in the future.
Any users can be blocked from depositing which prevents them from renting cards. This allows an attacker to manipulate the outcome of a market in their favor by strategically preventing other competitors to bid on their cards (causing forfeiture due to a low deposit balance).
Remove the contract limit or at least set the limit very high if it keeps happening.
#0 - mcplums
2021-06-17T06:32:43Z
This is a good one- but I don't think we need to make any changes to the contract. We can use it as originally intended, then if it is exploited as above, we can switch to only setting the variable to 0 or maxuint256. So it just acts as a toggle on whether deposits are allowed.
79.1061 USDC - $79.11
cmichel
On failure, many functions fail with an assert
which consumes all available gas of the transaction.
A best practice is to only use assert for tautologies and checks that one never expects to be false. (like invariants)
Occurences:
setReferenceContractAddress
Market.initialize
Market._transferCard
Market._payout
Treasury.payout
No gas is refunded to the user if they used any of the functions incorrectly
Use require
instead of assert
.
#0 - Splidge
2021-06-17T11:32:27Z
This covers a bunch of other issues which I already considered to be duplicates. I've marked all of them a duplicate of this one.
#1 - Splidge
2021-06-18T10:24:35Z
This most recent duplicate #83 appears to be the most complete one, however for simplicities sake I'll not go changing them all around right now. It actually considers the compiler version we are using to correctly state that the remining gas isn't consumed, as some other issues incorrectly stated. It also seems to cover every instance in the one report.
#3 - dmvt
2021-07-11T10:26:39Z
duplicate of #83
π Selected for report: JMukesh
Also found by: 0xRajeev, a_delamo, cmichel, maplesyrup
cmichel
Some parameters of functions are not checked for invalid values:
RCNftHubL2.constructor
: address _factoryAddress, address childChainManager
RCTreasury.constructor
: address _tokenAddress
A wrong user input or wallets defaulting to the zero addresses for a missing input can lead to the contract needing to redeploy or wasted gas.
Validate the parameters.
#0 - Splidge
2021-06-17T12:36:34Z
Duplicate of #56
#1 - dmvt
2021-07-11T10:36:27Z
duplicate of #56
π Selected for report: cmichel
434.0526 USDC - $434.05
cmichel
The Factory.createMarket
iterates over all _cardAffiliateAddresses
.
The transactions can fail if the arrays get too big and the transaction would consume more gas than the block limit. This will then result in a denial of service for the desired functionality and break core functionality.
Perform a _cardAffiliateAddresses.length == 0 || _cardAffiliateAddresses.length == tokenUris.length
check in createMarket
instead of silently skipping card affiliate cuts in Market.initialize
.
This would restrict the _cardAffiliateAddresses
length to the nftMintingLimit
as well.
#0 - Splidge
2021-06-21T15:15:57Z
implemented here
π Selected for report: cmichel
434.0526 USDC - $434.05
cmichel
The uberOwner
cannot do the same things the owner can.
They can "only" set the reference contract for the market.
The same ideas apply to Treasury
and Factory
's uberOwner
.
The name is misleading as it sounds like the uber-owner is more powerful than the owner.
Uberowner should at least be able to set the owner if not be allowed to call all functions that an owner
can.
Alternatively, rename the uberOwner
.
#0 - mcplums
2021-06-17T06:34:11Z
I like this! Is not too important, but can't hurt to have uber owner able to change the owner.
#1 - Splidge
2021-06-21T15:42:48Z
I will come back to this issue if time allows. Ownable.sol has been made such that you can't override transferOwnership()
or the onlyOwner
modifier.
This means the next best option would be changing to AccessControl.sol which is more effort than I think the benefit warrants given our current timescale.
π Selected for report: cmichel
434.0526 USDC - $434.05
cmichel
Usually one tries to avoid toggle functions in blockchains, because it could be that you think that the first transaction you sent was not correctly submitted (but it's just pending for a long time), or you might even be unaware that it was already sent if multiple roles can set it (like with changeMarketApproval
/ onlyGovernors
) or if it's an msig.
This results in potentially double-toggling the state, i.e, it is set to the initial value again.
Some example functions: changeMarketCreationGovernorsOnly
, changeMarketApproval
, and the ones that follow.
The outcome of toggle functions is hard to predict on blockchains due to the very async nature and lack of information about pending transactions.
Use functions that accept a specific value as a parameter instead.