Quantstamp reviewed Camp on 7 Aug 2025: 13 findings, 0 critical; 8 marked fixed, 5 open.
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 | 0 | 4 |
| Medium | 0 | 0 | 1 |
| Low | 5 | 0 | 3 |
This fork of the Synthetix `StakingRewards` contract introduces a `unstakeDelay` variable that is intended to enforce a minimum duration between `stake` and `withdraw` operations. For this purpose the contract records the last deposit timestamp for each user. However, the `stake()` function can be executed by a delegator, and does not require a minimum deposit. This way a user can stake a small amount on behalf of another user before their `unstakeDelay` passes, extending the lock and thus blocking them from withdrawing their tokens.
The `batchRelease()` function in `CampTimelockEscrowNativeOFT` processes deposits sequentially in a loop. When processing each deposit, it calls `_bridgeNativeTokens()`, which internally triggers LayerZero's dust removal mechanism via `_removeDust()`. This function rounds down the amount by removing decimal precision dust to ensure compatibility across chains with different token decimals. However, a malicious user can exploit this mechanism by setting a `_minAmount` parameter that becomes incompatible with the dust-adjusted amount. The LayerZero `OFTCore` contract performs a slippage check that reverts if the dust-adjusted amount falls below the user-specified `_minAmount`: ```solidity // In OFTCore._debitView() amountSentLD = _removeDust(_amountLD); amountReceivedLD = amountSentLD; // Check for slippage - this will revert if dust removal reduces amount below minAmount if (amountReceivedLD < _minAmountLD) { revert SlippageExceeded(amountReceivedLD, _minAmountLD); } ``` When this revert occurs during the `batchRelease` loop, it causes the entire transaction to revert. Since deposits are processed sequentially and there's no error handling around individual deposit processing, a single malicious deposit can permanently block the processing of all subsequent deposits in the queue, effectively creating a Denial of Service condition. Another simpler attack could be that `minAmount` is simply larger than the `_originalAmount`, and since there is no validation, it will always fail too. The fix with dust adjusted value should fix both scenarios.
The `bridgeOut()` function acts as the deposit entrypoint to the OFT bridge. It is supposed to receive native tokens, which are later bridged. The amount of tokens to be bridged is provided by the user with the `_originalAmount` parameter, however it is never verified against the actual supplied `msg.value`. This allows users to effectively initiate a bridging operation with debt, which then consumes tokens provided by other depositors. This is particularly exploitable by whitelisted users, who can drain the contract immediately. Other users cannot affect already initiated deposits, and only misuse tokens from those initiated after the malicious one. This could be exploited in combination with frontrunning to steal funds while ensuring that sufficient funds are available for the validations in the `send()` function. Alternatively, creating a deposit with very high debt would effectively DoS the bridge as a whole, as this deposit cannot be skipped in the `batchRelease()` function.
The `send()` operation incurs additional fees exceeding the `_originalAmount` recorded during the initial deposit using the `bridgeOut()` function. This total value is calculated in the `_bridgeNativeTokens()` functions, and then paid from the contract balance. However, there is no validation that this fee is paid during the initial deposit. In its current form, the contract may run out of funds while still having unprocessed queued deposits.
This escrow bridge relies on a queue to manage deposits. All new deposits are appended to the queue and share the same `holdPeriod` before the bridging operation can commence. The queue is checked and consumed using the `batchRelease()` function, which increments a global cursor indicating the cutoff index of processed deposits in the queue. For each batch, some supplied maximum amount of entries is checked, bridging deposits where the `unlockTimestamp` has passed. No deposits in the queue can be skipped, and the `bridgeOut()` function is likely substantially cheaper in gas than the `send()` function and does not enforce a minimum deposit. Therefore, small deposits could be use to extend the size of the queue substantially, requiring someone to pay the gas cost of initiating all those bridging transactions using the `batchRelease()` function before any deposits later in the queue can be executed.
The README document suggests that the contract allows the batched release of matured deposits by a authorized keeper. However, the corresponding `batchRelease()` function is not access controlled, and no `keeper` role is assigned to any user. However, this function may not need to rely on a privileged user for execution, and instead may exploit the natural incentive of queued depositors to ensure smooth bridging operations.
The `CampStaker` contract has an empty `receive()` function, allowing to receive native tokens. However there is no method to withdraw any native tokens if they are sent to the contract.
The `bridgeOut()` function performs unchecked down-casting from `uint256` to `uint128` when storing deposit amounts in the `Deposit` struct. Both `_originalAmount` and `_minAmount` parameters are cast to `uint128` without validation that they fit within the target type's range. ```solidity function bridgeOut(address _to, uint128 _gaslimit, uint256 _originalAmount, uint256 _minAmount) external payable nonReentrant { // ... code ... else { uint256 _totalDeposits = totalDeposits; pending[_totalDeposits].to = _to; pending[_totalDeposits].amount = uint128(_originalAmount); // Unchecked downcast pending[_totalDeposits].minAmount = uint128(_minAmount); // Unchecked downcast // ... rest of function } } ``` If either `_originalAmount` or `_minAmount` exceeds `type(uint128).max` (approximately 3.4 × 10^38), the values will silently wrap around to a much smaller number due to integer overflow. This could result in users unintentionally bridging significantly fewer tokens than intended, leading to a loss of funds without any explicit revert or warning.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (24)
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