CapsuleNFT

QuantstampFiles named in the report

Quantstamp reviewed CapsuleNFT on 18 Apr 2022: 16 findings, 0 critical; 9 marked fixed, 7 acknowledged.

No open critical findings
Open the reportOpen CapsuleNFT’s security record

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings16

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High021
Medium000
Low058
High`CapsuleMinter` does not check whether pre- and post-deposit ERC20 amounts are the sameAcknowledged

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); } ```

HighAnyone can call `initialize`Acknowledged

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(); } ```

LowClone-and-OwnAcknowledged

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.

LowPossibility of minting more than one NFT having the same `URI`Acknowledged

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++; } ```

LowPrivileged roles and ownershipAcknowledged

Smart contracts will often have `owner` variables to designate the person with special privileges to make modifications to the smart contract.

LowUnused `blacklist` functionalityAcknowledged

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`.

LowUsers in minting whitelist can send EtherAcknowledged

All payable functions using `checkStatus` modifier allow whitelisted users in `mintWhitelist` to send ether: - `mintSimpleCapsule(...)` - `mintSingleERC20Capsule(...)` - `mintSingleERC721Capsule(...)` - `mintMultiERC20Capsule(...)` - `mintMultiERC721Capsule(...)`

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (35)

  • contracts/CapsuleMinterStorage.sol
  • contracts/openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
  • contracts/openzeppelin/contracts/security/ReentrancyGuard.sol
  • contracts/openzeppelin/contracts/token/ERC20/IERC20.sol
  • contracts/CapToken.sol
  • contracts/openzeppelin/contracts/utils/introspection/IERC165.sol
  • contracts/openzeppelin/contracts/utils/introspection/ERC165.sol
  • contracts/CapsuleFactoryStorage.sol
  • contracts/access/Governable.sol
  • contracts/openzeppelin/contracts/utils/structs/EnumerableSet.sol
  • contracts/openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol
  • contracts/openzeppelin/contracts/utils/Strings.sol
  • contracts/openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
  • contracts/Errors.sol
  • contracts/CapsuleFactory.sol
  • contracts/openzeppelin/contracts/token/ERC721/IERC721.sol
  • contracts/CapsuleMinter.sol
  • contracts/openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol
  • contracts/interfaces/IGovernable.sol
  • contracts/openzeppelin/contracts/proxy/utils/Initializable.sol
  • contracts/openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol
  • contracts/openzeppelin/contracts/token/ERC20/ERC20.sol
  • contracts/Capsule.sol
  • contracts/interfaces/ICapsuleMinter.sol
  • contracts/openzeppelin/contracts/utils/Address.sol
  • contracts/interfaces/ICapsule.sol
  • contracts/CapsuleTimelock.sol
  • contracts/openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol
  • contracts/test/ERC20Mock.sol
  • contracts/openzeppelin/contracts/token/ERC721/ERC721.sol
  • contracts/interfaces/ICapsuleFactory.sol
  • contracts/openzeppelin/contracts/access/Ownable.sol
  • contracts/openzeppelin/contracts/utils/Context.sol
  • contracts/test/ERC721Mock.sol
  • contracts/openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol

Part of CapsuleNFT’s security record

CapsuleNFT
Audits1 audit by 1 firmLast audit Apr 2022, by Quantstamp.
Critical findingsNo open critical findingsIn 1 report.
Open CapsuleNFT’s security record

Security firm

Quantstamp

236 projects reviewed · 332 reports · latest Oct 2026

See Quantstamp’s audits

Something 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