A time-boxed security review of the Metalabel protocol was done by pashov, with a focus on the security aspects of the application's implementation.
A smart contract security review can never verify the complete absence of vulnerabilities. This is a time, resource and expertise bound effort where I try to find as many vulnerabilities as possible. I can not guarantee 100% security after the review or even if the review will find any problems with your smart contracts.
Metalabel is a release club protocol. Groups of people with similar interests can gather and drop work together as collaborators. The protocol allows the creators to split the economic rewards that their "metalabel" has received amongst them. It has multiple technical abstractions like:
-
Resources
- Collection - ERC721 contract that mints tokens (records)
- Split - a payment logic contract that has different percentage allocations for different contributors
- Waterfall - a payment logic contract that enforces paying one party a certain amount before paying another party
-
Accounts - for a user to unlock the functionalities of a protocol he needs to register an account (account creation is gated at first, later it becomes permissionless)
-
Node - an ownable abstraction that groups Records and Resources and allows the owner account or a controller address to manage them
-
Engine - a contract for dropping a new Collection, it manages mints, royalties, rendering (
tokenURI) of the ERC721
The protocol is well-tested, as it has 100% code coverage (line, branch, function).
- Account - can create a new Node
- Node owner - can manage nodes (configure collections, their mints, their royalties and price) and add controllers
- Controller - can manage nodes but can't add controllers
- Mint Authority - can mint permissioned sequences
AccountRegistryall methods - callable by anyone to register an account (unlessowneris set)NodeRegistry::createNode- callable by anyone that creates an accountDropEngine::mint- callable by anyone (unlessmintAuthoritiesmapping is set for the sequence)
Q: What in the protocol has value in the market?
A: ERC721 token mints can be paid with ETH, so ETH value and also the ERC721 tokens themselves.
Q: What is the worst thing that can happen to the protocol?
- Node ownership stolen
- An attacker sets himself as mint payments' recipient
- Exploiting the
mintfunctionality so it allows free or unlimited mints
The owner of a group node can set multiple controllers for all other nodes in the group.
The controller of a group node can manage all other nodes in the group.
Controllers can be smart contracts, accounts can be smart contracts as well.
Controller of a node has the same rights as it's owner (apart from adding more controllers).
| Severity | Impact: High | Impact: Medium | Impact: Low |
|---|---|---|---|
| Likelihood: High | Critical | High | Medium |
| Likelihood: Medium | High | Medium | Low |
| Likelihood: Low | Medium | Low | Low |
review commit hash - effe2e89996d6809c5ec6c6da10c663246f91fc1
The following smart contracts were in scope of the audit:
AccountRegistryCollectionCollectionFactoryDropEngineNodeRegistryResourceResourceFactorySplitFactoryWaterfallFactoryinterfaces/**
The following number of issues were found, categorized by their severity:
- High: 0 issues
- Medium: 4 issues
- Low: 2 issues
- Informational: 8 issues
| ID | Title | Severity |
|---|---|---|
| [M-01] | Insufficient input data validation for configureSequence |
Medium |
| [M-02] | Using the transfer function of address payable is discouraged |
Medium |
| [M-03] | Role transfer actions done in a single-step manner are dangerous | Medium |
| [M-04] | Records minted to an address that is a smart contract that can't handle ERC721 tokens will be stuck forever | Medium |
| [L-01] | resolveId will return the 0 ID even if an address does not have an account |
Low |
| [L-02] | Inconsistent tokenData.owner validation |
Low |
| [I-01] | Use a more abstract name for function parameter | Informational |
| [I-02] | NodeData.nodeType is used as an existence check and nothing else |
Informational |
| [I-03] | NatSpec docs are incomplete | Informational |
| [I-04] | Unused import | Informational |
| [I-05] | Copy-pasted comments in WaterfallFactory |
Informational |
| [I-06] | Missing override keyword |
Informational |
| [I-07] | Use a safe pragma statement | Informational |
| [I-08] | Typos and grammatical errors | Informational |
Impact: High, as some of those can result in a DoS or too big of a royalty payment
Likelihood: Low, as it requires a configuration error or a malicious actor
An authorized address for a node can call Collection::configureSequence where most of the input is not validated properly. The _sequence parameter of the method is of type SequenceData which fields are not validated. Missing checks are the following:
sealedBeforeTimestampis bigger thanblock.timestampsealedAfterTimestampis always bigger thansealedBeforeTimestamp- The difference between
sealedBeforeTimestampandsealedAfterTimestampis at least2 daysfor example - The difference between
sealedBeforeTimestampandsealedAfterTimestampis not more than500 daysfor example - The difference between
sealedBeforeTimestampandblock.timestampis not more than10 daysfor example
Also in DropEngine::configureSequence the royaltyBps is not validated that it is not more than 100% (a value of 10000). I suggest you add a lower royaltyBps upper bound.
Add sensible constraints and validations for all user input mentioned above.
Impact: Medium, as sequence won't be usable as mints will revert
Likelihood: Medium, as it happens any time the recipient is a smart contract or a multisig wallet that has a receive function taking up more than 2300 gas
The mint function in DropEngine uses the transfer method of address payable to transfer native asset funds to an address. This address is set by a node owner and is possible to be a smart contract that has a receive or fallback function that takes up more than the 2300 gas which is the limit of transfer. Examples are some smart contract wallets or multi-sig wallets, so usage of transfer is discouraged.
Use a call with value instead of transfer. There is no risk from reentrancy in the mint method as it has a check for the caller to be an EOA. When this is done you can remove the payable keyword from the revenueRecipient variable.
Impact: High, as important protocol functionality would become unusable
Likelihood: Low, as it requires an admin/owner error
This is a common problem where transferring a role or admin rights to a different address can go wrong if this address is wrong and not actually controlled by any user. This is taken into consideration in NodeRegistry where the node ownership transfer is a two-step operation. Not the same approach is used in AccountRegistry though, where the contract inherits from Owned which has a single-step ownership transfer pattern and also the transferAccount logic in it is also using a single-step pattern.
Use a two-step ownership/rights transfer pattern in both the AccountRegistry ownership and in the transferAccount method, you can reuse the approach you used in NodeRegistry.
[M-04] Records minted to an address that is a smart contract that can't handle ERC721 tokens will be stuck forever
Impact: High, as records will be stuck forever
Likelihood: Low, as it requires the engine to allow smart contracts as minters and that contracts should not support handling of ERC721 tokens
Both mintRecord methods in Collection use the _mint method of ERC721 which is missing a check if the recipient is a smart contract that can actually handle ERC721 tokens. If the case is that the recipient can't handle ERC721 tokens then they will be stuck forever. For this particular problem the safe methods were added to the ERC721 standard and Solmate has added the _safeMint method to check handle this problem in a minting context. This is actually not a problem in DropEngine because it allows only EOAs to mint, but since users can freely implement Engines then this is a valid problem.
Prefer using _safeMint over _mint for ERC721 tokens, but do this very carefully, because this opens up a reentrancy attack vector. It's best to add a nonReentrant modifier in the method that is calling _safeMint because of this.
This is error prone and requires all clients of the resolveId function to do checks for != 0, as we can see in NodeRegistry where this method is called. A better and safer approach is to just revert in the resolveId method when the subject does not have an account, then if the client of the method wants to catch the error he can do this.
The mintRecord functionality in Collection allows the caller to mint tokens to any address since the to argument is not validated in any way. The problem is that the getTokenData inherited function has the following check:
require(data.owner != address(0), "NOT_MINTED");which is not actually true, because a token could be minted to the zero address, even due to a mistake. I suggest to add zero-address checks for the to argument in both mintRecord functions in Collection for consistency.
The broadcast and broadcastAndStore methods in ResourceFactory have a waterfall parameter but some other type of resource might be used - for example a Collection or a Split. Rename parameter from waterfall to resource in both methods.
The only thing nodeType is used for in the system is to check if a node exists. This can be done with a simple boolean or if you actually need a "type" then use an enum where the first value is NON_EXISTENT for example.
In almost all methods the NatSpec documentation is incomplete as it is missing parameters and return variables in it. NatSpec documentation is essential for better understanding of the code by developers and auditors and is strongly recommended. Please refer to the NatSpec format and follow the guidelines outlined there.
The IERC721 import in Collection is not used. Remove it from the code.
Almost all methods reference a split instead of a waterfall in the contract, I expect they were copy-pasted from SplitFactory. Replace split with waterfall in all of the comments in WaterfallFactory.
Most methods in contracts that are implemented and inherited from an interface are missing the override keyword. Go through each method and apply the keyword where it is right to do so. Some examples are the mintRecord and royaltyInfo methods.
Always use stable pragma statement to lock the compiler version and to have deterministic compilation to bytecode. Keep in mind 0.8.13 has a bug when using assembly, even though the codebase does not do that.
A number of typos in the codebase that need to be fixed:
AccountTransfered -> AccountTransferred
has be -> has been
Inheritting -> Inheriting
Otherise -> Otherwise
managaeable -> manageable
so long as their is -> as long as there is
entites -> entities
Otherise -> Otherwise
additonal -> additional
seqeuence -> sequence
funcionality -> functionality
reliquish -> relinquish