# Improper implementation of arbitraryCall() allows protocol gov to steal funds from users' wallets

arbitraryCall() only blocks callees with active incentives, so after claim the incentiveToken is callable, letting compromised gov transferFrom users' approved tokens.

- Payout: Audit contest
- Severity: high
- Program: Streaming Protocol
- Found by: WatchPug
- Disclosed: 2022-02-11
- Write-up quality: 6/10
- Original: https://github.com/code-423n4/2021-11-streaming-findings/issues/258
- Library page: https://bugbountylibrary.com/r/c4%3A2021-11-streaming%3AH-04
- Tags: area:web3 domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:solidity technique:source-review vuln:access-control vuln:accounting-error vuln:business-logic

**Target:** Locke.sol arbitraryCall() privileged function
**Root cause:** arbitraryCall() guards only on incentives[who]==0, but incentives[who] resets to 0 after claimIncentive(), so the incentiveToken itself becomes a valid callee for arbitrary external calls.
**Steps:**
1. Alice approves USDC to streaming contract and calls createIncentive() with 1,000 USDC
2. Stream ends; creator calls claimIncentive(), setting incentives[USDC]==0
3. Compromised gov calls `arbitraryCall(USDC, abi.encodeWithSelector(transferFrom, Alice, gov, allowance))`
4. require(incentives[USDC]==0) passes; USDC transferFrom drains Alice's wallet up to allowance
**How found:** Code review of arbitraryCall()'s incentive guard; noticed incentives[who] is zeroed after claimIncentive(), making the token address pass the require.
**Exploitation:** Malicious/compromised protocol governance invokes arbitraryCall() targeting the incentiveToken address with transferFrom calldata, pulling all approved USDC from victim wallets to itself.
**Insight:** When a privileged arbitrary-call primitive is gated by transient state (an incentive balance that resets to zero), the gate can be satisfied post-reset; check whether the guard's state can be driven to the allowed value by normal user actions.
