Camp - NFT

QuantstampCode version: not recorded (see the report)

Quantstamp reviewed Camp (Camp - NFT) on 15 Sep 2025: 12 findings, 0 critical; 10 marked fixed, 2 open.

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

Report hosted on certificate.quantstamp.com.

Trustblock does not review code.

Findings12

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

SeverityOpenAcknowledgedFixed
Critical000
High002
Medium102
Low106
HighPossible Frontrunning of Buy Actions by Changing Token TermsFixed

Token access can be bought via the marketplace according to the conditions outlined in the token `terms`. These terms can be updated by the token owner at any point using the `updateTerms()` function. Despite classical frontrunning being mitigated by the private mempool of the Camp network, this can be exploited by speculatively or accidentally frontrunning a call to `buyAccess()` and changing the subscription price, duration, or payment token. Payment can be made in either native or wrapped wCAMP tokens. In the case of wrapped tokens, the `buyAccess()` function in the marketplace does not check if native tokens were sent as well. If the payment token is changed to use wCAMP before the buy operation is executed, the transaction may consume both native and wrapped assets if the marketplace has sufficient approval for payment in wrapped tokens. The transaction will not revert, and the native tokens are lost in the marketplace. Similarly, the duration of the subscription could be lowered to extract more value per timeframe, or the price updated to the current approval between buyer and marketplace. Finally, when purchasing with ERC-20 tokens, the `_routeTokenPayment()` function transfers tokens directly from the buyer. IP owners may be able to expect incoming buy transactions, and exploit them by calling `updateTerms()` to increase the price to match the buyer's full token approval, effectively stealing all approved tokens rather than just the expected subscription price.

HighTransferred NFTs May Be Missing Royalty VaultFixed

The token owners receive royalty payments for subscriptions, and may also be rewarded for dispute settlements. These rewards are paid to the token owner, but not directly to avoid DoS attack vectors. Instead, each token owner is supposed to have a `RoyaltyVault` contract to receive these funds. These contracts are currently created during the minting process. However, when a token is transferred to a new owner, they may not have a vault registered in the `royaltyVaults` mapping. In this case, royalty payments will be transferred to the zero address, resulting in a loss of funds due to the following code: ``` solidity if (token == address(0)) { SafeTransferLib.safeTransferETH(ipToken.royaltyVaults(recipient), share); } else { SafeTransferLib.safeTransferFrom(token, payer, ipToken.royaltyVaults(recipient), share); } ``` Furthermore, there's no way for the new owner or the contract owner to create and register a vault to solve this issue.

MediumExtreme Term Values Can Have Unintended ConsequencesFixed

Token terms designate the conditions for buying access and minting derivative tokens. The input validation for these values is very lax, therefore extreme values can be assigned with potentially unintended consequences. 1. The price can be set to 0. In this case parents do not receive royalties anymore. This may be exploitable by creating derivative tokens to offer similar content as the original at a much lower price. Similarly, a very high price will make subscriptions impossible. 2. The duration can be set very low, for example 1 second. This would make subscription management near impossible, as access can only be bought for 1 period at a time. 3. Setting the royalty fee to 100% blocks the creation of derivative tokens, as a fee of 0 is invalid and the total must not be higher than 100%. Even other, very high values may make the creation of derivative tokens with multiple parents very challenging. ``` solidity if (newTerms.duration == 0) revert InvalidDuration(); if (newTerms.duration > maxTermDuration) revert InvalidDuration(); if (newTerms.royaltyBps == 0 || newTerms.royaltyBps > LibBPS.DENOMINATOR) revert InvalidRoyalty(); if (newTerms.paymentToken != address(0) && newTerms.paymentToken != wCAMP) revert InvalidPaymentToken(); ```

MediumMaximizing Royalties by Frontrunning Token MintingFixed

Any token can have up to 8 parent tokens, whose owners receive a share of all generated profit. This revenue split is set in stone during the minting process according to the royalty terms of the involved tokens. The `RoyaltyModule` ensures that the total sum of royalties does not exceed 100%. If however the total is less than 100%, the token owner of a listed parent is incentivized to frontrun the minting transaction to maximize their royalty. It may therefore seem reasonable to suggest users to ensure that all relevant royalties add up to 100%, but this may not be desired by the minter to ensure usability for derivative tokens.

LowDeleted and Disputed Tokens Still Receive RoyaltiesFixed

Royalties are paid out when access to associated tokens in purchased. The `_distributeRoyalty()` function is responsible for calculating and distributing the generated profit among the token owner and the parent tokens. Currently, the state of the parent token is not checked, therefore `DELETED` and `DISPUTED` tokens keep earning royalties through derivatives.

LowJudgement Can Be Cast for Nonexistant DisputesFixed

Disputes can be raised by anyone for a token suspected of infringing on intellectual property. The token owner is then given time to assert counterevidence. After some cooldown period, the contract administrator is able to cast judgement on the dispute, deciding either in favor of the initiator or the token owner. However, due to bad input validation and enum default states, the `setDisputeJudgement()` function can be called for any token ID, those without associated dispute and even unminted tokens. ```solidity if (dispute.status != DisputeStatus.Raised && dispute.status != DisputeStatus.Asserted) revert InvalidDisputeStatus(); ``` Further, casting judgement pays out some reward with funds associated with other disputes. In turn, the contract may eventually run out of funds, causing a DoS on judgements and requiring the owner to equalize these losses.

LowMarketplace Can Receive Native Tokens without Ability to RescueFixed

The marketplace contract exposes an empty `receive()` function that is capable of receiving native tokens. However, these tokens are then stuck in the contract, as there is no way to rescue them. Further, the contract does not require a way to receive native tokens without associated action.

LowParent Tagging Is Slower than MintingFixed

The dispute module allows users to accuse token owners of intellectual property infringement, and get the token blocked after judgement by the contract owner. Judgement can be cast after some cooldown period has elapsed, which allows the token owner to assert counterevidence. Disputes should then propagate to derived tokens, this is initiated by users notifying the contract using the `tagChildIp()` function. In its current form, this raises a new dispute for the child token. However, a new derivative tokens could be minted before the cooldown period of this dispute has elapsed, leading to a potential scenario where the dispute module cannot catch up with minting.

LowTokens Can Be Minted with Already Disputed ParentsFixed

During the token minting process, the requirements for parent tokens are checked in the `setParentIpsAndRoyaltyPercents()` function. Currently, this function only reverts if a parent token has been deleted, but not if it has been disputed. Since the `DisputeModule` contract offers functionality to dispute tokens based on disputed parents, it seems reasonable to block such minting actions.

LowVery Small Prices Can Be Rounded to 0Fixed

During the `buyAccess()` operation, royalty calculations are applied to the price. For very low prices, and very low bps the result could be rounded to 0, resulting in empty transfers. ``` solidity uint16 constant DENOMINATOR = 10_000; function applyBps(uint256 amount, uint16 bps) internal pure returns (uint256) { return (amount * bps) / DENOMINATOR; // <-- If amount * bps < DENOMINATOR, this will round to 0 } ```

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.

Part of Camp’s security record

Camp
Audits3 audits by 1 firmLast audit Sep 2025, by Quantstamp.
Critical findingsNo open critical findingsAcross 3 reports.
Open Camp’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