As of 3 Nov 2026, CapsuleNFT has 1 audit report on Trustblock, by Quantstamp, dated 18 Apr 2022. That report lists no open critical findings.
Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| CapsuleNFT | Security firm: Quantstamp | Date: | Scope: Files named in the report | No open critical findings | Open · certificate.quantstamp.com |
Statuses are the security firm’s own, from each 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); } ```
Open the reportFunctions 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(); } ```
Open the reportThe 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.
Open the reportThe 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++; } ```
Open the reportSmart contracts will often have `owner` variables to designate the person with special privileges to make modifications to the smart contract.
Open the reportFunctions 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`.
Open the reportAll payable functions using `checkStatus` modifier allow whitelisted users in `mintWhitelist` to send ether: - `mintSimpleCapsule(...)` - `mintSingleERC20Capsule(...)` - `mintSingleERC721Capsule(...)` - `mintMultiERC20Capsule(...)` - `mintMultiERC721Capsule(...)`
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="capsulenft"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
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