FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: low
Likelihood: low

Missing Event Emission for expiryLocked State Transition

Author Revealed upon completion

Root + Impact

Description

  • When the first stake occurs in a pool, the expiryLocked flag should be set to true and an event should be emitted to signal this state change, similar to how the scopeLocked flag emits a ScopeLocked event when the pool scope becomes immutable.

  • The expiryLocked flag is set to true on the first stake without emitting an event, reducing transparency for off-chain indexers monitoring pool state changes. This differs from other important state transitions in the contract which emit events, and makes it difficult for monitoring systems to determine when the expiry became immutable without tracking all stake events and inferring the state change.

if (!expiryLocked) {
expiryLocked = true;
// Event emission missing
}

Risk

Likelihood:

  • The expiryLocked flag is set to true during the first stake operation in any pool when the stake() function

  • This state change occurs silently every time a pool receives its first stake, as the condition !expiryLocked evaluates to true on the initial stake call

Impact:

  • Off-chain indexers and monitoring systems cannot easily determine when the expiry became immutable without tracking all stake events and inferring the state change

  • Reduced transparency and auditability for pool state changes, as other important state transitions in the contract emit events but this one does not

Proof of Concept

Add the following test in ConfidencePool.branches.t.sol
and then run forge test --match-test test_expiryLockedEvent_NOT_emitted_onFirstStake
and the test will pass

import {Vm} from "forge-std/Vm.sol";
function test_expiryLockedEvent_NOT_emitted_onFirstStake() external {
assertEq(pool.expiryLocked(), false);
// Record logs to check for event emission
vm.recordLogs();
// First stake sets expiryLocked to true
_stake(alice, 10 * ONE);
// Verify the flag was set
assertEq(pool.expiryLocked(), true);
// Check that NO ExpiryLocked event was emitted (this demonstrates the bug)
Vm.Log[] memory logs = vm.getRecordedLogs();
bytes32 expiryLockedTopic = keccak256("ExpiryLocked(uint256)");
uint256 emitCount = 0;
for (uint256 i; i < logs.length; ++i) {
if (logs[i].emitter == address(pool) &&
logs[i].topics.length > 0 &&
logs[i].topics[0] == expiryLockedTopic) {
emitCount++;
}
}
assertEq(emitCount, 0, "ExpiryLocked event should NOT be emitted (bug exists)");
}

Recommended Mitigation

  • Add the event definition to src/interfaces/IConfidencePool.sol:

  • Emit the event in src/ConfidencePool.sol when setting the flag:

+ event ExpiryLocked(uint256 timestamp); // Add the event def in iConfidencePool.sol
if (!expiryLocked) {
expiryLocked = true;
emit ExpiryLocked(block.timestamp); // Emit the event when setting the flag:
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!