The factory permits its owner to allowlist any nominally standard ERC20 stake token. Standard ERC20 tokens with issuer-controlled address blacklists, such as USDC-like assets, remain compatible with the documented exclusion of only fee-on-transfer and rebasing tokens.
There is no forfeiture functionality that allows the recovery address to later rescuee those funds. There is also no deadline for SURVIVED or EXPIRED claims. sweepUnclaimedBonus() continues to reserve totalEligibleStake and, when a risk window was observed, the entire snapshotTotalBonus - claimedBonus. Consequently, the blacklisted position remains reserved indefinitely unless the token issuer removes the blacklist.
A staker blacklisted after depositing causes the pool reserves/bonus to stay locked in the pool permanently.
pragma solidity 0.8.26;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.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";
contract MockBlacklistableUSDC is ERC20 {
error Blacklisted(address account);
mapping(address account => bool blocked) public isBlacklisted;
constructor() ERC20("Mock USDC", "USDC") {}
function decimals() public pure override returns (uint8) {
return 6;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function setBlacklisted(address account, bool blocked) external {
isBlacklisted[account] = blocked;
}
function _update(address from, address to, uint256 amount) internal override {
if (from != address(0) && isBlacklisted[from]) revert Blacklisted(from);
if (to != address(0) && isBlacklisted[to]) revert Blacklisted(to);
super._update(from, to, amount);
}
}
contract BlacklistedStakerClaimPoC is BaseConfidencePoolTest {
uint256 internal constant USDC = 1e6;
function testPoC_BlacklistedClaimSurvivedLeavesPrincipalAndBonusIndefinitelyReserved() external {
(MockBlacklistableUSDC usdc, ConfidencePool usdcPool) = _deployUSDCpool();
_stakeUSDC(usdc, usdcPool, alice, 100 * USDC);
_stakeUSDC(usdc, usdcPool, bob, 100 * USDC);
_contributeUSDC(usdc, usdcPool, carol, 60 * USDC);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
usdcPool.pokeRiskWindow();
vm.warp(block.timestamp + 10 days);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
usdcPool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
usdc.setBlacklisted(alice, true);
vm.expectRevert(abi.encodeWithSelector(MockBlacklistableUSDC.Blacklisted.selector, alice));
vm.prank(alice);
usdcPool.claimSurvived();
vm.prank(bob);
usdcPool.claimSurvived();
assertEq(usdc.balanceOf(bob), 130 * USDC);
assertEq(usdcPool.totalEligibleStake(), 100 * USDC);
assertEq(usdcPool.claimedBonus(), 30 * USDC);
assertEq(usdc.balanceOf(address(usdcPool)), 130 * USDC);
vm.warp(block.timestamp + 10 * 365 days);
vm.expectRevert(IConfidencePool.NothingToSweep.selector);
usdcPool.sweepUnclaimedBonus();
assertEq(usdc.balanceOf(address(usdcPool)), 130 * USDC);
}
function _deployUSDCpool() internal returns (MockBlacklistableUSDC usdc, ConfidencePool usdcPool) {
usdc = new MockBlacklistableUSDC();
ConfidencePool implementation = new ConfidencePool();
usdcPool = ConfidencePool(Clones.clone(address(implementation)));
usdcPool.initialize(
agreement,
address(usdc),
address(safeHarborRegistry),
moderator,
block.timestamp + 31 days,
USDC,
recovery,
address(this),
_defaultScope()
);
}
function _stakeUSDC(MockBlacklistableUSDC usdc, ConfidencePool usdcPool, address user, uint256 amount) internal {
usdc.mint(user, amount);
vm.startPrank(user);
usdc.approve(address(usdcPool), amount);
usdcPool.stake(amount);
vm.stopPrank();
}
function _contributeUSDC(MockBlacklistableUSDC usdc, ConfidencePool usdcPool, address user, uint256 amount)
internal
{
usdc.mint(user, amount);
vm.startPrank(user);
usdc.approve(address(usdcPool), amount);
usdcPool.contributeBonus(amount);
vm.stopPrank();
}
}
Implement a deadline for claims after which the Protocol is allowed to sweep unclaimed reserves. Or a functionality to forfeit positions for token-blacklisted stakers.