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

If there no funds in the pool, when the white-hatt hacker tries to claim their bounty, they get nothing but the event is emitted.

Author Revealed upon completion

Root + Impact

Description

  • Due to the misplacement of the emit command, it gets trigguered even if there is no funds to transfer.


function claimAttackerBounty() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (bountyClaimed == bountyEntitlement) revert BountyAlreadyClaimed();
if (!goodFaith) revert InvalidGoodFaithParams();
if (msg.sender != attacker) revert NotAttacker();
if (block.timestamp > corruptedClaimDeadline) revert ClaimWindowExpired();
uint256 remaining = bountyEntitlement - bountyClaimed;
// aderyn-fp-next-line(reentrancy-state-change)
uint256 freeBalance = stakeToken.balanceOf(address(this));
uint256 payout = remaining <= freeBalance ? remaining : freeBalance;
uint256 newBountyClaimed = bountyClaimed + payout;
bountyClaimed = newBountyClaimed;
if (payout > 0) {
corruptedReserve -= payout;
if (!claimsStarted) claimsStarted = true;
stakeToken.safeTransfer(attacker, payout);
}
@> emit AttackerBountyClaimed(attacker, payout, newBountyClaimed, bountyEntitlement);
}

Risk

Likelihood:

  • It is quite likely.

Impact:

  • It just emits an event. It does not make any harm.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {stdStorage, StdStorage} from "forge-std/StdStorage.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
import {MockSafeHarborRegistry} from "test/mocks/MockSafeHarborRegistry.sol";
import {
IERC20Errors
} from "lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol";
import {console} from "forge-std/console.sol";
contract ConfidencePoolContestTest is BaseConfidencePoolTest {
using stdStorage for StdStorage;
event AttackerBountyClaimed(
address indexed attacker,
uint256 amount,
uint256 totalClaimed,
uint256 totalEntitlement
);
function test_claimAttackerBounty_emitsEventEvenWhenPoolIsEmpty() external {
_stake(alice, 5 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(
IAttackRegistry.ContractState.CORRUPTED
);
// Simulating the good faith attacker drained the pool's funds and transferred to the recoveryAddress.
deal(address(token), address(pool), 0);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
uint256 attackerBalanceBefore = token.balanceOf(attacker);
vm.startPrank(attacker);
vm.expectEmit(true, true, true, true);
emit AttackerBountyClaimed(
attacker,
0,
0,
pool.snapshotTotalStaked() + pool.snapshotTotalBonus()
);
pool.claimAttackerBounty();
vm.stopPrank();
uint256 attackerBalanceAfter = token.balanceOf(attacker);
assertEq(attackerBalanceAfter, attackerBalanceBefore);
}
}

Recommended Mitigation

function claimAttackerBounty() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (bountyClaimed == bountyEntitlement) revert BountyAlreadyClaimed();
if (!goodFaith) revert InvalidGoodFaithParams();
if (msg.sender != attacker) revert NotAttacker();
if (block.timestamp > corruptedClaimDeadline) revert ClaimWindowExpired();
uint256 remaining = bountyEntitlement - bountyClaimed;
// aderyn-fp-next-line(reentrancy-state-change)
uint256 freeBalance = stakeToken.balanceOf(address(this));
uint256 payout = remaining <= freeBalance ? remaining : freeBalance;
uint256 newBountyClaimed = bountyClaimed + payout;
bountyClaimed = newBountyClaimed;
if (payout > 0) {
corruptedReserve -= payout;
if (!claimsStarted) claimsStarted = true;
stakeToken.safeTransfer(attacker, payout);
+ emit AttackerBountyClaimed(attacker, payout, newBountyClaimed, bountyEntitlement);
}
- emit AttackerBountyClaimed(attacker, payout, newBountyClaimed, bountyEntitlement);
}

Support

FAQs

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

Give us feedback!