pragma solidity 0.8.26;
import {Test} from "forge-std/Test.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {ConfidencePoolFactory} from "src/ConfidencePoolFactory.sol";
import {MockAttackRegistry} from "test/mocks/MockAttackRegistry.sol";
import {MockERC20} from "test/mocks/MockERC20.sol";
import {MockSafeHarborRegistry} from "test/mocks/MockSafeHarborRegistry.sol";
contract Ownable2StepAgreementForStaleOwnerPoC is Ownable2Step {
mapping(address account => bool inScope) internal _inScope;
constructor(address owner_) Ownable(owner_) {}
function setContractInScope(address account, bool inScope) external {
_inScope[account] = inScope;
}
function isContractInScope(address account) external view returns (bool) {
return _inScope[account];
}
}
contract StaleAgreementOwnerPoC is Test {
uint256 internal constant ONE = 1e18;
address internal constant INITIAL_ACCOUNT = address(0xC0FFEE);
address internal constant REPLACEMENT_ACCOUNT = address(0xBEEF);
MockERC20 internal token;
MockSafeHarborRegistry internal registry;
MockAttackRegistry internal attackRegistry;
Ownable2StepAgreementForStaleOwnerPoC internal agreement;
ConfidencePoolFactory internal factory;
ConfidencePool internal pool;
address internal originalAgreementOwner = makeAddr("originalAgreementOwner");
address internal newAgreementOwner = makeAddr("newAgreementOwner");
address internal moderator = makeAddr("moderator");
address internal recovery = makeAddr("recovery");
address internal staleRecovery = makeAddr("staleRecovery");
function setUp() public {
token = new MockERC20();
registry = new MockSafeHarborRegistry();
attackRegistry = new MockAttackRegistry();
agreement = new Ownable2StepAgreementForStaleOwnerPoC(originalAgreementOwner);
agreement.setContractInScope(INITIAL_ACCOUNT, true);
agreement.setContractInScope(REPLACEMENT_ACCOUNT, true);
registry.setAttackRegistry(address(attackRegistry));
registry.setAgreementValid(address(agreement), true);
ConfidencePoolFactory impl = new ConfidencePoolFactory();
ConfidencePool poolImplementation = new ConfidencePool();
ERC1967Proxy proxy = new ERC1967Proxy(
address(impl),
abi.encodeCall(
ConfidencePoolFactory.initialize, (address(registry), address(poolImplementation), moderator)
)
);
factory = ConfidencePoolFactory(address(proxy));
factory.setStakeTokenAllowed(address(token), true);
vm.prank(originalAgreementOwner);
address created = factory.createPool(
address(agreement), address(token), block.timestamp + 31 days, ONE, recovery, _single(INITIAL_ACCOUNT)
);
pool = ConfidencePool(created);
vm.prank(originalAgreementOwner);
agreement.transferOwnership(newAgreementOwner);
vm.prank(newAgreementOwner);
agreement.acceptOwnership();
}
function testFormerAgreementOwnerKeepsPoolConfigurationAuthorityAfterAgreementTransfer() external {
assertEq(agreement.owner(), newAgreementOwner, "test precondition: agreement owner changed");
assertEq(pool.owner(), originalAgreementOwner, "pool owner remains creation-time agreement owner");
uint256 newExpiry = block.timestamp + 45 days;
vm.startPrank(originalAgreementOwner);
pool.setExpiry(newExpiry);
pool.setRecoveryAddress(staleRecovery);
pool.setPoolScope(_single(REPLACEMENT_ACCOUNT));
vm.stopPrank();
assertEq(pool.expiry(), newExpiry, "former agreement owner changed expiry");
assertEq(pool.recoveryAddress(), staleRecovery, "former agreement owner changed recovery address");
assertFalse(pool.isAccountInScope(INITIAL_ACCOUNT), "old scope was replaced");
assertTrue(pool.isAccountInScope(REPLACEMENT_ACCOUNT), "former agreement owner changed scope");
}
function testCurrentAgreementOwnerCannotConfigurePoolUnlessPoolOwnershipIsTransferred() external {
assertEq(agreement.owner(), newAgreementOwner, "test precondition: agreement owner changed");
assertEq(pool.owner(), originalAgreementOwner, "test precondition: pool owner is stale");
vm.startPrank(newAgreementOwner);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, newAgreementOwner));
pool.setExpiry(block.timestamp + 45 days);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, newAgreementOwner));
pool.setRecoveryAddress(staleRecovery);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, newAgreementOwner));
pool.setPoolScope(_single(REPLACEMENT_ACCOUNT));
vm.stopPrank();
}
function _single(address account) internal pure returns (address[] memory accounts) {
accounts = new address[](1);
accounts[0] = account;
}
}