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

ConfidencePoolFactory.sol - initialize() is front-runnable due to missing access control

Author Revealed upon completion

Root + Impact

Description

The initialize() function in ConfidencePoolFactory.sol uses the OpenZeppelin initializer modifier, which only ensures the function is called once, but does not restrict who calls it. If the factory proxy is deployed via a separate transaction from its initialization, it remains in an uninitialized state on-chain for a brief window. An attacker monitoring the mempool can front-run the legitimate initialization transaction and call initialize() themselves, setting themselves as the owner.


Root Cause: Lack of caller authorization (onlyOwner or onlyFactory) in initialize(). The initializer modifier is insufficient for access control.


PoC Full Video:


I'm upload google drive full PoC video for better understand https://drive.google.com/file/d/1-KYsCQSbHK3eS0SDDw9EJfGnlzLMHb9w/view?usp=drivesdk

//

Risk

Likelihood:

· This issue depends on the deployment script pattern. If ERC1967Proxy deployment and initialize() call are two separate transactions, the vulnerability is guaranteed to be exploitable by any mempool watcher.

· The proxy address is known immediately after deployment, giving attackers a clear target.

· Since many upgradeable contracts are deployed this way, the likelihood is High in misconfigured deployments.


PoC Full Video:

I'm upload google drive full PoC video for better understand https://drive.google.com/file/d/1-KYsCQSbHK3eS0SDDw9EJfGnlzLMHb9w/view?usp=drivesdk


Impact:

· An attacker gains full ownership of the factory contract.

· The attacker can call setPoolImplementation() to deploy malicious pools, leading to theft of all funds deposited into future pools.

· The attacker can pause() the entire factory, causing Denial of Service (DoS).

· The attacker can use _authorizeUpgrade() (since it is onlyOwner) to upgrade the factory to any arbitrary logic, permanently compromising the protocol.

Proof of Concept

Steps to Reproduce (Foundry):
1. Start a local Anvil node: anvil --port 8546 &
2. Set environment variable: export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
3. Run the PoC script: forge script script/DeployPoC.s.sol --fork-url http://localhost:8546 --broadcast -vvvv
PoC Contracts Used:
· VulnerableFactoryLogic: Simulates the implementation with a vulnerable initialize().
· MinimalProxy: Minimal proxy (using immutable to avoid storage collision).
· Attacker: Calls initialize() on the proxy before the legitimate deployer.
Output Obtained:
```
== Logs ==
Logic deployed at: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Proxy deployed at: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Attacker front-ran initialize!
Proxy owner is now: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
```
Verification:
cast call 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "getOwner()" --rpc-url http://localhost:8546
Returns: 0x1804c8AB... (attacker address) – proving the proxy owner is now the attacker.

Recommended Mitigation

Fix: Use the atomic deployment + initialization pattern, where the proxy is deployed and initialized in the same transaction.
```diff
bytes memory initData = abi.encodeCall(
ConfidencePoolFactory.initialize,
(safeHarborRegistry, poolImpl, moderator)
);
- ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), "");
- ConfidencePoolFactory(address(proxy)).initialize(...);
+ new ERC1967Proxy(address(implementation), initData);
```
This ensures the proxy is never left in an uninitialized state on-chain, eliminating the front-run window.

Support

FAQs

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

Give us feedback!