Root + Impact
The ConfidencePool contract inherits ReentrancyGuard but the nonReentrant modifier is not applied to any of the external functions that transfer tokens and update state. The functions claimSurvived, claimExpired, claimCorrupted, claimAttackerBounty, sweepUnclaimedCorrupted, and sweepUnclaimedBonus all perform external safeTransfer calls after modifying state.
The stake token is controlled by the factory owner via an allowlist. If a token with callback behavior (ERC777) or a deliberately malicious token is ever added to the allowlist, an attacker could re-enter the contract during the token transfer and claim a second payout before the first transaction finalizes
function claimSurvived() external nonReentrant {
if (outcome != PoolStates.Outcome.SURVIVED) revert OutcomeNotSet();
if (hasClaimed[msg.sender]) revert InvalidAmount();
uint256 userEligible = eligibleStake[msg.sender];
if (userEligible == 0) revert InvalidAmount();
_clampUserSums(msg.sender);
hasClaimed[msg.sender] = true;
uint256 bonusShare = _bonusShare(msg.sender, userEligible);
uint256 payout = userEligible + bonusShare;
totalEligibleStake -= userEligible;
claimedBonus += bonusShare;
delete eligibleStake[msg.sender];
delete userSumStakeTime[msg.sender];
delete userSumStakeTimeSq[msg.sender];
if (!claimsStarted) claimsStarted = true;
stakeToken.safeTransfer(msg.sender, payout);
emit ClaimSurvived(msg.sender, userEligible, bonusShare);
}
Risk
Likelihood:
· The vulnerability is exploitable only if the stake token implements callback behavior (ERC777) or is explicitly malicious. This is not the default for standard ERC20s.
· However, the factory owner controls the allowlist. A compromised owner or a token that upgrades to add callback behavior could introduce the risk after deployment.
· The attack requires a user to have a claimable balance (SURVIVED or EXPIRED outcome), so the timing window depends on when a pool resolves.
· Given the centralization of the allowlist, I'd rate this as Medium likelihood.
Impact:
· A malicious token could drain the entire pool balance through repeated reentrant claims.
· Honest stakers would lose their principal and bonus to the attacker.
· The protocol's reputation would be damaged if such an exploit occurred.
Proof of Concept
function transfer(address to, uint256 amount) external override returns (bool) {
IConfidencePool(pool).claimSurvived();
return true;
}
Recommended Mitigation
- function claimSurvived() external {
+ function claimSurvived() external nonReentrant {
// ... existing logic ...
}
- function claimExpired() external {
+ function claimExpired() external nonReentrant {
// ... existing logic ...
}
- function claimCorrupted() external {
+ function claimCorrupted() external nonReentrant {
// ... existing logic ...
}
- function claimAttackerBounty() external {
+ function claimAttackerBounty() external nonReentrant {
// ... existing logic ...
}
- function sweepUnclaimedCorrupted() external {
+ function sweepUnclaimedCorrupted() external nonReentrant {
// ... existing logic ...
}
- function sweepUnclaimedBonus() external {
+ function sweepUnclaimedBonus() external nonReentrant {
// ... existing logic ...
}