FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: low
Likelihood: low

`minStake` Has No Upper Bound

Author Revealed upon completion

Root + Impact

Description

initialize checks minStake_ != 0 but places no upper bound. If set to type(uint256).max, every stake() call reverts with BelowMinStake because no token transfer can credibly reach that amount. The pool becomes permanently unusable for staking. This is an accidental footgun: a fat-fingered minStake value at pool creation permanently DOSes staking with no recovery path (there is no setter for minStake).

// ConfidencePool.sol — initialize() line 198
@> if (minStake_ == 0) revert InvalidAmount(); // <-- only checks zero, no upper bound

Risk

Likelihood: Low — requires the pool owner to pass an absurd minStake value at creation. Under the trust model, the owner would not intentionally shoot themselves.

Impact: Low — staking is permanently blocked; the pool becomes a bonus-only vessel. No funds are stolen.

Proof of Concept

File: L13-MinStake-NoUpperBound.poc.t.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
/// @notice PoC for L-13: minStake has no upper bound — type(uint256).max DOSes staking
contract L13_MinStake_NoUpperBound_POC is BaseConfidencePoolTest {
function testPOC_L13_minStake_canBeSetToUint256Max() external {
ConfidencePool freshPool = _deployPoolWithMinStake(type(uint256).max);
// minStake accepts type(uint256).max — no upper bound check exists
// Any stake attempt reverts with BelowMinStake because no transfer can reach this amount
token.mint(alice, 1_000_000 * ONE);
vm.startPrank(alice);
token.approve(address(freshPool), 1_000_000 * ONE);
vm.expectRevert(IConfidencePool.BelowMinStake.selector);
freshPool.stake(1_000_000 * ONE);
vm.stopPrank();
}
function testPOC_L13_normalMinStake_allowsStaking() external {
// Sanity check: with a normal minStake, staking works
assertEq(pool.minStake(), ONE);
_stake(alice, 10 * ONE);
assertEq(pool.eligibleStake(alice), 10 * ONE);
}
function _deployPoolWithMinStake(uint256 minStake_) internal returns (ConfidencePool freshPool) {
ConfidencePool implementation = new ConfidencePool();
freshPool = ConfidencePool(Clones.clone(address(implementation)));
freshPool.initialize(
agreement, address(token), address(safeHarborRegistry),
moderator, block.timestamp + 31 days, minStake_, recovery, address(this), _defaultScope()
);
}
}

Run: forge test --match-path 'L13-MinStake-NoUpperBound.poc.t.sol' -vv

Recommended Mitigation

Add a reasonable upper bound on minStake:

+ uint256 private constant MAX_MIN_STAKE = 1_000_000 * 1e18;
- if (minStake_ == 0) revert InvalidAmount();
+ if (minStake_ == 0 || minStake_ > MAX_MIN_STAKE) revert InvalidAmount();

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!