This effectively prevents any pool from being created for that agreement at index 0 (or whatever index was used) ever again.
Setup scenario
pragma solidity 0.8.26;
import "forge-std/Test.sol";
import "../src/ConfidencePoolFactory.sol";
import "../src/ConfidencePool.sol";
import "@battlechain/interface/IAgreement.sol";
contract ConfidencePoolFactoryTest is Test {
ConfidencePoolFactory public factory;
ConfidencePool public poolImplementation;
MockAgreement public mockAgreement;
MockSafeHarborRegistry public mockRegistry;
address public factoryOwner = address(0x1);
address public agreementOwner = address(0x2);
address public moderator = address(0x3);
address public recoveryAddress = address(0x4);
address public stakeToken = address(0x5);
function setUp() public {
mockAgreement = new MockAgreement();
mockAgreement.setOwner(agreementOwner);
mockAgreement.setContractInScope(true);
mockRegistry = new MockSafeHarborRegistry();
mockRegistry.setAgreementValid(address(mockAgreement), true);
poolImplementation = new ConfidencePool();
vm.startPrank(factoryOwner);
factory = new ConfidencePoolFactory();
factory.initialize(
address(mockRegistry),
address(poolImplementation),
moderator
);
factory.setStakeTokenAllowed(stakeToken, true);
vm.stopPrank();
}
}
contract MockAgreement is IAgreement {
address public owner;
mapping(address => bool) public inScope;
bool public scopeValidationEnabled = true;
bool public scopeResult = true;
function setOwner(address _owner) external { owner = _owner; }
function setContractInScope(bool result) external { scopeResult = result; }
function setScopeValidationEnabled(bool enabled) external { scopeValidationEnabled = enabled; }
function owner() external view returns (address) { return owner; }
function isContractInScope(address account) external view returns (bool) {
require(scopeValidationEnabled, "Scope validation disabled");
return scopeResult;
}
}
contract MockSafeHarborRegistry {
mapping(address => bool) public validAgreements;
function setAgreementValid(address agreement, bool valid) external {
validAgreements[agreement] = valid;
}
function isAgreementValid(address agreement) external view returns (bool) {
return validAgreements[agreement];
}
function getAttackRegistry() external view returns (address) {
return address(this);
}
}
POC test
pragma solidity 0.8.26;
import "forge-std/Test.sol";
import "./ConfidencePoolFactoryTest.sol";
contract SaltBurningTest is ConfidencePoolFactoryTest {
function test_SaltBurningOnFailedInitialization() public {
address[] memory accounts = new address[](1);
accounts[0] = address(0x100);
vm.startPrank(agreementOwner);
address pool1 = factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
accounts
);
assertTrue(pool1 != address(0));
console.log("Pool 1 deployed at:", pool1);
address[] memory pools = factory.getPoolsByAgreement(address(mockAgreement));
assertEq(pools.length, 1);
assertEq(pools[0], pool1);
console.log("Pool 1 deployed successfully at index 0");
vm.stopPrank();
vm.prank(address(mockAgreement));
mockAgreement.setContractInScope(false);
vm.startPrank(agreementOwner);
vm.expectRevert(abi.encodeWithSelector(
bytes4(keccak256("AccountNotInAgreementScope(address)")),
accounts[0]
));
factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
accounts
);
vm.stopPrank();
mockAgreement.setContractInScope(true);
vm.startPrank(agreementOwner);
vm.expectRevert();
factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
accounts
);
vm.stopPrank();
console.log("Salt for index 1 is permanently burned!");
console.log("Cannot deploy any more pools for this agreement");
}
function test_SaltBurningWithDuplicateAccounts() public {
address[] memory accounts = new address[](2);
accounts[0] = address(0x100);
accounts[1] = address(0x100);
vm.startPrank(agreementOwner);
vm.expectRevert(abi.encodeWithSelector(
bytes4(keccak256("DuplicateAccount(address)")),
address(0x100)
));
factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
accounts
);
vm.stopPrank();
address[] memory validAccounts = new address[](1);
validAccounts[0] = address(0x200);
vm.startPrank(agreementOwner);
vm.expectRevert();
factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
validAccounts
);
vm.stopPrank();
console.log("Index 0 permanently burned due to duplicate account error!");
}
function test_SaltBurningWithInvalidExpiry() public {
address[] memory accounts = new address[](1);
accounts[0] = address(0x100);
vm.startPrank(agreementOwner);
vm.expectRevert(abi.encodeWithSignature("ExpiryTooSoon()"));
factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 1 days,
1 ether,
recoveryAddress,
accounts
);
vm.stopPrank();
vm.startPrank(agreementOwner);
vm.expectRevert();
factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
accounts
);
vm.stopPrank();
console.log("Index 0 permanently burned due to invalid expiry!");
}
function test_SaltBurningWithInvalidRecoveryAddress() public {
address[] memory accounts = new address[](1);
accounts[0] = address(0x100);
vm.startPrank(agreementOwner);
vm.expectRevert(abi.encodeWithSignature("InvalidRecoveryAddress()"));
factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
address(0),
accounts
);
vm.stopPrank();
vm.startPrank(agreementOwner);
vm.expectRevert();
factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
accounts
);
vm.stopPrank();
console.log("Index 0 permanently burned due to invalid recovery address!");
}
function test_MultipleFailedAttemptsBurnMultipleSalts() public {
address[] memory accounts = new address[](1);
accounts[0] = address(0x100);
vm.startPrank(agreementOwner);
mockAgreement.setContractInScope(false);
vm.expectRevert(abi.encodeWithSelector(
bytes4(keccak256("AccountNotInAgreementScope(address)")),
accounts[0]
));
factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
accounts
);
address[] memory duplicateAccounts = new address[](2);
duplicateAccounts[0] = address(0x100);
duplicateAccounts[1] = address(0x100);
mockAgreement.setContractInScope(true);
vm.expectRevert(abi.encodeWithSelector(
bytes4(keccak256("DuplicateAccount(address)")),
address(0x100)
));
factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
duplicateAccounts
);
address pool = factory.createPool(
address(mockAgreement),
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
accounts
);
assertTrue(pool != address(0));
console.log("Pool created at index 2 after burning indices 0 and 1");
vm.stopPrank();
address[] memory pools = factory.getPoolsByAgreement(address(mockAgreement));
assertEq(pools.length, 1);
assertEq(pools[0], pool);
console.log("Indices 0 and 1 are permanently burned but index 2 works");
console.log("This means some agreement indexes are permanently unusable");
}
}