Description
The _replaceScope() function iterates over all accounts in the scope to validate and store them. There is no maximum limit on the number of accounts that can be added. A malicious actor could create a pool with an extremely large scope (e.g., 10,000 accounts), causing createPool() to exceed the block gas limit or become extremely expensive to call.
The _replaceScope() function also clears the existing scope before adding new accounts, iterating over the old scope as well. The gas cost is O(n) where n is the scope length.
function _replaceScope(address[] calldata accounts) internal {
if (accounts.length == 0) revert EmptyScope();
address[] memory old = _scopeAccounts;
for (uint256 i; i < old.length; ++i) {
isAccountInScope[old[i]] = false;
}
delete _scopeAccounts;
for (uint256 i; i < accounts.length; ++i) {
address account = accounts[i];
if (isAccountInScope[account]) revert DuplicateAccount(account);
if (!IAgreement(agreement).isContractInScope(account)) {
revert AccountNotInAgreementScope(account);
}
_scopeAccounts.push(account);
isAccountInScope[account] = true;
}
}
isk
Likelihood:
An agreement owner could create a pool with a scope of thousands of accounts
Each account requires an external call to IAgreement(agreement).isContractInScope()
The total gas cost could exceed block limits, permanently blocking the pool creation
Existing pools with large scopes would be expensive to query via getScopeAccounts()
Impact:
Denial of Service for pool creation with large scopes
Gas griefing for users interacting with large-scope pools
Potential to exceed block gas limit, making a pool impossible to create or interact withProof of Concept
function testLargeScopeGasGriefing() public {
address[] memory accounts = new address[](10000);
for (uint256 i = 0; i < 10000; i++) {
accounts[i] = address(uint160(i + 1));
}
vm.expectRevert();
factory.createPool(
agreement,
stakeToken,
block.timestamp + 30 days,
1 ether,
recoveryAddress,
accounts
);
}
Recommended Mitigation
- remove this code
+ add this // ConfidencePool.sol
+ uint256 public constant MAX_SCOPE_SIZE = 100; // Configurable via factory
function _replaceScope(address[] calldata accounts) internal {
if (accounts.length == 0) revert EmptyScope();
+ if (accounts.length > MAX_SCOPE_SIZE) revert ScopeTooLarge();
// Clear existing scope
address[] memory old = _scopeAccounts;
for (uint256 i; i < old.length; ++i) {
isAccountInScope[old[i]] = false;
}
delete _scopeAccounts;
// Add new scope
for (uint256 i; i < accounts.length; ++i) {
address account = accounts[i];
if (isAccountInScope[account]) revert DuplicateAccount(account);
if (!IAgreement(agreement).isContractInScope(account)) {
revert AccountNotInAgreementScope(account);
}
_scopeAccounts.push(account);
isAccountInScope[account] = true;
}
}