# Two-step ownership transfer process in LSP0ERC725AccountCore can be bypassed

Malicious pending owner reenters acceptOwnership during LSP1 notification in transferOwnership, draining LYX and resetting owner via delegatecall.

- Payout: Audit contest
- Severity: medium
- Program: LUKSO
- Found by: MiloTruck
- Disclosed: 2023-09-18
- Write-up quality: 7/10
- Original: https://code4rena.com/reports/2023-06-lukso#m-02
- Library page: https://bugbountylibrary.com/r/c4%3A2023-06-lukso%3AM-02
- Tags: area:web3 domain:web3 impact:funds-loss impact:integrity kind:contest platform:code4rena sev:medium tech:evm tech:solidity technique:chained vuln:access-control vuln:business-logic vuln:reentrancy

**Target:** LSP0ERC725AccountCore smart contract (LSP14 two-step ownership)
**Root cause:** transferOwnership() notifies the pending owner via LSP1 tryNotifyUniversalReceiver before checking currentOwner == owner(); the pending owner can call acceptOwnership() inside that callback, so the post-call check is satisfied by the attacker's own ownership rather than detecting the takeover.
**Steps:**
1. Victim calls `account.transferOwnership(address(maliciousReceiver))` with attacker contract as pending owner.
2. transferOwnership sets pending owner and calls `pendingNewOwner.tryNotifyUniversalReceiver(_TYPEID_LSP0_OwnershipTransferStarted, "")`.
3. In `universalReceiver()`, attacker calls `account.acceptOwnership()` to become owner.
4. Attacker calls `account.execute(0, address(this), 10 ether, "")` to drain LYX balance.
5. Attacker calls `account.execute(4, address(implementation), 0, abi.encodeWithSelector(Implementation.setOwner.selector, owner))` to delegatecall-overwrite slot 0 `_owner` back to previous owner.
6. transferOwnership's `require(currentOwner == owner())` passes since owner was restored.
**How found:** Noticed the LSP1 notification to pendingNewOwner occurs before the currentOwner == owner() guard, making the guard reentrancy-bypassable via acceptOwnership in the callback.
**Exploitation:** Single transaction: victim mistakenly nominates attacker contract as pending owner; attacker's universalReceiver callback accepts ownership, drains the account's LYX via execute CALL, then delegatecalls a contract writing slot 0 to restore the original owner, leaving the account drained and ownership seemingly unchanged.
**Escalation:** Delegatecall into self-destructing contract could permanently destroy the account; otherwise owner slot overwrite hides the takeover.
**Insight:** When a two-step ownership transfer notifies the pending owner via a callback before validating state, treat the callback as attacker-controlled reentrancy: check whether acceptOwnership can run inside it and whether the post-callback invariant check can be satisfied by the attacker restoring state.

