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: 9/38
Findings: 6
Award: $1,096.81
🌟 Selected for report: 0
🚀 Solo Findings: 0
103.9584 USDC - $103.96
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; }
Given:
supportsInterface
and getSplitter
;incrementWindow()
10 times with the malicious contract with royaltyAmount
= 10,000 USDC;Consider changing to:
function incrementWindow(uint256 royaltyAmount) public returns (bool) { uint256 wethBalance; require( IRoyaltyVault(msg.sender).supportsInterface(IID_IROYALTY), "Royalty Vault not supported" ); IERC20(royaltyAsset).transferFrom(msg.sender, address(this), royaltyAmount); balanceForWindow.push(royaltyAmount); currentWindow += 1; emit WindowIncremented(currentWindow, royaltyAmount); return true; }
#0 - sofianeOuafir
2022-04-14T19:29:36Z
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
function initialize( string memory _collectionName, string memory _collectionSymbol, string memory _collectionURI, uint256 _maxSupply, uint256 _mintFee, address _payableToken, bool _isForSale, address _splitFactory ) external onlyOwner onlyValidSupply(_maxSupply) { _name = _collectionName; _symbol = _collectionSymbol; _baseUri = _collectionURI; maxSupply = _maxSupply; mintFee = _mintFee; payableToken = IERC20(_payableToken); isForSale = _isForSale; splitFactory = _splitFactory; initialized = true; }
initialize()
should only be callable when initialize != true
.
Otherwise, the Owner
can call it again and change all sorts of critical settings of the contract, and cause severe impact on prior users.
maxSupply
and 1 ETH of mint fee is initialized;initialize()
and set _maxSupply
to 99999 and _mintFee
to 1 FAKETOKEN, which is issued by the attacker himself, therefore, no other can mint.Add onlyUnInitialized
to initialize()
:
function initialize( string memory _collectionName, string memory _collectionSymbol, string memory _collectionURI, uint256 _maxSupply, uint256 _mintFee, address _payableToken, bool _isForSale, address _splitFactory ) external onlyOwner onlyUnInitialized onlyValidSupply(_maxSupply) {
#0 - sofianeOuafir
2022-04-14T19:50:04Z
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
function setPlatformFee(uint256 _platformFee) external override onlyOwner { platformFee = _platformFee; emit NewRoyaltyVaultPlatformFee(_platformFee); }
There is no upper bound of _platformFee
.
uint256 platformShare = (balanceOfVault * platformFee) / 10000; uint256 splitterShare = balanceOfVault - platformShare;
If the malicious/compromised owner sets _platformFee
to a number of 10000.
When sendToSplitter()
, the attacker will get all the funds.
Consider introducing a reasonable upper bound for the _platformFee
.
function setPlatformFee(uint256 _platformFee) external override onlyOwner { require(_platformFee <= 5000, "invalid platformFee") // max 50% platformFee = _platformFee; emit NewRoyaltyVaultPlatformFee(_platformFee); }
#0 - sofianeOuafir
2022-04-14T20:35:59Z
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:01:01Z
Agreed, because there is no trivial "upper limit" for a platform fee.
See #9 for why I am leaning High Risk.
724.5042 USDC - $724.50
function withdraw() external onlyOwner { uint256 amount = payableToken.balanceOf(address(this)); payableToken.transferFrom(address(this), msg.sender, amount); emit NewWithdrawal(msg.sender, amount); }
Unlike ERC721, most ERC20 tokens require an allowance when calling transferFrom()
, even when the from == msg.sedner
.
Therefore, in the withdraw()
function, it should be using payableToken.trasnfer(msg.sender, amount);
instead of payableToken.transferFrom()
.
The current implementation will most certainly revert with the error: "ERC20: insufficient allowance" for almost all ERC20 tokens.
Change to: payableToken.trasnfer(msg.sender, amount);
#0 - sofianeOuafir
2022-04-15T12:57:09Z
duplicate of #52
#1 - deluca-mike
2022-06-11T21:58:33Z
Actually a duplicate of #80
61.8874 USDC - $61.89
RoyaltyVaultFactory
function setPlatformFeeRecipient( address _vault, address _platformFeeRecipient ) external { require(_vault != address(0), "Invalid vault"); require( _platformFeeRecipient != address(0), "Invalid platform fee recipient" ); IRoyaltyVault(_vault).setPlatformFeeRecipient(_platformFeeRecipient); } }
function setPlatformFee(address _vault, uint256 _platformFee) external { IRoyaltyVault(_vault).setPlatformFee(_platformFee); }
The attacker (any address) can call setPlatformFee
and change the PlatformFeeRate to 100% and then setPlatformFeeRecipient
to their address and just call sendToSplitter()
on the vault and steal all the funds.
Change to:
function setPlatformFeeRecipient( address _vault, address _platformFeeRecipient ) external onlyOwner{ require(_vault != address(0), "Invalid vault"); require( _platformFeeRecipient != address(0), "Invalid platform fee recipient" ); IRoyaltyVault(_vault).setPlatformFeeRecipient(_platformFeeRecipient); } }
function setPlatformFee(address _vault, uint256 _platformFee) external onlyOwner{ IRoyaltyVault(_vault).setPlatformFee(_platformFee); }
#0 - sofianeOuafir
2022-04-14T22:43:29Z
duplicate of #10
51.2271 USDC - $51.23
[S]: Suggested optimation, save a decent amount of gas without compromising readability;
[M]: Minor optimation, the amount of gas saved is minor, change when you see fit;
[N]: Non-preferred, the amount of gas saved is at cost of readability, only apply when gas saving is a top priority.
Every call to an external contract costs a decent amount of gas. For optimization of gas usage, avoiding unnecessary external call can save gas if possible.
For example:
function createVault(address _splitter, address _royaltyAsset) external returns (address vault) { splitterProxy = _splitter; royaltyAsset = _royaltyAsset; vault = address( new ProxyVault{salt: keccak256(abi.encode(_splitter))}() ); delete splitterProxy; delete royaltyAsset; }
constructor() { royaltyVault = IVaultFactory(msg.sender).royaltyVault(); splitterProxy = IVaultFactory(msg.sender).splitterProxy(); royaltyAsset = IVaultFactory(msg.sender).royaltyAsset(); platformFee = IVaultFactory(msg.sender).platformFee(); platformFeeRecipient = IVaultFactory(msg.sender).platformFeeRecipient(); }
Can be changed to:
constructor(address _royaltyVault, address _splitterProxy, address _royaltyAsset, uint256 _platformFee, address _platformFeeRecipient) { royaltyVault = _royaltyVault; splitterProxy = _splitterProxy; royaltyAsset = _royaltyAsset; platformFee = _platformFee; platformFeeRecipient = _platformFeeRecipient; }
function createVault(address _splitter, address _royaltyAsset) external returns (address vault) { splitterProxy = _splitter; royaltyAsset = _royaltyAsset; vault = address( new ProxyVault{salt: keccak256(abi.encode(_splitter))}(royaltyVault, splitterProxy, royaltyAsset, platformFee, platformFeeRecipient) ); delete splitterProxy; delete royaltyAsset; }
It can save 5 times of external calls.
uint256
variables to 0
is redundantuint256 amount = 0;
Setting uint256
variables to 0
is redundant as they default to 0
.
Other examples include:
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
Caching the array length in the stack saves around 3 gas per iteration.
Instances include:
CoreFactory.sol#createProject()
Splitter.sol#verifyProof()
++i
is more efficient than i++
Using ++i
is more gas efficient than i++
, especially in for loops.
For example:
for (uint256 i = 0; i < proof.length; i++)
Change to:
for (uint256 i = 0; i < proof.length; +i)
Unused constant variables in contracts increase contract size and gas usage at deployment.
uint256 public constant PERCENTAGE_SCALE = 10e5;
In the contract, the constant variable PERCENTAGE_SCALE
is set once but has never been read, therefore it can be removed.
Every reason string takes at least 32 bytes.
Use short reason strings that fits in 32 bytes or it will become more expensive.
Instances include:
require(amount > 0, "CoreCollection: Amount should be greater than 0");
require( msg.sender == splitFactory || msg.sender == owner(), "CoreCollection: Only Split Factory or owner can initialize vault." );
require( startingIndex == 0, "CoreCollection: Starting index is already set" );
Unused events increase contract size and gas usage at deployment.
event VaultCreated(address vault);
VaultCreated
is unused.
Unused private function increase contract size and gas usage at deployment.
function attemptETHTransfer(address to, uint256 value) private returns (bool) { // Here increase the gas limit a reasonable amount above the default, and try // to send ETH to the recipient. // NOTE: This might allow the recipient to attempt a limited reentrancy attack. (bool success, ) = to.call{value: value, gas: 30000}(""); return success; }
attemptETHTransfer()
is unused.
function amountFromPercent(uint256 amount, uint32 percent) private pure returns (uint256) { // Solidity 0.8.0 lets us do this without SafeMath. return (amount * percent) / 100; }
amountFromPercent()
is unused.
immutable
instead of getter to save gascontract CoreProxy is Ownable { address private immutable _implement; constructor(address _imp) { _implement = _imp; } fallback() external { address _impl = implement(); assembly { let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize()) let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0) let size := returndatasize() returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } default { return(ptr, size) } } } function implement() public view returns (address) { return _implement; } }
Change to:
contract CoreProxy is Ownable { address private immutable _implement; constructor(address _imp) { _implement = _imp; } fallback() external { address _impl = _implement; assembly { let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize()) let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0) let size := returndatasize() returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } default { return(ptr, size) } } } function implement() external view returns (address) { return _implement; } }
#0 - sofianeOuafir
2022-04-15T16:19:46Z
high quality report