Root + Impact
Description
**Description:**
firstly,
- The tokens to be awarded if a `scope` is breached legally is for The `attacker`.
- if the `attacker` breached illegally, for his punishment the funds are taken to `recoveryAddress`.
This will break `stakers` TRUST on the bet, it's bad for the BUSINESS.
This will cause no `staker` to stake, but only `sponsor/poolOwner` will contribute, then no difference with the traditional bounty system, but only services/deployment charges.
This function can be called later in `ConfidencePool.sol` to sweep `stakers` funds, even thou the `pool` isn't really breached, cheating on `stakers` and will break TRUST.
```solidity
@>
function claimCorrupted() external nonReentrant {
if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
if (goodFaith && bountyClaimed < bountyEntitlement) revert MustClaimBountyFirst();
@> uint256 toSweep = stakeToken.balanceOf(address(this));
if (toSweep == 0) revert NothingToSweep();
corruptedReserve = toSweep <= corruptedReserve ? corruptedReserve - toSweep : 0;
if (!goodFaith) {
bountyClaimed = bountyEntitlement;
}
if (!claimsStarted) claimsStarted = true;
@> stakeToken.safeTransfer(recoveryAddress, toSweep);
emit ClaimCorrupted(msg.sender, recoveryAddress, toSweep);
}
```
Risk
Likelihood:
A highly secured `scope` might be used intentionally again and again to scam the `stakers`, `stakers` will deposits since they are sure about it and later it will be finalized as `BadFaith` even thou it survived, cheating on `stakers`, `stakers` lose money.
`ATTACKER` might scam or RUG-PULL `stakers` by observing if too much token is `staked` then he'll finalize as `BadFaith` regardless, to steal a lot from `stakers`, while `sponsor/poolOwner` in a bounty supposed to give out and not take.
Why not if take some percentage.
Impact:
`stakers` lose money on false `outcome`.
This system will break `stakers` TRUST.
Proof of Concept
**Proof of Concept:**
⚠⚠⚠⚠⚠⚠⚠⚠⚠
`sponsor/poolOwner` and `moderator` = `ATTACKER`
1. `sponsor/poolOwner` contributes to the pool
2. `sponsor/poolOwner` sets some expiry
3. `stakers` staked some tokens for the bet.
4. `pool` then past the attack stage.
5. `moderator` finalized as `BadFaith`, even thou it's false, the `pool` survived.
6. `ATTACKER` claimed all the staked tokens and bonus to `recoveryAddress` of the `moderator`, hurting/cheating the community or `stakers`.
create a .sol file in `./test/unit` and paste this code, then run `forge test --mt test_ModeratorSettingBadFaithBreaksTrust` on terminal.
<details>
<summary>Code</summary>
```solidity
pragma solidity 0.8.26;
import {BaseConfidencePoolTest} from "../helpers/BaseConfidencePoolTest.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {Test,console2} from "forge-std/Test.sol";
contract BonusCalculationBugTest is BaseConfidencePoolTest {
uint256 internal constant RISK_WINDOW = 30 days;
uint256 internal constant BONUS_POOL = 1000 * ONE;
function setUp() public override {
super.setUp();
_contributeBonus(address(this), BONUS_POOL);
}
function _triggerRiskWindow() internal {
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
pool.pokeRiskWindow();
}
function _flagCorrupted(bool goodFaith, address attacker) internal {
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, goodFaith, attacker);
}
function test_ModeratorSettingBadFaithBreaksTrust() public {
address contributor = makeAddr("c");
uint256 amount = 100e18;
token.mint(contributor, amount);
uint256 stakeAmount = 1 * ONE;
vm.startPrank(contributor);
token.approve(address(pool), amount);
pool.contributeBonus(amount);
vm.stopPrank();
pool.setExpiry(block.timestamp + 30 days);
_stake(bob, stakeAmount);
_stake(alice, stakeAmount);
_triggerRiskWindow();
vm.warp(block.timestamp + 30 days);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
_flagCorrupted(false, address(0));
pool.claimCorrupted();
uint256 poolRemainingBalance = token.balanceOf(address(pool));
console2.log("Pool Remaining Balance:", poolRemainingBalance);
}
}
```
</details>
Recommended Mitigation
**Recommended Mitigation:**
For a justice and also to gain TRUST.
1. if a `pool` is breached illegally and the `moderator` finalized as `BadFaith`. some percentage of the `stakers` funds should be returned to `stakers`, and the remaining should be taken to `recoveryAddress`.
2. if a `pool` is breached illegally and the `moderator` finalized as `BadFaith`. The `stakers` funds should be returned to `stakers`, and the remaining should be taken to `recoveryAddress`.
```diff
- function claimCorrupted() external nonReentrant {
- if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
- if (goodFaith && bountyClaimed < bountyEntitlement) revert MustClaimBountyFirst();
- // aderyn-fp-next-line(reentrancy-state-change)
- uint256 toSweep = stakeToken.balanceOf(address(this));
- if (toSweep == 0) revert NothingToSweep();
- // Clamp the decrement — `toSweep` can exceed the original reserve when post-resolution
- // donations have inflated the balance.
- corruptedReserve = toSweep <= corruptedReserve ? corruptedReserve - toSweep : 0;
- if (!goodFaith) {
- bountyClaimed = bountyEntitlement;
- }
- if (!claimsStarted) claimsStarted = true;
- stakeToken.safeTransfer(recoveryAddress, toSweep);
- emit ClaimCorrupted(msg.sender, recoveryAddress, toSweep);
- }
```
```diff
+ mapping(address => bool) public hasWithdrawn;
+ function claimCorrupted() external nonReentrant {
+ if (outcome != PoolStates.Outcome.CORRUPTED) revert OutcomeNotSet();
+ if (goodFaith && bountyClaimed < bountyEntitlement) revert MustClaimBountyFirst();
+ uint256 balance = stakeToken.balanceOf(address(this));
+ if (balance == 0) revert NothingToSweep();
+ if (!goodFaith) {
+ // ✅ Bad-faith: reserve staker principal, sweep the rest
+ uint256 toSweep = balance > totalEligibleStake ? balance - totalEligibleStake : 0;
+ if (toSweep == 0) revert NothingToSweep();
+ // records in corruptedReserve is kept
+ corruptedReserve = toSweep <= corruptedReserve ? corruptedReserve - toSweep : 0;
+ bountyClaimed = bountyEntitlement;
+ if (!claimsStarted) claimsStarted = true;
+ stakeToken.safeTransfer(recoveryAddress, toSweep);
+ emit ClaimCorrupted(msg.sender, recoveryAddress, toSweep);
+ } else {
+ // ✅ Good-faith: entire pool goes to attacker (bounty)
+ // This should be handled by claimAttackerBounty(), not here
+ revert("Good-faith: use claimAttackerBounty()");
+ }
+ }
+ function emergencyWithdraw() external nonReentrant {
+ // ✅ Only bad-faith CORRUPTED
+ if (goodFaith || outcome != PoolStates.Outcome.CORRUPTED) revert();
+ if (hasWithdrawn[msg.sender]) revert InvalidAmount();
+ uint256 amount = eligibleStake[msg.sender];
+ if (amount == 0) revert InvalidAmount();
+ // ✅ Update global accounting
+ _clampUserSums(msg.sender);
+ sumStakeTime -= userSumStakeTime[msg.sender];
+ sumStakeTimeSq -= userSumStakeTimeSq[msg.sender];
+ eligibleStake[msg.sender] = 0;
+ userSumStakeTime[msg.sender] = 0;
+ userSumStakeTimeSq[msg.sender] = 0;
+ totalEligibleStake -= amount;
+ // ✅ Reduce corruptedReserve by the amount withdrawn
+ corruptedReserve -= amount;
+ hasWithdrawn[msg.sender] = true;
+ if (!claimsStarted) claimsStarted = true;
+ stakeToken.safeTransfer(msg.sender, amount);
+ emit Withdrawn(msg.sender, amount);
+ }
```
Thank you, i hope codehawks will be back.
we love you patrick and the team❤❤❤