# Losing fund during force deployment

In zkSync's ContractDeployer.forceDeployOnAddresses, a failed per-contract force-deploy leaves the sent ETH stranded in ContractDeployer instead of reverting or sweeping it.

- Payout: Contest reward
- Severity: medium
- Program: zkSync
- Credited: HE1M
- Disclosed: 2023-06-14
- Write-up quality: 6/10
- Original: https://github.com/code-423n4/2023-03-zksync-findings/issues/64
- Library page: https://bugbountylibrary.com/r/c4%3A2023-03-zksync%3AM-05
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:medium tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** ContractDeployer.forceDeployOnAddresses (L2 system contract)
**Root cause:** forceDeployOnAddresses loops over deployments and self-calls forceDeployOnAddress with value; a failed inner call does not revert the loop, so the transferred ETH stays in ContractDeployer. The aggregated value is compared against msg.value, not the contract's ETH balance, so stranded funds cannot be reused for later deployments.
**Steps:**
1. Call `forceDeployOnAddresses(deployments)` with `msg.value` equal to the sum of `_deployments[i].value`.
2. For each i, the function does `this.forceDeployOnAddress{value: _deployments[i].value}(_deployments[i], msg.sender)`.
3. Cause one deployment to fail (unknown bytecode, failure storing constructing bytecode hash in AccountCodeStorage, or failure constructing/transferring value).
4. The inner call fails but the outer loop continues; the `_deployments[i].value` ETH remains in ContractDeployer.
5. Subsequent deployments cannot consume it because the check compares aggregated value to `msg.value`, not balance; funds are stuck.
**How found:** Code review of forceDeployOnAddresses: noticed the self-call with value inside a loop that does not revert on failure, and that the value check uses msg.value rather than contract balance, implying stranded ETH on partial failure.
**Exploitation:** Not an external attack; a privileged upgrade-time force deployment that partially fails strands FORCE_DEPLOYER ETH in ContractDeployer with no easy recovery path.
**Insight:** When a batch function forwards ETH per-item via self-call and intentionally swallows failures, check whether failed items' value is swept or refunded; comparing aggregate value to msg.value instead of address(this).balance is the tell that stranded funds become unrecoverable.
