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: 17/38
Findings: 4
Award: $440.16
🌟 Selected for report: 0
🚀 Solo Findings: 0
103.9584 USDC - $103.96
Judge has assessed an item in Issue #63 as High risk. The relevant finding follows:
Function Splitter:incrementWindow is executed by vault:sendToSplitter transfered tokens is pushed into balanceForWindow array and window is incremented, but the function can be executed by any contract and more than allowed share of tokens can be transfered by whitelisted users
Proof of concept bytes4 public constant IID_IROYALTY = type(IRoyaltyVault).interfaceId;
function incrementWindow(uint256 royaltyAmount) public returns (bool) { uint256 wethBalance;
require( IRoyaltyVault(msg.sender).supportsInterface(IID_IROYALTY), "Royalty Vault not supported" ); require( IRoyaltyVault(msg.sender).getSplitter() == address(this), "Unauthorised to increment window" ); wethBalance = IERC20(splitAsset).balanceOf(address(this)); require(wethBalance >= royaltyAmount, "Insufficient funds"); require(royaltyAmount > 0, "No additional funds for window"); balanceForWindow.push(royaltyAmount); currentWindow += 1; emit WindowIncremented(currentWindow, royaltyAmount); return true;
} The function can be executed by any contract with function supportsInterface and getSplitter that returns splitter address
royaltyAmount equivalent to current unclaimed balance can be added to window and the window can be incremented to add windows
claimForAllWindows can be executed by whitelisted user to transfer percent allocated tokens from each new window
Mitigation A storage variable with tokens added to the window can be added, and total-windowBalance can be added to next window, and when token is transfered windowBalance can be decremented
🌟 Selected for report: hyh
Also found by: Ruhum, WatchPug, hubble, kirk-baird, leastwood, pedroais, rayn, saian, securerodd
70.1719 USDC - $70.17
Judge has assessed an item in Issue #63 as High risk. The relevant finding follows:
Function CoreCollection:initialize can be executed by owner after initialisation and state variables like mintFee, maxSupply can be changed to increase/decrease fee and supply, isForSale can be set to false to stop token sale
Proof of concept https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/core-contracts/contracts/CoreCollection.sol#L78-L97
Mitigation require statement can be added to revert if function is already initialized
70.2462 USDC - $70.25
CoreCollection:withdraw
will failIn function CoreCollection
mintfee is transfered to same address if vault is not initialized,
these tokens can be withdrawn by owner using withdraw
function, but transferFrom function will fail due to lack of allowance
function withdraw() external onlyOwner { uint256 amount = payableToken.balanceOf(address(this)); payableToken.transferFrom(address(this), msg.sender, amount); emit NewWithdrawal(msg.sender, amount); }
transferFrom
can be replaced with transfer
Function Splitter:incrementWindow
is executed by vault:sendToSplitter
transfered tokens is pushed into balanceForWindow
array and window is incremented, but the function can be executed by any contract and more than allowed share of tokens can be transfered by whitelisted users
bytes4 public constant IID_IROYALTY = type(IRoyaltyVault).interfaceId; function incrementWindow(uint256 royaltyAmount) public returns (bool) { uint256 wethBalance; require( IRoyaltyVault(msg.sender).supportsInterface(IID_IROYALTY), "Royalty Vault not supported" ); require( IRoyaltyVault(msg.sender).getSplitter() == address(this), "Unauthorised to increment window" ); wethBalance = IERC20(splitAsset).balanceOf(address(this)); require(wethBalance >= royaltyAmount, "Insufficient funds"); require(royaltyAmount > 0, "No additional funds for window"); balanceForWindow.push(royaltyAmount); currentWindow += 1; emit WindowIncremented(currentWindow, royaltyAmount); return true; }
The function can be executed by any contract with function supportsInterface
and getSplitter
that returns splitter address
royaltyAmount equivalent to current unclaimed balance can be added to window and the window can be incremented to add windows
claimForAllWindows
can be executed by whitelisted user to transfer percent allocated tokens from each new window
A storage variable with tokens added to the window can be added, and total-windowBalance
can be added to next window, and when token is transfered windowBalance can be decremented
CoreCollection
can be re-initialized by ownerFunction CoreCollection:initialize
can be executed by owner after initialisation and state variables like mintFee, maxSupply can be changed to increase/decrease fee and supply, isForSale can be set to false to stop token sale
require statement can be added to revert if function is already initialized
Imported file is not used in the contract and can be removed
import statement can be removed
Input validation is absent for address variables which may result in re-deployment if address is wrong in constructor or value transfer to wrong address, it is recommended to add validation statements in all address inputs especially in value transfers and immutable variable initialisation.
Add address validation statement
Some functions in the code are not commented or missing parts of comments. Adding comments can improve readability of the code
missing return comment https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/core-contracts/contracts/CoreFactory.sol#L108
missing param comment https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/splits/contracts/SplitFactory.sol#L57
Comments can be added to the functions
In function RoyaltyVault:sentToSplitter
external function return value boolean is compared with boolean constant. The return value can be directly used in the require statement
The return value of the function can be directly used in the require statement
Code can be structed in order type declarations, state variables, events and functions which can improve readability Refer: https://docs.soliditylang.org/en/v0.8.10/style-guide.html#order-of-layout
https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreFactory.sol
#0 - sofianeOuafir
2022-04-15T16:09:43Z
high quality report
#1 - deluca-mike
2022-04-22T09:05:03Z
Second point became #142
#2 - deluca-mike
2022-04-22T09:10:20Z
Third point became #143
195.7837 USDC - $195.78
In function CoreFactory:createProject
, collection creation statements in for loop can be replaced by function addCollection
for (uint256 i; i < _collections.length; i++) { Collection memory _collection = _collections[i]; address coreCollection = _createCollection(_collection); if (_collection.claimsMerkleRoot != bytes32(0)) { ICoreCollection(coreCollection).initializeClaims( _collection.claimsMerkleRoot ); } emit NewCollection(_collection.id, coreCollection, _projectId); ICoreCollection(coreCollection).transferOwnership(msg.sender); }
can be replaced by
for (uint256 i; i < _collections.length; i++) { Collection memory _collection = _collections[i]; addCollection(_projectId, _collection); }
collection create statements inside for loop can be replaced by addCollection
function
>
can be replaced with !=
!=
is more efficient than >
for unsigned integers in require statements with optimizer enabled
Revert strings take a min of 32 bytes, shortening revert strings to 32 bytes can decrease deployment cost as well as runtime cost when revert condition fails
reduce size of revert strings to 32 bytes or use custom errors
pre-increment costs less compared to post-increment, and unchecked
can be added to the increment as there is no chance of overflow due to the condition statement
Change post-increment to pre-increment
calldata
Function parameters that are read-only can be changed to calldata to avoid copying to memory and save gas
Change parameters from memory to calldata
Storage variables read takes ~100 gas in repeated reads, these variables can be read once stored in local variables
if (mintFee > 0) { _handlePayment(mintFee * amount); }
if (startingIndex == 0) { setStartingIndex(); } tokenId = ((startingIndex + totalSupply()) % maxSupply) + 1; _mint(_to, tokenId);
if ( royaltyVault != address(0) && IRoyaltyVault(royaltyVault).getVaultBalance() > 0 ) { IRoyaltyVault(royaltyVault).sendToSplitter(); }
splitterProxy, platformFeeRecepient, royaltyAsset in
require(splitterProxy != address(0), "Splitter is not set"); uint256 platformShare = (balanceOfVault * platformFee) / 10000; uint256 splitterShare = balanceOfVault - platformShare; require( IERC20(royaltyAsset).transfer(splitterProxy, splitterShare) == true, "Failed to transfer royalty Asset to splitter" ); require( ISplitter(splitterProxy).incrementWindow(splitterShare) == true, "Failed to increment splitter window" ); require( IERC20(royaltyAsset).transfer( platformFeeRecipient, platformShare ) == true, "Failed to transfer royalty Asset to platform fee recipient" ); emit RoyaltySentToSplitter(splitterProxy, splitterShare); emit FeeSentToPlatform(platformFeeRecipient, platformShare);
When function is called multiple times in a same block, return value can be cached and re-used to save gas,
royaltyVaultInitialized
is called twice in _handlePayment
address recipient = royaltyVaultInitialized() ? royaltyVault : address(this); payableToken.transferFrom(msg.sender, recipient, _amount); emit NewPayment(msg.sender, recipient, _amount, royaltyVaultInitialized());
Store return value of royalVaultInitialized() and re-used
In Splitter:incrementWindow
function royalAmount
validation can be placed above to save gas if the call is reverted due to 0 value
require( IRoyaltyVault(msg.sender).supportsInterface(IID_IROYALTY), "Royalty Vault not supported" ); require( IRoyaltyVault(msg.sender).getSplitter() == address(this), "Unauthorised to increment window" ); wethBalance = IERC20(splitAsset).balanceOf(address(this)); require(wethBalance >= royaltyAmount, "Insufficient funds"); require(royaltyAmount > 0, "No additional funds for window"); balanceForWindow.push(royaltyAmount); currentWindow += 1; emit WindowIncremented(currentWindow, royaltyAmount); return true;
didSucceed
in event TransferEth
will always be trueIn Splitter:transferSplitAsset
event parameter didSucceed can be avoided as false value will be reverted
// Try to transfer ETH to the given recipient. didSucceed = IERC20(splitAsset).transfer(to, value); require(didSucceed, "Failed to transfer ETH"); emit TransferETH(to, value, didSucceed);
didSucceed can be removed
attemptETHTransfer
In Splitter
contract private function attemptEthTransfer
is not called and can be removed to reduce bytecode
The function can be removed
#0 - sofianeOuafir
2022-04-15T16:23:20Z
high quality report