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: 8/38
Findings: 5
Award: $1,257.94
๐ Selected for report: 1
๐ Solo Findings: 1
103.9584 USDC - $103.96
https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L161 https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L162
Splitter.incrementWindow
checks incrementation of royalty against the entire balance of SplitProxy
. Malicious users can thus re-count any not yet withdrawn splitAsset
and include in a new window, and subsequently withdraw more shares than allowed, thus stealing earnings from other users.
Splitter.incrementWindow
should check window incrementation against newly transferred royalty, and reject if the amount is larger than the received amount of tokens. However, the current implementation accidentally checks against the entire balance, thus if any not yet withdrawn tokens exist, malicious users can re-count those tokens, and immediately withdraw a share from those.
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");
To illustrate the attack more clearly, suppose
splitAsset
tokens in SplitProxyThe attack can be executed as follow
incrementWindow
with royaltyAmount = 5
2-1. The two require checks are easily satisfied since RoyaltyVault can do anything attacker intends it to do
2-2. Balance check is satisfied, since balance(5) >= royaltyAmount(5)
2-3. A new window with royaltyAmount = 5
is addedIn practice, it is possible to repeat the attack several times and siphon the entire royalty pool.
vim, ganache-cli
Add a counter to keep track of counted transfers. Function claim
and claimForAllWindows
should decrease this counter, while incrementWindow
should increase this counter. Then upon call to incrementWindow
, totalBalance - counter
is the actual not yet accounted for royalties. Check this value against the passed royaltyAmount
to guard against recounting attacks.
#0 - deluca-mike
2022-04-22T05:21:27Z
While this seems like a unique enough issue, there are some similarities with #21, and I am not familiar enough with the overall system to determine if the overlap is sufficient to be marked as a duplicate or not. Looking for feedback from the sponsor and why they should or should not be duplicates.
Regardless, info in the other issue can be useful to solving the issues in general.
#1 - deluca-mike
2022-06-11T22:27:55Z
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
https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L87
Scarcity and uniqueness arguably the core philosophy of NFTs. This bug allows NFT creators(CoreProxy owners) to re-initialize the CoreProxy contract anytime, effectively breaking all promises on scarcity and uniqueness granted upon contract creation. It also allows changing of other fields such as fees for minting, uri for minted NFTs, thus putting buyers at the mercy of contract owner.
For proxy contracts, the backing implementation often provides an initialize
function to help proxy populate initial state. Since the function is intended for initialization, it is expected to be called just once(just like constructors should only be called once for normal contracts).
However, in the case of CoreCollection.initialize
, no modifiers that check whether the contract has already been initialized is used, thus allowing contract owners to re-initialize the contract states whenever they want.
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; }
Re-initialize allows re-setting several critical fields, including _baseUri
, mineFee
and maxSupply
, just to name a few. This violates the common guideline of NFTs, and should be mitigated before actual contract deployment.
vim, ganache-cli
Add the onlyUnInitialized
modifier to initialize
.
function initialize( ... ) external onlyOwner onlyUninitialized onlyValidSupply(_maxSupply) { ... }
#0 - sofianeOuafir
2022-04-14T19:51:11Z
duplicate of #4
๐ Selected for report: rayn
805.0047 USDC - $805.00
https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L185 https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/ERC721Payable.sol#L50 https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol#L31
Each CoreProxy is allowed to be associated with a RoyaltyVault, the latter which would be responsible for collecting minting fees and distributing to beneficiaries. Potential mismatch between token used in CoreProxy and RoyaltyVault might result in minting tokens being permanently stuck in RoyaltyVault.
Each RoyaltyVault can only handle the royaltyVault.royaltyAsset
token assigned upon creation, if any other kind of tokens are sent to the vault, it would get stuck inside the vault forever.
function sendToSplitter() external override { ... require( IERC20(royaltyAsset).transfer(splitterProxy, splitterShare) == true, "Failed to transfer royalty Asset to splitter" ); ... require( IERC20(royaltyAsset).transfer( platformFeeRecipient, platformShare ) == true, "Failed to transfer royalty Asset to platform fee recipient" ); ... }
Considering that pairing of CoreProxy and RoyaltyVault is not necessarily handled automatically, and can sometimes be manually assigned, and further combined with the fact that once assigned, CoreProxy does not allow modifications of the pairing RoyaltyVault. We can easily conclude that if a CoreProxy is paired with an incompatible RoyaltyVault, the payableToken
minting fees automatically transferred to RoyaltyVault by _handlePayment
will get permanently stuck.
function setRoyaltyVault(address _royaltyVault) external onlyVaultUninitialized { ... royaltyVault = _royaltyVault; ... } function _handlePayment(uint256 _amount) internal { address recipient = royaltyVaultInitialized() ? royaltyVault : address(this); payableToken.transferFrom(msg.sender, recipient, _amount); ... }
vim, ganache-cli
While assigning vaults to CoreProxy, check if payableToken
is the same as royaltyVault.royaltyAsset
function setRoyaltyVault(address _royaltyVault) external onlyVaultUninitialized { require( payableToken == _royaltyVault.royaltyAsset(), "CoreCollection : payableToken must be same as royaltyAsset." ); ... royaltyVault = _royaltyVault; ... }
#0 - deluca-mike
2022-06-11T22:37:38Z
Downgraded to medium because, while a more automated and validatedย way of assigning a compatible royalty vault would prevent this issue, in the current framework you'd need to make a user error (albeit one that is not easy to spot), to lose funds.
244.2546 USDC - $244.25
We list 4 low-critical findings and 1 non-critical finding here:
In summary of recommended security practices, it's better to lock pragma version, use 3rd party libraries for security (e.g. SafeERC20), and follow openzeppelin document for upgradable contract deployment.
Floating pragma may cause unexpected compilation time behaviour and introduce unintended bugs.
https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L2 https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/ProxyVault.sol#L2 https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/SplitProxy.sol#L2
hardhat
Don't use ^
, locking pragma to ensure compiler version. e.g. pragma solidity 0.8.4;
Some ERC20 returns false rather than reverting when transfer fails, should use SafeERC20 to check return value.
https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L93
hardhat
Use SafeERC20: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol
using SafeERC20 for IERC20;
In document: https://github.com/code-423n4/2022-03-joyn#smart-contracts
### CoreProxy.sol (37 sloc) Contract used for deploying instances of CoreCollection. We use the proxies for gas optimization and will allow us to upgrade our users contracts in the future. ### ProxyVault.sol (48 sloc) Contract used for deploying instances of RoyaltyVault. We use the proxies for gas optimization and will allow us to upgrade our users contracts in the future. ### SplitProxy.sol (101 sloc) Contract used for deploying instances of Splitter. We use the proxies for gas optimization and will allow us to upgrade our users contracts in the future.
But in source code, these contracts are not upgradable.
https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreProxy.sol#L16 https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/ProxyVault.sol#L29 https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/SplitProxy.sol#L25
hardhat
Add setter function to set _impl
address, or follow openzeppelin documents (deploy proxies with hardhat): https://docs.openzeppelin.com/upgrades-plugins/1.x/hardhat-upgrades
Since IRoyaltyVault
and ERC165
have no storage, Ownable storage follows right after VaultStorage in both RoyaltyVault and ProxyVault, so everything is fine at the moment. However, it is best to keep the ordering properly sorted out, else any future changes in inherited contracts might unexpectedly cause storage layout mismatch. This is especially crucial if the contracts are intended to be upgradeable as stated in the document.
https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/ProxyVault.sol#L8 https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol#L11
hardhat
Keep the ordering properly sorted out.
It doesn't have any structs/variables to record relationships between projects and collections. Thus any relationship will only be recorded in emitted events. We consider this behaviour unfavorable and urge developers to properly bind the collections to projects.
hardhat
Define a mapping to bind projects and collections.
34.5615 USDC - $34.56
The for loop has no overflow risk of i
. Use an unchecked block to save gas.
Use unchecked
blocks to avoid overflow checks, or use ++i
rather than i++
if you don't use unchecked blocks.
for (uint256 i = 0; i < length; ) { ... unchecked { ++i; } }
platformFeeRecipient
and platformFee
can be declared to private constants for gas savings.
Declare these variables to private.