Platform: Code4rena
Start Date: 30/03/2022
Pot Size: $30,000 USDC
Total HM: 21
Participants: 38
Period: 3 days
Judge: Michael De Luca
Total Solo HM: 10
Id: 104
League: ETH
Rank: 1/38
Findings: 8
Award: $4,160.27
🌟 Selected for report: 2
🚀 Solo Findings: 2
🌟 Selected for report: hickuphh3
Also found by: 0xDjango, kirk-baird, leastwood, m9800, minhquanym, pedroais
203.7202 USDC - $203.72
The transfer()
function is used to send royalty assets to the splitter contract and its recipients. If the vault operates on non-standard ERC20 tokens, its possible for transfers to not revert upon failure. Similarly, transferFrom()
is used to pull funds from the user when minting an NFT in CoreCollection.mintToken()
. The ERC721Payable._handlePayment()
function does not check the return value of payableToken.transferFrom()
. As a result, if this call does not revert and instead returns false upon a failed transfer, the mintToken()
function will continue to execute successfully. Therefore, users may be able to mint NFTs for free.
Consider integrating SafeERC20
in all areas of Joyn's code to ensure return values for transfer()
and transferFrom()
calls are handled correctly.
#0 - itsmetechjay
2022-04-04T14:18:26Z
Leastwood provided this information to me via DM as he had issues with the website upon submission and Sock told him that I would handle the submissions on his behalf.
#1 - sofianeOuafir
2022-04-14T19:04:14Z
In my opinion, the severity level should be 3 (High Risk) instead of 2 (Med Risk) duplicate of #52
103.9584 USDC - $103.96
https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L149-L169 https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L112-L142
The incrementWindow()
function is used to notify the splitter contract whenever funds are sent to the contract. Users who belong to the merkle tree are able to claim their set percentage allocation on each new window. However, it is possible to spoof a call to incrementWindow()
such that the two requires are satisfied as follows:
IRoyaltyVault(msg.sender).supportsInterface(IID_IROYALTY)
IRoyaltyVault(msg.sender).getSplitter()
As a result, the current access control is insufficient to deter users from generating an infinite number of windows and claiming on those windows to drain the contract of its balance.
Consider implementing proper access control such that msg.sender
must be some preset RoyaltyVault.sol
contract.
#0 - sofianeOuafir
2022-04-14T20:45:50Z
duplicate of #3
🌟 Selected for report: hyh
Also found by: Ruhum, WatchPug, hubble, kirk-baird, leastwood, pedroais, rayn, saian, securerodd
70.1719 USDC - $70.17
The initialize()
function is called by CoreFactory.sol
when creating projects or adding collections to an existing project. When ownership of the CoreCollection.sol
contract is transferred to the project owner, it gives the owner access to a subset of functions, including the initialize()
function.
This function does not implement proper access control to prevent reinitialization. As such, an owner could rug its own collection by reinitializing with a mint fee of zero and a different maxSupply
, allowing them to potentially a duplicate NFT as the tokenId
is calculated by the following equation:
tokenId = ((startingIndex + totalSupply()) % maxSupply) + 1
Prevent collection owners from reinitializing the CoreCollection.sol
contract by making use of OpenZeppelin's Initializable.sol
contract.
#0 - sofianeOuafir
2022-04-14T19:54:55Z
In my opinion, the severity level should be 3 (High Risk) instead of 2 (Med Risk)
duplicate of #4
🌟 Selected for report: kirk-baird
Also found by: 0xDjango, Dravee, Ruhum, TomFrenchBlockchain, WatchPug, defsec, hubble, hyh, leastwood, minhquanym
85.0569 USDC - $85.06
https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L185-L195 https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L296-L309
Joyn allows project owners to configure their own royalty vault or give the SplitFactory.sol
contract the ability to do this when the Splitter.sol
and RoyaltyVault.sol
contracts are deployed. Because there is an option for the owner to control the royaltyVault
address stored in CoreCollection.sol
, it creates a degree of centralisation risk whereby the owner could break the functionality of _beforeTokenTransfer()
.
There are three key ways to break the functionality of _beforeTokenTransfer()
:
setPlatformFee()
and set it to some value greater than 10000
. This will cause sendToSplitter()
to revert.setPlatformFeeRecipient()
and set it to the zero address such that transfers to that address will fail._beforeTokenTransfer()
to always revert.Let's consider the following scenario:
setRoyaltyVault()
.IRoyaltyVault(royaltyVault).getVaultBalance()
and IRoyaltyVault(royaltyVault).sendToSplitter()
revert.Consider preventing project owners from being able to call setRoyaltyVault()
. This will help to reduce some of the centralisation risk that the protocol has.
#0 - sofianeOuafir
2022-04-14T20:49:26Z
In my opinion, the severity level should be 2 (Med Risk) instead of 3 (High Risk)
duplicate of #9
#1 - deluca-mike
2022-04-22T03:27:29Z
See #9 for why I am leaning towards this being High Risk.
🌟 Selected for report: leastwood
2683.3489 USDC - $2,683.35
https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L139-L167 https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/ERC721Payable.sol#L50-L56
The mintToken()
function is called to mint unique tokens from an ERC721
collection. This function will either require users to provide a merkle proof to claim an airdropped token or pay a fee in the form of a payableToken
. However, because the payableToken
is paid before a token is minted, it may be possible to reenter the mintToken()
function if there is a callback attached before or after the token transfer. Because totalSupply()
has not been updated for the new token, a user is able to bypass the totalSupply() + amount <= maxSupply
check. As a result, if the user mints the last token, they can reenter and mint duplicate NFTs as the way tokenId
is generated will wrap around to the start again.
For the sake of this example, let's say startingIndex = 0
and maxSupply = 100
. tokenId
is minted according to ((startingIndex + totalSupply()) % maxSupply) + 1
. If we see that a user mints a token where totalSupply() = maxSupply - 1 = 99
and they reenter the function, then the next token to mint will actually be of index 1
as totalSupply() % maxSupply = 0
. Calculating the first tokenId
, we get ((0 + 0) % maxSupply) + 1 = 1
which is a duplicate of our example.
Consider adding reentrancy protections to prevent users from abusing this behaviour. It may also be useful to follow the checks-effects pattern such that all external/state changing calls are made at the end.
#0 - sofianeOuafir
2022-04-14T22:37:22Z
This is an issue we intend to investigate and fix if indeed it is an issue
#1 - deluca-mike
2022-04-22T05:57:19Z
This is a valid high risk issue. Also, for reference, the checks-effects-interactions (CEI) pattern suggests you, in this order:
🌟 Selected for report: leastwood
805.0047 USDC - $805.00
https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol
The Joyn documentation mentions that Joyn royalty vaults should be equipped to handle revenue generated on a collection's primary and secondary sales. Currently, CoreCollection.sol
allows the collection owner to receive a fee on each token mint, however, there is no existing implementation which allows the owner of a collection to receive fees on secondary sales.
After discussion with the Joyn team, it appears that this will be gathered from Opensea which does not have an on-chain royalty mechanism. As such, each collection will need to be added manually on Opensea, introducing further centralisation risk. It is also possible for users to avoid paying the secondary fee by using other marketplaces such as Foundation.
Consider implementing the necessary functionality to allow for the collection of fees through an on-chain mechanism. ERC2981
outlines the approiate behaviour for this.
#0 - sofianeOuafir
2022-04-14T22:27:50Z
This is a great observation. Something we are aware of and intend to fix as well. 👍
🌟 Selected for report: kirk-baird
146.7121 USDC - $146.71
https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol#L31-L61 https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L149-L169
The RoyaltyVault.sol
contract interacts with the Splitter.sol
to send accumulated royalties to the collection's respective recipients. The sendToSplitter()
function will query the balance of the royalty asset and send the amount after fee deductions to the splitter contract. However, the recipient of these funds expects the entire amount as Splitter.incrementWindow()
is called on this amount. If fee-on-tokens or any rebasing token is used here, then the splitter contract will receive less than the sent amount. As a result, some users will not be able to claim their entire proportion of the royalties.
Consider taking a snapshot of the token balance before and after the transfer and treat the difference between these two amounts as the received token amount. This will be compatible with all types of tokens and avoid any issues where users are unable to withdraw their entire deposited amount.
#0 - itsmetechjay
2022-04-04T14:13:44Z
Leastwood provided this information to me via DM as he had issues with the website upon submission and Sock told him that I would handle the submissions on his behalf.
#1 - sofianeOuafir
2022-04-14T22:29:02Z
duplicate of #130
#2 - deluca-mike
2022-04-22T06:08:03Z
Actually a duplicate of #43
62.3005 USDC - $62.30
The Joyn protocol allows whitelisted users to claim NFTs under a token airdrop. The whitelisted user will call mintToken()
with a specific amount and a valid merkle proof. In the event isForSale
is set to true and some users have not claimed their airdrop, it is possible for all the available tokens to be bought by non-whitelisted users. As a result, any user who was meant to be able to claim an airdropped token with inevitably miss out on their opportunity as the collection is bought out before they can react.
Consider allocating a portion of available NFTs to whitelisted users to prevent behaviour where users buy out a collection before airdropped users are able to claim their tokens.
#0 - sofianeOuafir
2022-04-14T23:10:09Z
The issue brought up is not an issue. It has been designed with the intention described in this issue 👍
#1 - deluca-mike
2022-04-22T06:04:51Z
Agreed, not an issue. Converting this into a QA Report.