Quantstamp reviewed CapsuleNFT on 18 Apr 2022: 16 findings, 0 critical; 9 marked fixed, 7 acknowledged.
Report hosted on certificate.quantstamp.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 2 | 1 |
| Medium | 0 | 0 | 0 |
| Low | 0 | 5 | 8 |
In functions `mintSingleERC20Capsule(...)` and `mintMultiERC20Capsule(...)`, there are no checks to determine whether the pre- and post-deposit ERC20 amounts are the same. This can be an issue when special, deflationary ERC20 contracts burn some percentage of the tokens in every token transfer. Such behaviour in `CapsuleMinter` is contrary to the intent in the whitepaper, which states "the Capsule Protocol ensures that the exact same amount of token deposited is found at the contract post-mint". The code of the function `mintMultiERC20Capsule(...)` is presented below: ``` function mintMultiERC20Capsule( address _capsule, address[] memory _tokens, uint256[] memory _amounts, string memory _uri, address _receiver ) external payable nonReentrant onlyCollectionMinter(_capsule) checkStatus { uint256 tokensLength = _tokens.length; uint256 amountsLength = _amounts.length; require(tokensLength <= TOKEN_TYPE_LIMIT, Errors.INVALID_TOKEN_ARRAY_LENGTH); require(amountsLength <= TOKEN_TYPE_LIMIT, Errors.INVALID_AMOUNT_ARRAY_LENGTH); require(tokensLength == amountsLength, Errors.LENGTH_MISMATCH); // get the current top counter uint256 _id = ICapsule(_capsule).counter(); // Some tokens, like USDT, may have a transfer fee, so we want to record actual transfer amount uint256[] memory _actualAmounts = new uint256[](amountsLength); // loop assumes that the token address and amount is mapped to the same index in both arrays // meaning: the user is sending _amounts[0] of _tokens[0] for (uint256 i; i < tokensLength; i++) { address _token = _tokens[i]; uint256 _amount = _amounts[i]; require(_amount != 0, Errors.INVALID_TOKEN_AMOUNT); require(_token != address(0), Errors.INVALID_TOKEN_ADDRESS); // transfer tokens from caller to contract _actualAmounts[i] = _depositToken(IERC20(_token), _msgSender(), _amount); } // then add user data into the contract (tie Capsule NFT to input): multiERC20Capsule[_capsule][_id].tokenAddresses = _tokens; multiERC20Capsule[_capsule][_id].tokenAmounts = _actualAmounts; // lastly, mint the Capsule NFT ICapsule(_capsule).mint(_receiver, _uri); emit MultiERC20CapsuleMinted(_receiver, _capsule, _tokens, _amounts, _uri); } ``` <br> <br> <br> <br> <br> <br> <br> <br> <br> <br> <br> <br> <br> <br> <br> The code of the function `mintSingleERC20Capsule(...)` (without inline comments) is presented below: ``` function mintSingleERC20Capsule( address _capsule, address _token, uint256 _amount, string memory _uri, address _receiver ) external payable nonReentrant onlyCollectionMinter(_capsule) checkStatus { require(_amount != 0, Errors.INVALID_TOKEN_AMOUNT); require(_token != address(0), Errors.INVALID_TOKEN_ADDRESS); // get the current top counter uint256 id = ICapsule(_capsule).counter(); // transfer tokens from caller to contract uint256 _actualAmount = _depositToken(IERC20(_token), _msgSender(), _amount); // then, add user data into the contract (tie NFT to value): // - set the ID of the Capsule NFT at counter to map to the passed in tokenAddress // - set the ID of the Capsule NFT at counter to map to the passed in tokenAmount singleERC20Capsule[_capsule][id].tokenAddress = _token; singleERC20Capsule[_capsule][id].tokenAmount = _actualAmount; // lastly, mint the Capsule NFT (minted at the current counter (obtained above as id)) ICapsule(_capsule).mint(_receiver, _uri); emit SingleERC20CapsuleMinted(_receiver, _capsule, _token, _amount, _uri); } ```
Functions dedicated to initializing contracts can be initialized by any address, leaving them open to exploitation. Below we reproduce the code from `CapsuleMinter.initialize(...)`, where we notice that the only ckeck is if `_factory != address(0)`. As long as we call this function with the input parameter `_factory != 0` we can make the function run without problems. ``` function initialize(address _factory) external initializer { require(_factory != address(0), Errors.ZERO_ADDRESS); _initializeOwnable(); factory = ICapsuleFactory(_factory); capsuleMintTax = 0.001 ether; } ``` Now, we present the code of the function `CapsuleFactory.initialize(...)`, which also does not present any access control, and can be called by any address. ``` function initialize() external initializer { _initializeOwnable(); capsuleCollectionTax = 0.025 ether; taxCollector = _msgSender(); } ```
The clone-and-own approach involves copying and adjusting open source code at one's own discretion. From the development perspective, it is initially beneficial as it reduces the amount of effort. However, from the security perspective, it involves some risks as the code may not follow the best practices, may contain a security vulnerability, or may include intentionally or unintentionally modified upstream libraries. Rather than the clone-and-own approach, a good industry practice is to use the Truffle framework for managing library dependencies. This eliminates the clone-and-own risks yet allows for following best practices, such as, using libraries.
The function `Capsule.mint(...)` does not validate the input parameters `_account` and `_uri`. Thus, it is possible to mint more than one NFT having the same URI, and it is also possible to mint NFTs for `address(0x0)`. Finally, it is also possible to mint NFTs having empty URIs. The function is reproduced below: ``` function mint(address _account, string memory _uri) external onlyMinter { require(!isCollectionLocked(), 'collection is locked'); _mint(_account, counter); _setTokenURI(counter, _uri); counter++; } ```
Smart contracts will often have `owner` variables to designate the person with special privileges to make modifications to the smart contract.
Functions to manage the `blacklist` are implemented in the codebase, but are not used in the audited contracts. Perhaps, these functions are used in contracts outside the scope of this audit. On the other hand, in this report we indicate that the transfer of Capsule's ownership in function `CapsuleFactory.updateCapsuleCollectionOwner(address _previousOwner, address _newOwner)` could check if the `_newOwner` is contained in the `blacklist`.
All payable functions using `checkStatus` modifier allow whitelisted users in `mintWhitelist` to send ether: - `mintSimpleCapsule(...)` - `mintSingleERC20Capsule(...)` - `mintSingleERC721Capsule(...)` - `mintMultiERC20Capsule(...)` - `mintMultiERC721Capsule(...)`
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (35)
236 projects reviewed · 332 reports · latest Oct 2026
See Quantstamp’s auditsSomething wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us