Root + Impact
Description
claimSurvived and claimExpired both hardcode msg.sender as the transfer recipient. A staker calls the function, all accounting state is updated, and then safeTransfer(msg.sender, payout) fires at the very end. If the token reverts on that transfer — because the staker's address is blacklisted by the token issuer (USDC/USDT both have on-chain blacklists) — the entire transaction rolls back atomically. Every state update is undone. The staker is stuck: they can call claimSurvived forever and it will always revert at the same line. There is no alternative entry point, no admin rescue, and no way to redirect the payout to a different address.
Because the rollback leaves eligibleStake[staker] non-zero and totalEligibleStake unchanged, sweepUnclaimedBonus correctly reserves those funds and will always revert with NothingToSweep. The principal and bonus share are permanently locked in the contract.
function claimSurvived() external nonReentrant {
hasClaimed[msg.sender] = true;
totalEligibleStake -= userEligible;
claimedBonus += bonusShare;
delete eligibleStake[msg.sender];
stakeToken.safeTransfer(msg.sender, payout);
}
Same pattern exists in claimExpired:
And sweepUnclaimedBonus will always protect the stuck funds because totalEligibleStake is never decremented:
Risk
Likelihood:
-
USDC and USDT both maintain on-chain blacklists. If either is on the factory allowlist (common in DeFi protocols), any staker who gets sanctioned by OFAC or flagged by the token issuer after staking hits this permanently.
-
Blacklisting events happen in the real world — exchange compliance actions, OFAC sanctions, and court-ordered freezes all result in addresses being blacklisted on live tokens. A staker has no control over this after they deposit.
Impact:
-
The staker permanently loses their entire principal plus their pro-rata bonus share. There is no recovery path — not for the staker, not for the pool owner, not for anyone.
-
The locked funds inflate totalEligibleStake indefinitely, which means sweepUnclaimedBonus can never sweep anything while that staker is unclaimed. Other protocol accounting that depends on totalEligibleStake reaching zero is also affected.
Recommended Mitigation
Add a recipient parameter so a staker can redirect their payout to an address that can receive the token. This is the standard "pull with destination" pattern used by protocols that support tokens with blacklists.
- function claimSurvived() external nonReentrant {
+ function claimSurvived(address recipient) external nonReentrant {
+ if (recipient == address(0)) revert InvalidRecoveryAddress();
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);
+ stakeToken.safeTransfer(recipient, payout);
+ emit ClaimSurvived(msg.sender, userEligible, bonusShare);
}
Apply the same change to claimExpired. Callers who are not blacklisted just pass msg.sender as the recipient — no behavior change for the normal path.
efccweb3@EFCCWEB3:~/2026-07-bc-confidence-pools$ forge test --match-test testBlacklistedStakerPrincipalPermanentlyStuck -vvvv
[⠊] Compiling...
[⠑] Compiling 1 files with Solc 0.8.26
[⠃] Solc 0.8.26 finished in 20.67s
Compiler run successful!
Ran 1 test for test/unit/ConfidencePool.blacklist.t.sol:USDCBlacklistStuckPrincipalTest
[PASS] testBlacklistedStakerPrincipalPermanentlyStuck() (gas: 614085)
Traces:
[753385] USDCBlacklistStuckPrincipalTest::testBlacklistedStakerPrincipalPermanentlyStuck()
├─ [48648] BlacklistableERC20::mint(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 100000000000000000000 [1e20])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], amount: 100000000000000000000 [1e20])
│ └─ ← [Stop]
├─ [0] VM::startPrank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
│ └─ ← [Return]
├─ [24325] BlacklistableERC20::approve(0xa0Cb889707d426A7A386870A03bc70d1b0697598, 100000000000000000000 [1e20])
│ ├─ emit Approval(owner: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], spender: 0xa0Cb889707d426A7A386870A03bc70d1b0697598, amount: 100000000000000000000 [1e20])
│ └─ ← [Return] true
├─ [240244] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::stake(100000000000000000000 [1e20])
│ ├─ [237575] ConfidencePool::stake(100000000000000000000 [1e20]) [delegatecall]
│ │ ├─ [2323] MockSafeHarborRegistry::getAttackRegistry() [staticcall]
│ │ │ └─ ← [Return] MockAttackRegistry: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]
│ │ ├─ [2374] MockAttackRegistry::getAgreementState(MockAgreement: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
│ │ │ └─ ← [Return] 1
│ │ ├─ [2537] BlacklistableERC20::balanceOf(0xa0Cb889707d426A7A386870A03bc70d1b0697598) [staticcall]
│ │ │ └─ ← [Return] 0
│ │ ├─ [25606] BlacklistableERC20::transferFrom(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 0xa0Cb889707d426A7A386870A03bc70d1b0697598, 100000000000000000000 [1e20])
│ │ │ ├─ emit Transfer(from: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], to: 0xa0Cb889707d426A7A386870A03bc70d1b0697598, amount: 100000000000000000000 [1e20])
│ │ │ └─ ← [Return] true
│ │ ├─ [537] BlacklistableERC20::balanceOf(0xa0Cb889707d426A7A386870A03bc70d1b0697598) [staticcall]
│ │ │ └─ ← [Return] 100000000000000000000 [1e20]
│ │ ├─ emit Staked(staker: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], amount: 100000000000000000000 [1e20])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [26748] BlacklistableERC20::mint(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], 100000000000000000000 [1e20])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], amount: 100000000000000000000 [1e20])
│ └─ ← [Stop]
├─ [0] VM::startPrank(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
│ └─ ← [Return]
├─ [24325] BlacklistableERC20::approve(0xa0Cb889707d426A7A386870A03bc70d1b0697598, 100000000000000000000 [1e20])
│ ├─ emit Approval(owner: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], spender: 0xa0Cb889707d426A7A386870A03bc70d1b0697598, amount: 100000000000000000000 [1e20])
│ └─ ← [Return] true
├─ [81199] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::stake(100000000000000000000 [1e20])
│ ├─ [81030] ConfidencePool::stake(100000000000000000000 [1e20]) [delegatecall]
│ │ ├─ [323] MockSafeHarborRegistry::getAttackRegistry() [staticcall]
│ │ │ └─ ← [Return] MockAttackRegistry: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]
│ │ ├─ [374] MockAttackRegistry::getAgreementState(MockAgreement: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
│ │ │ └─ ← [Return] 1
│ │ ├─ [537] BlacklistableERC20::balanceOf(0xa0Cb889707d426A7A386870A03bc70d1b0697598) [staticcall]
│ │ │ └─ ← [Return] 100000000000000000000 [1e20]
│ │ ├─ [3706] BlacklistableERC20::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], 0xa0Cb889707d426A7A386870A03bc70d1b0697598, 100000000000000000000 [1e20])
│ │ │ ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: 0xa0Cb889707d426A7A386870A03bc70d1b0697598, amount: 100000000000000000000 [1e20])
│ │ │ └─ ← [Return] true
│ │ ├─ [537] BlacklistableERC20::balanceOf(0xa0Cb889707d426A7A386870A03bc70d1b0697598) [staticcall]
│ │ │ └─ ← [Return] 200000000000000000000 [2e20]
│ │ ├─ emit Staked(staker: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], amount: 100000000000000000000 [1e20])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [3254] MockAttackRegistry::setAgreementState(5)
│ └─ ← [Return]
├─ [0] VM::prank(moderator: [0x62853092D02FA98524B9618334F605a1801432cA])
│ └─ ← [Return]
├─ [130808] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::flagOutcome(1, false, 0x0000000000000000000000000000000000000000)
│ ├─ [130627] ConfidencePool::flagOutcome(1, false, 0x0000000000000000000000000000000000000000) [delegatecall]
│ │ ├─ [323] MockSafeHarborRegistry::getAttackRegistry() [staticcall]
│ │ │ └─ ← [Return] MockAttackRegistry: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]
│ │ ├─ [374] MockAttackRegistry::getAgreementState(MockAgreement: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
│ │ │ └─ ← [Return] 5
│ │ ├─ emit ScopeLocked(timestamp: 1750000000 [1.75e9])
│ │ ├─ emit RiskWindowEnded(timestamp: 1750000000 [1.75e9])
│ │ ├─ emit OutcomeFlagged(moderator: moderator: [0x62853092D02FA98524B9618334F605a1801432cA], outcome: 1, goodFaith: false, attacker: 0x0000000000000000000000000000000000000000)
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [632] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::outcome() [staticcall]
│ ├─ [466] ConfidencePool::outcome() [delegatecall]
│ │ └─ ← [Return] 1
│ └─ ← [Return] 1
├─ [1426] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::totalEligibleStake() [staticcall]
│ ├─ [1260] ConfidencePool::totalEligibleStake() [delegatecall]
│ │ └─ ← [Return] 200000000000000000000 [2e20]
│ └─ ← [Return] 200000000000000000000 [2e20]
├─ [20691] BlacklistableERC20::blacklist(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
│ └─ ← [Stop]
├─ [679] BlacklistableERC20::isBlacklisted(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
│ └─ ← [Return] true
├─ [0] VM::prank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [29592] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::claimSurvived()
│ ├─ [29409] ConfidencePool::claimSurvived() [delegatecall]
│ │ ├─ [756] BlacklistableERC20::transfer(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 100000000000000000000 [1e20])
│ │ │ └─ ← [Revert] USDC: account is blacklisted
│ │ └─ ← [Revert] USDC: account is blacklisted
│ └─ ← [Revert] USDC: account is blacklisted
├─ [1296] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::eligibleStake(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
│ ├─ [1124] ConfidencePool::eligibleStake(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [delegatecall]
│ │ └─ ← [Return] 100000000000000000000 [1e20]
│ └─ ← [Return] 100000000000000000000 [1e20]
├─ [3242] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::hasClaimed(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
│ ├─ [3070] ConfidencePool::hasClaimed(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [delegatecall]
│ │ └─ ← [Return] false
│ └─ ← [Return] false
├─ [1426] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::totalEligibleStake() [staticcall]
│ ├─ [1260] ConfidencePool::totalEligibleStake() [delegatecall]
│ │ └─ ← [Return] 200000000000000000000 [2e20]
│ └─ ← [Return] 200000000000000000000 [2e20]
├─ [537] BlacklistableERC20::balanceOf(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::prank(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
│ └─ ← [Return]
├─ [53611] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::claimSurvived()
│ ├─ [53448] ConfidencePool::claimSurvived() [delegatecall]
│ │ ├─ [23013] BlacklistableERC20::transfer(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], 100000000000000000000 [1e20])
│ │ │ ├─ emit Transfer(from: 0xa0Cb889707d426A7A386870A03bc70d1b0697598, to: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], amount: 100000000000000000000 [1e20])
│ │ │ └─ ← [Return] true
│ │ ├─ emit ClaimSurvived(staker: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], principal: 100000000000000000000 [1e20], bonusShare: 0)
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [537] BlacklistableERC20::balanceOf(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]) [staticcall]
│ └─ ← [Return] 100000000000000000000 [1e20]
├─ [1426] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::totalEligibleStake() [staticcall]
│ ├─ [1260] ConfidencePool::totalEligibleStake() [delegatecall]
│ │ └─ ← [Return] 100000000000000000000 [1e20]
│ └─ ← [Return] 100000000000000000000 [1e20]
├─ [537] BlacklistableERC20::balanceOf(0xa0Cb889707d426A7A386870A03bc70d1b0697598) [staticcall]
│ └─ ← [Return] 100000000000000000000 [1e20]
├─ [0] VM::expectRevert(NothingToSweep())
│ └─ ← [Return]
├─ [2179] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::sweepUnclaimedBonus()
│ ├─ [2014] ConfidencePool::sweepUnclaimedBonus() [delegatecall]
│ │ ├─ [537] BlacklistableERC20::balanceOf(0xa0Cb889707d426A7A386870A03bc70d1b0697598) [staticcall]
│ │ │ └─ ← [Return] 100000000000000000000 [1e20]
│ │ └─ ← [Revert] NothingToSweep()
│ └─ ← [Revert] NothingToSweep()
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] newRecovery: [0xf384aB531188266F3308d37E3989959b29B289dc]
├─ [0] VM::label(newRecovery: [0xf384aB531188266F3308d37E3989959b29B289dc], "newRecovery")
│ └─ ← [Return]
├─ [9483] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::setRecoveryAddress(newRecovery: [0xf384aB531188266F3308d37E3989959b29B289dc])
│ ├─ [9314] ConfidencePool::setRecoveryAddress(newRecovery: [0xf384aB531188266F3308d37E3989959b29B289dc]) [delegatecall]
│ │ ├─ emit RecoveryAddressUpdated(oldAddr: recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa], newAddr: newRecovery: [0xf384aB531188266F3308d37E3989959b29B289dc])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [0] VM::expectRevert(NothingToSweep())
│ └─ ← [Return]
├─ [2179] 0xa0Cb889707d426A7A386870A03bc70d1b0697598::sweepUnclaimedBonus()
│ ├─ [2014] ConfidencePool::sweepUnclaimedBonus() [delegatecall]
│ │ ├─ [537] BlacklistableERC20::balanceOf(0xa0Cb889707d426A7A386870A03bc70d1b0697598) [staticcall]
│ │ │ └─ ← [Return] 100000000000000000000 [1e20]
│ │ └─ ← [Revert] NothingToSweep()
│ └─ ← [Revert] NothingToSweep()
├─ [537] BlacklistableERC20::balanceOf(0xa0Cb889707d426A7A386870A03bc70d1b0697598) [staticcall]
│ └─ ← [Return] 100000000000000000000 [1e20]
├─ [537] BlacklistableERC20::balanceOf(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
│ └─ ← [Return] 0
└─ ← [Return]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 14.97ms (7.84ms CPU time)
pragma solidity 0.8.26;
import {Test} from "forge-std/Test.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.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 {MockAttackRegistry} from "test/mocks/MockAttackRegistry.sol";
import {MockSafeHarborRegistry} from "test/mocks/MockSafeHarborRegistry.sol";
import {MockAgreement} from "test/mocks/MockAgreement.sol";
contract BlacklistableERC20 is ERC20 {
mapping(address => bool) public isBlacklisted;
address public immutable blacklister;
constructor() ERC20("USD Coin", "USDC") {
blacklister = msg.sender;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function blacklist(address account) external {
require(msg.sender == blacklister, "not blacklister");
isBlacklisted[account] = true;
}
function _update(address from, address to, uint256 value) internal override {
require(!isBlacklisted[to], "USDC: account is blacklisted");
super._update(from, to, value);
}
}
contract USDCBlacklistStuckPrincipalTest is Test {
uint256 internal constant ONE = 1e18;
uint256 internal constant BASE_TIMESTAMP = 1_750_000_000;
address internal constant DEFAULT_SCOPE_ACCOUNT = address(0xC0FFEE);
BlacklistableERC20 internal usdc;
MockAttackRegistry internal attackRegistry;
MockSafeHarborRegistry internal safeHarborRegistry;
MockAgreement internal agreementContract;
ConfidencePool internal pool;
address internal agreement;
address internal moderator = makeAddr("moderator");
address internal recovery = makeAddr("recovery");
address internal alice = makeAddr("alice");
address internal bob = makeAddr("bob");
address internal carol = makeAddr("carol");
function setUp() public {
vm.warp(BASE_TIMESTAMP);
usdc = new BlacklistableERC20();
attackRegistry = new MockAttackRegistry();
safeHarborRegistry = new MockSafeHarborRegistry();
agreementContract = new MockAgreement(address(this));
agreement = address(agreementContract);
agreementContract.setContractInScope(DEFAULT_SCOPE_ACCOUNT, true);
safeHarborRegistry.setAttackRegistry(address(attackRegistry));
safeHarborRegistry.setAgreementValid(agreement, true);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.NEW_DEPLOYMENT);
ConfidencePool implementation = new ConfidencePool();
pool = ConfidencePool(Clones.clone(address(implementation)));
address[] memory scope = new address[]();
scope[0] = DEFAULT_SCOPE_ACCOUNT;
pool.initialize(
agreement,
address(usdc),
address(safeHarborRegistry),
moderator,
block.timestamp + 31 days,
ONE,
recovery,
address(this),
scope
);
}
function testBlacklistedStakerPrincipalPermanentlyStuck() external {
uint256 aliceStake = 100 * ONE;
uint256 bobStake = 100 * ONE;
uint256 bonusAmount = 50 * ONE;
usdc.mint(alice, aliceStake);
vm.startPrank(alice);
usdc.approve(address(pool), aliceStake);
pool.stake(aliceStake);
vm.stopPrank();
usdc.mint(bob, bobStake);
vm.startPrank(bob);
usdc.approve(address(pool), bobStake);
pool.stake(bobStake);
vm.stopPrank();
usdc.mint(carol, bonusAmount);
vm.startPrank(carol);
usdc.approve(address(pool), bonusAmount);
pool.contributeBonus(bonusAmount);
vm.stopPrank();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
assertEq(uint256(pool.outcome()), uint256(PoolStates.Outcome.SURVIVED));
assertEq(pool.totalEligibleStake(), aliceStake + bobStake);
usdc.blacklist(alice);
assertTrue(usdc.isBlacklisted(alice));
vm.prank(alice);
vm.expectRevert();
pool.claimSurvived();
assertEq(pool.eligibleStake(alice), aliceStake,
"Alice eligibleStake unchanged after revert");
assertFalse(pool.hasClaimed(alice),
"Alice hasClaimed still false");
assertEq(pool.totalEligibleStake(), aliceStake + bobStake,
"totalEligibleStake unchanged — Alice's share still counted");
uint256 bobBefore = usdc.balanceOf(bob);
vm.prank(bob);
pool.claimSurvived();
assertGt(usdc.balanceOf(bob) - bobBefore, bobStake,
"Bob received principal + bonus share");
assertEq(pool.totalEligibleStake(), aliceStake,
"Only Alice's stake remains in totalEligibleStake");
assertEq(usdc.balanceOf(address(pool)), aliceStake,
"Pool balance equals Alice's stuck principal");
vm.expectRevert(IConfidencePool.NothingToSweep.selector);
pool.sweepUnclaimedBonus();
pool.setRecoveryAddress(makeAddr("newRecovery"));
vm.expectRevert(IConfidencePool.NothingToSweep.selector);
pool.sweepUnclaimedBonus();
assertEq(usdc.balanceOf(address(pool)), aliceStake,
"CRITICAL: 100 USDC permanently locked in pool");
assertEq(usdc.balanceOf(alice), 0,
"Alice received nothing — principal irrecoverable");
}
}