Hi all. A protocol bug in the L1 → L2 migrator, responsibly disclosed through the Livepeer Immunefi Bug Bounty program, has now been patched. Sidestream led the analysis and patch, then coordinated final review and execution with the security committee. The bug, if exploited, would have allowed any L1 address to force a delegate change on an L2 delegator they do not own. No user funds were lost, the issue was not exploited on the network to our knowledge, and the system is now fully patched. A bounty has been paid to the reporter, and we thank them for their responsible disclosure.
The issue
The L1 → L2 migrator entry points (migrateUnbondingLocks and migrateDelegator on L1Migrator) authenticated the L1 owner of a migration request, but did not verify that the specified L2 destination address belonged to the same party. Because the L2 Migrator is a trusted caller of the L2 BondingManager, the cross-chain payload could force a delegate change on an arbitrary L2 delegator chosen by the caller.
In practice, this had two effects:
- Griefing. A caller could set a victim’s L2 delegate to
address(0), making their stake inactive (no voting power or rewards until they re-delegate). Cumulatively, this reduces the next round’s total active stake, which in turn deflates the governance quorum threshold. If the transcoder hadn’t yet calledreward()that round, the victim also missed that round’s rewards. - Voting-power concentration. A self-delegated L1 caller could redirect victims’ delegated stake to themselves on L2, accumulating governance voting power. In theory, this could have led to passing a treasury-affecting proposal.
The second path was bounded in practice: it required specific attacker preconditions (self-delegated as a registered transcoder on both L1 and L2), would have been visible on-chain throughout, and would have taken approximately 8 days to play out, giving the protocol and community time to detect and respond. No exploitation occurred.
The fix
L1Migrator was patched to:
- Enforce that the L1 source and L2 destination addresses are identical in
requireValidMigrationviarequire(_l1Addr == _l2Addr, "L2_ADDR_MISMATCH"). - Reject empty unbonding-lock-ID arrays (
require(unbondingLockIdsLen > 0, "EMPTY_LOCK_IDS")) and zero-amount lock entries (require(amount > 0, "ZERO_LOCK_AMOUNT")) ingetMigrateUnbondingLocksParams.
Together, these checks eliminate both disclosed paths.
This was addressed in two steps:
- Interim mitigation.
L1Migratorwas paused via the L1 governance multisig, neutralizing the attack vector while the fix was developed. - Full fix.
L1Migratoris not a proxy and cannot be upgraded in place, so the patched logic shipped as a new contract at0x2a69191B43c9DB47C927bD7287F9C93838d07759. The L2Migrator was then repointed to it to it by the L2 governance multisig, so cross-chain messages are accepted only from the new contract.
The original L1Migrator remains paused and is no longer used.
Behavioural change and multisig compensation
Why the L1 ≠ L2 path existed originally. The migrator lets you specify a different L2 destination than the L1 source because not every delegator is an EOA. Some are contract accounts (multisig wallets) that can’t guarantee the same address on Arbitrum, so they need a different L2 destination. The fix removes that flexibility, which was exactly what made the bug exploitable.
Affected accounts. 6 multisig wallets remain on L1 with approximately 459 LPT bonded (around 1,050 LPT of compounded value on L2 as of the snapshot at L1 block 24,935,949, ≈ late April 2026). The scan used to identify them is published here and is reproducible against any L1 archive node.
Compensation. Reopening the differing-address path would have meant adding new on-chain logic. Given the small, known exposure, direct compensation is the lower-risk tradeoff. So instead, the Livepeer Foundation will send unbonded LPT directly to each multisig’s L1 address. Each gets its compounded snapshot value plus a generous two years of staking rewards at a flat 60% annual rate, roughly 2,700 LPT in total across the six. These transfers go straight to the identified L1 addresses, so no action is required from the holders.
Everyone else. All other migration paths remain open. EOAs can continue to use L1Migrator.migrateUnbondingLocks and migrateDelegator (with the new same-address requirement), or L2Migrator.claimStake (the Merkle-snapshot-based path, which has been continuously available throughout and is unaffected by the L1 system pause).
No LIP required
The security committee deployed this patch under its existing upgrade authority to protect user funds. LIP-25 carves critical bug fixes and emergency pauses out of the LIP process, so no LIP is required. As with prior security patches, the public record is a forum disclosure like this one.
Thanks and pointers
Thanks again to the reporter for the responsible disclosure, and to Sidestream, our protocol security partner, for their thorough and timely handling of the triage, analysis, mitigation design, and patch development through to deployment. The deployed contract and patch are linked below for anyone who wants to verify directly.
- Patched
L1Migrator(L1 mainnet):0x2a69191B43c9DB47C927bD7287F9C93838d07759 - Patch PR / commits: fix: Improve L1Migrator input validation by SidestreamSweatyPumpkin · Pull Request #98 · livepeer/arbitrum-lpt-bridge · GitHub
Sidestream can share further info on community request.
For questions, email security@livepeer.foundation or reply in this thread.