Farseen0/scar-pairs huggingface dataset:
MEDIUM severity: InvestmentManager can use `AccountableFixedTerm::coverDefault` to misuse token approvals from anyone. **Description:** `AccountableFixedTerm::coverDefault` allows InvestmentManager of the loan to add additional assets to the system.

And `lockAssets()` pulls assets from the input "provider" address, transferring them to the vault.

This means any user address who had asset token balance, and...

Vulnerable code:
function coverDefault(uint256 assets, address provider) external onlySafetyModuleOrManager whenNotPaused {
        _requireLoanInDefault();

        loanState = LoanState.InDefaultClaims;

        IAccountableVault(vault).lockAssets(assets, provider);

        emit DefaultCovered(safetyModule, provider, assets);
    }
HIGH severity: Auto-draw on `AccountableFixedTerm::pay` lets third parties force unwanted borrowing. In [`AccountableFixedTerm::pay`](https://github.com/Accountable-Protocol/audit-2025-09-accountable/blob/fc43546fe67183235c0725f6214ee2b876b1aac6/src/strategies/AccountableFixedTerm.sol#L354-L357), any positive `_loan.drawableFunds` are automatically drawn via `_updateAndRelease(drawableFunds)`...

Vulnerable code:
uint256 drawableFunds = _loan.drawableFunds;
if (drawableFunds > 0) {
    _updateAndRelease(drawableFunds);
}
HIGH severity: Frequent `AccountableOpenTerm::accrueInterest` calls reduce interest accrual. **Description:** In [`AccountableOpenTerm::_linearInterest`](https://github.com/Accountable-Protocol/audit-2025-09-accountable/blob/fc43546fe67183235c0725f6214ee2b876b1aac6/src/strategies/AccountableOpenTerm.sol#L602-L604), interest accrual uses integer math, `_linearInterest(rate, dt) = rate * dt.....

Vulnerable code:
function _linearInterest(uint256 interestRate, uint256 timeDelta) internal pure returns (uint256) {
    return interestRate.mulDiv(timeDelta, DAYS_360_SECONDS);
}

function test_interest_rounding_from_frequent_accrue_calls() public {
    vm.warp(1739893670);

    vm.prank(manager);
    usdcLoan.setPendingBorrower(borrower);
    vm.prank(borrower);
    usdcLoan.acceptBorrowerRole();

    // Use a common APR (15%) and short interval; depositPeriod = 0 to keep price logic simple.
    LoanTerms memory terms = LoanTerms({
        minDeposit: 0,
        minRedeem: 0,
        maxCapacity: USDC_AMOUNT,
        minCapacity: USDC_AMOUNT / 2,
        interestRate: 150_000,        // 15% APR in bps units
        interestInterval: 30 days,
        duration: 0,
        depositPeriod: 0,
        acceptGracePeriod: 0,
        lateInterestGracePeriod: 0,
        lateInterestPenalty: 0,
        withdrawalPeriod: 0
    });
    vm.prank(manager);
    usdcLoan.setTerms(terms);
    vm.prank(borrower);
    usdcLoan.acceptTerms();

    // Provide principal so interest accrues on outstanding assets.
    vm.prank(alice);
    usdcVault.deposit(USDC_AMOUNT, alice, alice);

    // Snapshot the baseline state just after start.
    uint256 snap = vm.snapshot();

    // ------------------------------------------------------------
    // Scenario A: "Spam accrual" — call accrueInterest() every 12s for 1 hour.
    // Each 12s step yields baseRate = rate * 12 / 360d ≈ 0 (integer), but _accruedAt is reset,
    // so we lose that fractional time forever.
    // ------------------------------------------------------------
    uint256 step = 180;          // 3 minutes
    uint256 total = 3600;        // 1 hour
    uint256 n = total / step;    // 300 iterations

    for (uint256 i = 0; i < n; i++) {
        vm.warp(block.timestamp + step);
        usdcLoan.accrueInterest(); // returns new scale but we just trigger the reset
    }

    // Capture the resulting scale factor after the spammy accrual pattern
    uint256 sfSpam = usdcLoan.accrueInterest(); // one more call just to read the value

    // ------------------------------------------------------------
    // Scenario B: Single accrual after the same total wall-clock time.
    // ------------------------------------------------------------
    vm.revertTo(snap);
    vm.warp(block.timestamp + total);
    uint256 sfClean = usdcLoan.accrueInterest();

    // Expect the spammed path to have strictly lower scale factor than the clean path.
    assertLt(sfSpam, sfClean, "frequent zero-delta accrual bleeds interest vs single accrual");

    // Anything more often than 207 in this case will result in no interest growth at all.
    assertEq(sfSpam, 1e36, "frequent accruals yield no interest growth");
}
HIGH severity: Invalid `maxWithdraw()` check in `withdraw()`. **Description:** Vault incorrectly checks `maxWithdraw(receiver)` instead of `maxWithdraw(controller/owner)`.

**Impact:**
- Allows unauthorized withdrawals by exploiting the receiver's limits instead of the owner's.
- DDoS in `withdraw()`

**Proof of Concept:** ❌ Violated:...

Vulnerable code:
// MUST NOT be higher than the actual maximum that would be accepted
rule eip4626_maxWithdrawNoHigherThanActual(env e, uint256 assets, address receiver, address owner) {

    setup(e);

    storage init = lastStorage;

    mathint limit = maxWithdraw(e, owner) at init;

    withdraw@withrevert(e, assets, receiver, owner) at init;
    bool reverted = lastReverted;

    // Withdrawals above the limit must revert
    assert(assets > limit => reverted, "Withdraw above limit MUST revert");
}

diff --git a/credit-vaults-internal/src/vault/AccountableAsyncRedeemVault.sol b/credit-vaults-internal/src/vault/AccountableAsyncRedeemVault.sol
index a64f47c..c8824bb 100644
--- a/credit-vaults-internal/src/vault/AccountableAsyncRedeemVault.sol
+++ b/credit-vaults-internal/src/vault/AccountableAsyncRedeemVault.sol
@@ -173,7 +173,7 @@ contract AccountableAsyncRedeemVault is IAccountableAsyncRedeemVault, Accountabl
     function withdraw(uint256 assets, address receiver, address controller) public onlyAuth returns (uint256 shares) {
         _checkController(controller);
         if (assets == 0) revert ZeroAmount();
-        if (assets > maxWithdraw(receiver)) revert ExceedsMaxRedeem();
+        if (assets > maxWithdraw(controller)) revert ExceedsMaxRedeem(); // @certora FIX for eip4626_maxWithdrawNoHigherThanActual (receiver -> controller)

         VaultState storage state = _vaultStates[controller];
         shares = _convertToShares(assets, state.withdrawPrice, Math.Rounding.Floor);
HIGH severity: Missing controller validation in `AccountableAsyncRedeemVault::requestRedeem` allows zero address state. **Description:** The `requestRedeem()` function fails to call `_checkController(controller)` validation, allowing the zero address to accumulate vault state.

**Impact:** `zeroControllerEmptyState` violation.

**Proof of Concept:** ❌ Violated:...

Vulnerable code:
// Zero address must have empty state for all vault fields
invariant zeroControllerEmptyState(env e)
    ghostVaultStatesMaxMint256[0] == 0 &&
    ghostVaultStatesMaxWithdraw256[0] == 0 &&
    ghostVaultStatesDepositAssets256[0] == 0 &&
    ghostVaultStatesRedeemShares256[0] == 0 &&
    ghostVaultStatesDepositPrice256[0] == 0 &&
    ghostVaultStatesMintPrice256[0] == 0 &&
    ghostVaultStatesRedeemPrice256[0] == 0 &&
    ghostVaultStatesWithdrawPrice256[0] == 0 &&
    ghostVaultStatesPendingDepositRequest256[0] == 0 &&
    ghostVaultStatesPendingRedeemRequest256[0] == 0 &&
    ghostVaultStatesClaimableCancelDepositRequest256[0] == 0 &&
    ghostVaultStatesClaimableCancelRedeemRequest256[0] == 0 &&
    !ghostVaultStatesPendingCancelDepositRequest[0] &&
    !ghostVaultStatesPendingCancelRedeemRequest[0] &&
    ghostRequestIds128[0] == 0
filtered { f -> !EXCLUDED_FUNCTION(f) } { preserved with (env eFunc) { SETUP(e, eFunc); } }

diff --git a/credit-vaults-internal/src/vault/AccountableAsyncRedeemVault.sol b/credit-vaults-internal/src/vault/AccountableAsyncRedeemVault.sol
index 4cd0a3e..a64f47c 100644
--- a/credit-vaults-internal/src/vault/AccountableAsyncRedeemVault.sol
+++ b/credit-vaults-internal/src/vault/AccountableAsyncRedeemVault.sol
@@ -113,6 +113,9 @@ contract AccountableAsyncRedeemVault is IAccountableAsyncRedeemVault, Accountabl
         onlyAuth
         returns (uint256 requestId)
     {
+        // @certora FIX for zeroControllerEmptyState
+        _checkController(controller);
+
         _checkOperator(owner);
         _checkShares(owner, shares);
LOW severity: Reserved assets could be extracted from the Vault. **Description:** Some strategy functions can release assets without checking if those assets are part of `reservedLiquidity`. `AccountableFixedTerm._loan.drawableFunds` is not verified to be in sync with the queue `reservedLiquidity`. Hence the borrower can inadvertently borrow more funds than...

Vulnerable code:
// Reserved liquidity must not exceed total assets
invariant reservedLiquidityBacked(env e)
    ghostReservedLiquidity256 <= ghostTotalAssets256
HIGH severity: `Authorizable::_verify` should use EIP-712 typed structured data hashing. **Description:** [`Authorizable::_verify`](https://github.com/Accountable-Protocol/audit-2025-09-accountable/blob/fc43546fe67183235c0725f6214ee2b876b1aac6/src/access/Authorizable.sol#L46-L78) signs ad-hoc payloads that include `chainId`, but the flow is not...

Vulnerable code:
struct TxAuthData {
      bytes   functionCallData;   // selector + encoded args
      address contractAddress;    // target contract (can be redundant with domain; decide and document)
      address account;            // controller / signer subject
      uint256 nonce;              // per-account nonce
      uint256 blockExpiration;    // deadline
  }

  bytes32 constant TXAUTH_TYPEHASH = keccak256(
      "TxAuthData(bytes functionCallData,address contractAddress,address account,uint256 nonce,uint256 blockExpiration)"
  );

bytes32 structHash = keccak256(abi.encode(
      TXAUTH_TYPEHASH,
      keccak256(txAuth.functionCallData), // hash dynamic bytes
      txAuth.contractAddress,
      txAuth.account,
      txAuth.nonce,
      txAuth.blockExpiration
  ));
  bytes32 digest = _hashTypedDataV4(structHash);
  require(
      SignatureChecker.isValidSignatureNow(signer, digest, signature),
      "INVALID_SIGNATURE"
  );
HIGH severity: Deployment script requires unencrypted private key. **Description:** The deployment scripts [`FactoryScript.s.sol`](https://github.com/Accountable-Protocol/audit-2025-09-accountable/blob/fc43546fe67183235c0725f6214ee2b876b1aac6/script/FactoryScript.s.sol) and...

Vulnerable code:
uint256 deployerPk = vm.envUint("DEPLOYER_TESTNET_PK");

forge script script/Deploy.s.sol:DeployScript \
    --rpc-url "$RPC_URL" \
    --broadcast \
    --account deployerKey \
    --sender <address associated with deployerKey> \
    -vvv
HIGH severity: ERzero amount transfer rejection. **Description:** The `_checkTransfer` function reverts on zero-amount transfers, violating ERC-20 standard which mandates that transfers of 0 values [MUST be treated](https://eips.ethereum.org/EIPS/eip-20#transfer) as normal transfers.

**Impact:** Violation of `eip20_transferSupportZeroAmount`...

Vulnerable code:
// EIP20-06: Verify transfer() handles zero amount transfers correctly
// EIP-20: "Transfers of 0 values MUST be treated as normal transfers and fire the Transfer event."
rule eip20_transferSupportZeroAmount(env e, address to, uint256 amount) {

    setup(e);

    // Perform transfer
    transfer(e, to, amount);

    // Zero amount transfers must succeed
    satisfy(amount == 0);
}

// EIP20-09: Verify transferFrom() handles zero amount transfers correctly
// EIP-20: "Transfers of 0 values MUST be treated as normal transfers and fire the Transfer event."
rule eip20_transferFromSupportZeroAmount(env e, address from, address to, uint256 amount) {

    setup(e);

    // Perform the transferFrom
    transferFrom(e, from, to, amount);

    // Zero amount transferFrom must succeed
    satisfy(amount == 0);
}

diff --git a/credit-vaults-internal/src/vault/AccountableVault.sol b/credit-vaults-internal/src/vault/AccountableVault.sol
index 629b6d0..fb3676a 100644
--- a/credit-vaults-internal/src/vault/AccountableVault.sol
+++ b/credit-vaults-internal/src/vault/AccountableVault.sol
@@ -141,7 +141,8 @@ abstract contract AccountableVault is IAccountableVault, ERC20, AccessBase {

     /// @dev Checks transfer restrictions before executing the underlying transfer
     function _checkTransfer(uint256 amount, address from, address to) private {
-        if (amount == 0) revert ZeroAmount();
+        // @certora FIX for eip20_transferSupportZeroAmount and eip20_transferFromSupportZeroAmount
+        // if (amount == 0) revert ZeroAmount();
         if (!transferableShares) revert SharesNotTransferable();
         if (!isVerified(to, msg.data)) revert Unauthorized();
         if (throttledTransfers[from] > block.timestamp) revert TransferCooldown();
HIGH severity: State changes without events. There are state variable changes in this function but no event is emitted. Consider emitting an event to enable offchain indexers to track the changes.

- [Line: 47](https://github.com/Accountable-Protocol/audit-2025-09-accountable/blob/fc43546fe67183235c0725f6214ee2b876b1aac6/src/modules/GlobalRegi...

Vulnerable code:
function setSecurityAdmin(address securityAdmin_) external onlyOwner {

function setOperationsAdmin(address operationsAdmin_) external onlyOwner {

function setTreasury(address treasury_) external onlyOwner {
HIGH severity: Storage read optimizations. **Description:**
1. [`AccountableOpenTerm::_calculateRequiredLiquidity`](https://github.com/Accountable-Protocol/audit-2025-09-accountable/blob/fc43546fe67183235c0725f6214ee2b876b1aac6/src/strategies/AccountableOpenTerm.sol#L642-L655): `vault` and `_scaleFactor` can be cached. Also consider...

Vulnerable code:
function maxWithdraw(address receiver) public view override returns (uint256 maxAssets) {
        VaultState storage state = _vaultStates[receiver];
        maxAssets = state.maxWithdraw;
        if (state.redeemShares == 0) return 0;
    }
HIGH severity: Native HYPE transfers to HyperCore will not work. **Description:** `Hype_Module::hyper_depositSpot` always calls `IERC20(token).transfer(assetAddress(uint64(asset)), _wei)`, including when `asset == 150` (native HYPE). Per HyperCore [docs](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/hypercore-less-than-greater-than-hyper...

Vulnerable code:
function hyper_depositSpot(
          address token,
          uint32 asset,
          uint64 _wei
    - ) external onlyRole(EXECUTOR_ROLE) nonReentrant {
    + ) external payable onlyRole(EXECUTOR_ROLE) nonReentrant {
    +     if (asset == 150) {
    +         require(token == address(0), "token must be zero for HYPE");
    + 	      require(msg.value == _wei, "msg.value mismatch");
    +         (bool ok, ) = address(0x2222222222222222222222222222222222222222).call{value: _wei}("");
    + 	      require(ok, "native HYPE transfer failed");
    +         return;
    +     }

          IERC20(token).transfer(assetAddress(uint64(asset)), _wei);
      }
HIGH severity: Unsafe ERtransfers. **Description:** `Hype_Module::hyper_depositSpot` uses `IERC20(token).transfer(...)` directly and ignores the return value. Many ERC-20s are non-standard (e.g., USDT) and either don’t return `bool` or revert on failure in non-obvious ways, making bare `transfer`/`transferFrom` unsafe.

**Impact:**.....

Vulnerable code:
using SafeERC20 for IERC20;

IERC20(token).safeTransfer(assetAddress(uint64(asset)), amount);
HIGH severity: Remove loop when sending actions. **Description:** `Hype_Module::sendAction` manually allocates and copies a 4-byte header plus payload in a loop. Use packed encoding to avoid the loop and shrink bytecode, e.g.:

This builds the prefix + payload in one go with lower gas and less code.

**D2:** Fixed in commit...

Vulnerable code:
bytes memory data = abi.encodePacked(bytes4(uint32(0x01000000) | uint32(actionIndex)), action);
HIGH severity: Calling `StakingVault::notifyRewardAmount` on empty vault leaves ILV stuck. **Description:** `StakingVault::notifyRewardAmount` is the vault’s reward hook that updates the rewards-per-share accumulator (`accIlvPerShare`) so stakers can later claim ILV. It is called by [`L2RevenueDistributorV3::_applyAllocation`](https://github.com/0xKaizenLabs/staking-contracts-v3/blob/c786...

Vulnerable code:
if (pool.kind == PoolKind.Vault) {
    // Transfer ILV to the vault and notify
    ilv.safeTransfer(pool.recipient, amount);
    IStakingVaultMinimal(pool.recipient).notifyRewardAmount(amount);
} else {

function notifyRewardAmount(uint256 ilvAmount)
    external
    override
    nonReentrant
    whenNotPaused
    onlyRole(DISTRIBUTOR_ROLE)
{
    // If no rewards are provided, return
    if (ilvAmount == 0) return;
HIGH severity: Deployment script requires unencrypted private key. **Description:** The [Makefile](https://github.com/0xKaizenLabs/staking-contracts-v3/blob/c78653ed5f2e5a6d5ace13c303a8765fe30679b0/Makefile)’s deployment targets require a raw private key to be supplied via an environment variable and pass it directly on the command line to `forge`:

Supplying...

Vulnerable code:
# Guard (fails if PRIVATE_KEY not provided)
@if [ -z "$(PRIVATE_KEY)" ]; then \
    echo "$(RED)ERROR: PRIVATE_KEY not set$(NC)"; \
    exit 1; \
fi

# Usage (Sepolia)
forge script script/Deployer.s.sol:Deployer \
  --rpc-url $(BASE_SEPOLIA_RPC) \
  --private-key $(PRIVATE_KEY) \
  --broadcast \
  --verify \
  --etherscan-api-key $(BASESCAN_API_KEY) \
  -vvvv

# Usage (Mainnet)
forge script script/Deployer.s.sol:Deployer \
  --rpc-url $(BASE_RPC) \
  --private-key $(PRIVATE_KEY) \
  --broadcast \
  --verify \
  --etherscan-api-key $(BASESCAN_API_KEY) \
  --slow \
  -vvvv

DEPLOYER_ACCOUNT ?= deployerKey
DEPLOYER_SENDER  ?= $(shell cast wallet address $(DEPLOYER_ACCOUNT) 2>/dev/null)

forge script script/Deployer.s.sol:Deployer \
  --rpc-url $(BASE_SEPOLIA_RPC) \
  --account $(DEPLOYER_ACCOUNT) \
  --sender $(DEPLOYER_SENDER) \
  --broadcast \
  --verify \
  --etherscan-api-key $(BASESCAN_API_KEY) \
  -vvvv

cast wallet list | grep -q "$(DEPLOYER_ACCOUNT)" || { \
  echo "$(RED)ERROR: Keystore account '$(DEPLOYER_ACCOUNT)' not found$(NC)"; exit 1; }
LOW severity: Dust amounts of rewards get stuck in the Vault. **Description:** In `StakingVault`, rewards are distributed through the `notifyRewardAmount` function. However, due to the current implementation, some rewards can remain stuck in the contract because of rounding errors.

For example:

* User1 stakes `1e18`
* User2 stakes `7e18`
* The total reward.....

Vulnerable code:
function notifyRewardAmount(uint256 ilvAmount)
        external
        override
        nonReentrant
        whenNotPaused
        onlyRole(DISTRIBUTOR_ROLE)
    {
        // If no rewards are provided, return
        if (ilvAmount == 0) return;

        // Cache total staked for gas efficiency
        uint256 _totalStaked = totalStaked;

        // When no one is staking, return (prevents division by zero)
        if (_totalStaked == 0) {
            return;
        }

        // Update rewards-per-share accumulator
        accIlvPerShare += (ilvAmount * Constants.ACC_PRECISION) / _totalStaked;

        emit RewardsNotified(ilvAmount, accIlvPerShare);
    }

function test_twoUsersWhoStakeAreEligibleForAllRewards() public {
    address user = makeAddr("User1");
    address user2 = makeAddr("User2");
    uint96 amount = 1e18;
    uint32 duration = 31 days;
    uint96 notifyAmt = 1e18;

    // User 1 and User 2 deposit
    approveAndDeposit(user, amount, duration);
    approveAndDeposit(user2, amount * 6, duration);

    // Fund and notify rewards
    ilv.mint(address(vault), notifyAmt);
    vm.prank(admin);
    vault.notifyRewardAmount(notifyAmt);

    // Check if total pending rewards equal notifyAmt
    uint256 pending = vault.pendingRewards(user);
    uint256 pending2 = vault.pendingRewards(user2);
    assertEq(pending + pending2, notifyAmt);
}
LOW severity: Distributor can over-allocate claimables, causing temporary claim DoS. **Description:** `L2RevenueDistributorV3::distribute` credits claimable pools by incrementing `pendingIlv` but does not reserve tokens for those future claims. Subsequent distributions that transfer ILV out to vaults can consume the same on-chain balance the claimables depend on. This can leave...

Vulnerable code:
/// PoC: Overcommit claimables via two distributions -> claim reverts (insufficient balance)
    function test_PoC_Overcommit_BricksClaimables_TwoDistributes() public {
        // 1) Seed distributor
        uint256 amount1 = 1_000e18; // clean number to avoid rounding noise
        ilv.mint(address(distributor), amount1);

        // 2) First distribution. Pool 2 (claimable, 2000 bps) accrues 20% pending; 80% sent to vaults.
        vm.prank(operator);
        distributor.distribute(amount1);

        uint256 expectedPendingAfter1 = (amount1 * 2000) / Constants.BPS_DENOMINATOR;
        assertEq(pendingOf(2), expectedPendingAfter1, "pending after first distribute");
        assertEq(ilv.balanceOf(address(distributor)), expectedPendingAfter1, "residual ILV equals claimable pending");

        // 3) Naive second distribution using the entire remaining balance.
        uint256 amount2 = ilv.balanceOf(address(distributor)); // == expectedPendingAfter1
        vm.prank(operator);
        distributor.distribute(amount2);

        // Now: balance < pending (overcommitted)
        uint256 pendingNow = pendingOf(2); // 20% of amount1 + 20% of amount2
        uint256 balNow = ilv.balanceOf(address(distributor));
        assertLt(balNow, pendingNow, "distributor balance < claimable pending");

        vm.prank(admin);
        distributor.setClaimAllowlist(2, address(this), true);

        // 4) Claims are bricked until top-up
        vm.expectRevert("balance"); // SafeERC20 transfer fails due to insufficient ILV
        distributor.claimPool(2, address(this));
    }

uint256 public reservedClaimables;

function _applyAllocation(Pool storage pool, uint256 amount) internal {
      if (pool.kind == PoolKind.Vault) {
          ilv.safeTransfer(pool.recipient, amount);
          IStakingVaultMinimal(pool.recipient).notifyRewardAmount(amount);
      } else {
          pool.pendingIlv += amount;
          reservedClaimables += amount;
      }
  }

  function claimPool(uint256 id, address to) external nonReentrant whenNotPaused {
      // ...existing checks...
      uint256 amount = pool.pendingIlv;
      if (amount == 0) revert NothingToClaim(id);
      pool.pendingIlv = 0;
      reservedClaimables -= amount;     // reduce reserved on successful claim
      ilv.safeTransfer(to, amount);
      emit PoolClaimed(id, to, amount);
  }
LOW severity: Dust rewards can become permanently locked. **Description**
During `distribute()`, leftover amounts (“dust”) can remain as a remainder. These remainders are attempted to be allocated to the first eligible participant. However, if the remainder is **smaller than `totalStaked / 1e18`**, the reward is not distributed and instead gets...

Vulnerable code:
if (rwDistributed < remainder) {
    uint256 dust = remainder - rwDistributed;
    rwDistributed += _allocateFirstEligible(dust, poolCount, units);
}

function _applyAllocation(Pool storage pool, uint256 amount) internal {
    // skip if dust is too small to be distributed
    if (amount * Constants.ACC_PRECISION <= pool.totalStaked) {
        return;
    }

    if (pool.kind == PoolKind.Vault) {
        ilv.safeTransfer(pool.recipient, amount);
        IStakingVaultMinimal(pool.recipient).notifyRewardAmount(amount);
    } else {
        pool.pendingIlv += amount;
    }
}
LOW severity: Equal distribution fallback logic error. **Description:** In the `L2RevenueDistributorV3::_allocateEqual()` function, when the total reserve units are zero, the contract falls back to equal distribution of remaining ILV to active reserve-weighted pools. However, the eligibility check for Vault pools includes the condition `units[i] > 0`,.....

Vulnerable code:
bool isEligible = (pool.kind == PoolKind.Claimable) || (pool.kind == PoolKind.Vault);