H-1: Arbitrary `raw_call` in LeverageZapper `_swap` allows trove theft
Summary:
An attacker can steal any unclaimed trove opened through the LeverageZapper by exploiting the unvalidated raw_call in _swap().
Description:
_swap() executes an arbitrary call with no validation on the target address or call selector:
raw_call(swap.router, swap.data) # no whitelist, no selector check
After open_leveraged_trove() (as well as trove leverage changes), the zapper owns the newly created trove until the user calls accept_ownership(). During this window, an attacker can call open_leveraged_trove() with crafted swap data such as abi.encode(transfer_ownership(victim_trove_id, attacker)).
The "swap" then executes transfer_ownership() on the TroveManager. Since msg.sender is the zapper and the zapper is the trove's owner, the call succeeds. The victim's pending_owner is overwritten to the attacker, who then calls accept_ownership() to steal the trove.
The attacker only needs enough collateral to open a minimum trove to make the overall transaction succeed.
Impact:
High. Direct theft of any leveraged trove that hasn't been claimed by its owner. The attacker pays only the cost of opening a minimum trove. All troves opened through the zapper are vulnerable unless users are able to atomically use the zapper and claim the trove which can be done with smart contract wallet and EIP-7702 compatible wallets but many wallets are still non-compatible and thus vulnerable.
Recommendation:
Validate the swap router against a whitelist, or replace raw_call with a typed swap interface that cannot target protocol contracts:
assert swap.router != params.trove_manager, "!router"
Developer Response:
Fixed in PR#13.