Root + Impact
Factory sets the same defaultOutcomeModerator for all pools and there's no way to change it afterwards
Description
outcomeModerator in every pool gets set by factory when ConfidencePoolFactory.createPool() function runs which assigns the same defaultOutcomeModerator to every pool, but then there's no way to change it afterwards, unless factory owner changes ConfidencePoolFactory.defaultOutcomeModeratorevery time before a user wants to create a pool but this doesn't seem autonomous or the intended path for setting up the pools for the protocol.
function createPool(
address agreement,
address stakeToken,
uint256 expiry,
uint256 minStake,
address recoveryAddress,
address[] calldata accounts
) external whenNotPaused returns (address pool) {
if (agreement == address(0) || stakeToken == address(0)) revert ZeroAddress();
if (recoveryAddress == address(0)) revert ZeroAddress();
if (!allowedStakeToken[stakeToken]) revert StakeTokenNotAllowed();
if (expiry < block.timestamp + _MIN_EXPIRY_LEAD) revert ExpiryTooSoon();
if (!safeHarborRegistry.isAgreementValid(agreement)) revert InvalidAgreement();
if (IAgreement(agreement).owner() != msg.sender) revert UnauthorizedCreator();
bytes32 salt = keccak256(abi.encode(agreement, _poolsByAgreement[agreement].length));
pool = Clones.cloneDeterministic(poolImplementation, salt);
IConfidencePool(pool)
.initialize(
agreement,
stakeToken,
address(safeHarborRegistry),
=> defaultOutcomeModerator,
expiry,
minStake,
recoveryAddress,
msg.sender,
accounts
);
Risk
Likelihood:
Impact:
Proof of Concept
Recommended Mitigation
implement a setter function for moderator in ConfidencePool contract or modify the ConfidencePoolFactory.createPool() so users can supply the moderator value as input
function createPool(
address agreement,
address stakeToken,
uint256 expiry,
uint256 minStake,
address recoveryAddress,
address[] calldata accounts,
+ address poolModerator
) external whenNotPaused returns (address pool) {
if (agreement == address(0) || stakeToken == address(0)) revert ZeroAddress();
if (recoveryAddress == address(0)) revert ZeroAddress();
if (!allowedStakeToken[stakeToken]) revert StakeTokenNotAllowed();
if (expiry < block.timestamp + _MIN_EXPIRY_LEAD) revert ExpiryTooSoon();
// aderyn-fp-next-line(reentrancy-state-change)
if (!safeHarborRegistry.isAgreementValid(agreement)) revert InvalidAgreement();
// aderyn-fp-next-line(reentrancy-state-change)
if (IAgreement(agreement).owner() != msg.sender) revert UnauthorizedCreator();
// Salt incorporates the per-agreement index so an agreement can back many pools while
// keeping deterministic, collision-free clone addresses.
bytes32 salt = keccak256(abi.encode(agreement, _poolsByAgreement[agreement].length));
pool = Clones.cloneDeterministic(poolImplementation, salt);
// aderyn-fp-next-line(reentrancy-state-change)
IConfidencePool(pool)
.initialize(
agreement,
stakeToken,
address(safeHarborRegistry),
- defaultOutcomeModerator,
+ poolModerator,
expiry,
minStake,
recoveryAddress,
msg.sender,
accounts
);
or implement this function in ConfidencePool.sol contract:
+ function setOutcomeModerator(address newOutcomeModerator) external onlyOwner {
+ if (newOutcomeModerator == address(0)) revert InvalidOutcomeModerator();
+
+ address oldOutcomeModerator = outcomeModerator;
+ outcomeModerator = newOutcomeModerator;
+
+ emit OutcomeModeratorUpdated(oldOutcomeModerator, newOutcomeModerator);
+ }