# Audit Voting Manager Optimised

**Client:** iAero Protocol **Date:** 2025-09-05 **Audited artifact:** `VotingManagerOptimised.sol` (Solidity ^0.8.24) **Standards/Libraries:** OpenZeppelin v5 series (`Ownable`, `ReentrancyGuard`, `Pausable`, `Math`, `SafeERC20`) **External deps:**

* `IVoter` / `IVoterTime` (Aerodrome-style voting window & epoch math)
* `IPermalockVault` (selector-allowlisted `executeNFTAction`)
* Chainlink-style `AggregatorV3Interface` price feeds

***

## 1) Executive Summary

`VotingManagerOptimised` manages epoch-based voting for a custodial veNFT (held by an external PermalockVault) and handles bribe deposits/refunds in ERC-20 and ETH with USD thresholding via oracles. Keeper roles execute votes either with explicit weights or via an auto-allocation algorithm that uses bribes + base revenue signals. Per-epoch records enable claims to treasury when a pool was actually voted; otherwise depositors may refund after a grace period. Storage is partitioned by `(pool, epochId)` and includes pruning.

**Assessment:** The contract exhibits a strong baseline: pervasive `nonReentrant`, CEI discipline, fee-on-transfer-safe ERC20 intake (balance diff), oracle validation (stale/round checks + decimals normalization), per-epoch mutexing, and explicit refund/claim windows. Observed risks are bounded and acceptable given the client’s stated risk appetite.

**Overall risk:** Low–Medium.

***

## 2) In-Scope Components

* `VotingManagerOptimised` (full contract)
* External interfaces assumed correct: `IVoter`, `IVoterTime`, `IPermalockVault`, `AggregatorV3Interface`.
* Out of scope: concrete voter/gauge implementations, concrete vault implementation, token contracts, oracle deployments.

***

## 3) Threat Model & Assumptions

* **Admin/keeper honesty:** Owner is trusted; keepers are semi-trusted (can add pools/execute votes within contract rules).
* **Vault policy:** `IPermalockVault.executeNFTAction` enforces a **per-target selector allowlist** such that only `IVoter.reset` and `IVoter.vote` are permitted for `voter`. Any deviation would be operational, not a flaw here.
* **Oracles:** USD price oracles are configured correctly, up-to-date, and correspond to the intended tokens (or ETH at `address(0)`).
* **voter/epoch schedule:** `IVoterTime` returns sensible epoch boundaries (weekly cadence).
* **Tokens:** Standard ERC-20 semantics; no malicious reentrancy from token callbacks (we use `nonReentrant` and CEI regardless).

***

## 4) Methodology

* Manual line-by-line review for access control, reentrancy, CEI, math/overflow, oracle handling, epoch arithmetic, and storage growth.
* Adversarial reasoning for bribe lifecycle (deposit → claim/refund → prune), keeper voting flows, and external call rollback behavior.
* Gas and UX notes where they intersect with safety.

***

## 5) Findings

Severity scale: **C**ritical / **H**igh / **M**edium / **L**ow / **I**nformational

### M-1 — Allocation reducer can over-shrink a pool during final canonicalization

**Location:** `_calculateOptimalAllocation()` — final “reduce to exactly 10\_000 bps” branch. **Issue:** When total weights exceed 10\_000 bps, the reducer chooses largest donors and may skip donors that cannot give without violating `minVoteWeightBPS`. The current approach can set a donor’s allocation to `0` temporarily while searching alternative donors, which can unintentionally persist a zero allocation. **Impact:** Minor allocation distortion vs policy intent; not a safety issue. **Recommendation:** Use a non-mutating skip of ineligible donors and compute `take = min(excess, alloc[i] - minVoteWeightBPS)`; do not write `0` to “skip.” **Client stance:** Acceptable.

***

### M-2 — No local voting window pre-check

**Location:** `executeVotesWithWeights`, `executeVotesAuto`. **Issue:** The functions rely on `IVoter` to revert if outside the voting window. **Impact:** Operational (wasted gas, noisier ops). **Recommendation:** Add `require(inVotingWindow(), "not in voting window")` before attempting actions. **Client stance:** Acceptable.

***

### L-1 — Epoch start underflow guard is implicit

**Location:** `_epochStart(t) = epochVoteStart(t) - 1 hours`. **Issue:** If external schedule ever returned `≤ 1 hour`, subtraction would underflow. **Impact:** View/function revert if upstream schedule is misconfigured. **Recommendation:** Add `require(vs > 1 hours, "bad schedule")` defensively. **Client stance:** Acceptable.

***

### I-1 — Operational dependency: vault allowlist must enable `reset` and `vote`

**Location:** Calls via `IPermalockVault.executeNFTAction` to `voter`. **Issue:** If the vault’s selector allowlist is not properly seeded (or later restricted), votes will revert. **Impact:** Operational availability only. **Recommendation:** Codify allowlist seeding in deployment runbooks/multisig scripts; monitor events. **Client stance:** Understood.

***

### I-2 — Pruning reorders bribe slices

**Location:** `pruneBribes` (swap-and-pop). **Issue:** Intended; indices are not promised stable. **Impact:** UI/indexing should not assume stable ordering. **Recommendation:** Document for integrators. **Client stance:** Acceptable.

***

## 6) Positive Observations

* **Reentrancy:** All state-mutating externals use `nonReentrant`; reward/refund payers use CEI and avoid external state dependencies.
* **Fee-on-transfer safety:** ERC-20 bribe intake uses `before/after` balance-diff to compute `received`.
* **Oracle hygiene:** Validates `enabled`, `feed != 0`, `answer > 0`, `answeredInRound >= roundId`, and staleness; normalizes decimals to 1e18.
* **Refund safety:** Refunds require (epoch ended + grace) AND (not executed OR executed but pool not voted). Prevents treasury double-spend collisions.
* **Emergency withdrawal policy:** Blocks ETH and any **allowed** bribe tokens from sweeping—protects refundable user funds.
* **Mutex & rollback:** `epochLock` prevents concurrent votes; `try/catch` reverts with lock cleanup.

***

## 7) Recommended Tests (High-value)

1. **Bribe lifecycle (ERC-20 & ETH):** deposit across multiple epochs; enforce per-epoch USD minimums; ensure claims are transferable only when pool voted; refunds after grace only when not voted (or not executed).
2. **Oracle decimals & staleness:** feeds with 6/8/18 decimals; stale feed → revert paths.
3. **Auto allocation edges:**
   * Zero scores → revert.
   * Sum < 100% → top-up best pool.
   * Sum > 100% → reducer keeps non-zero pools above `minVoteWeightBPS`.
   * Cap & min interplay; largest-remainder distribution respects cap.
4. **Voting window:** success inside window; revert via downstream outside window.
5. **Mutex failure:** Force a revert in the second `executeNFTAction` call and assert `epochLock` resets.

***

## 8) Compatibility & Deployment Notes

* **Solidity:** ^0.8.24 is compatible with OZ v5.
* **EVMs:** No assembly beyond event array shrinking in a view; standard opcodes only.
* **Clients (ethers v6):** ABI surface is standard; large arrays handled in views.
* **Ops:** Ensure vault allowlist grants `IVoter.reset` and `IVoter.vote` selectors for the configured `voter`. Keepers should be managed via multisig and can be rotated.

***

## 9) Conclusion

From a security perspective, `VotingManagerOptimised` is well-structured and appropriate for production given your stated risk tolerance. The identified issues are mainly **operational or allocation-policy nits** rather than exploitable vulnerabilities. If you later want to harden UX and allocation determinism, the suggested patches are straightforward and do not alter core behavior.

**Final rating:** **Low–Medium risk** (acceptable).

## VotingManagerOptimised — Function Table

**Purpose:** Bribe intake, oracle‑valued scoring, and execution of Aerodrome votes via the Vault’s veNFT. Handles **refunds** for unused bribes and **treasury collection** for used bribes.

### Core Parameters

* `minBribeUSDPerEpoch` (default `10e18`)
* `bribeDiscountBPS` (default `10_000`=100%)
* `maxPoolAllocationBPS` (default `7_000`=70%)
* `minVoteWeightBPS` (default `5`=0.05%)
* `refundGraceSeconds` (default 1 day after epoch end)

### Access Control

* **Owner:** pause/unpause, oracles, allowed bribe tokens, keeper set, global params, remove pools, emergency withdraw (restricted).
* **Keeper:** add pools, set base revenue per epoch, execute votes (auto or manual weights).

### External/Public Functions

| Function                       | Signature                                                                                                                                                                                                                     |           Access |                                   Guards | Notes                                                                                               |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------: | ---------------------------------------: | --------------------------------------------------------------------------------------------------- |
| **constructor**                | `(address vault, address voter, address treasury)`                                                                                                                                                                            |                — |                                        — | Enables ETH as bribe token by default; still need **ETH oracle**.                                   |
| **setOracle**                  | `(address token, address feed, uint48 maxStaleSec, bool enabled)`                                                                                                                                                             |        **Owner** |                                        — | For `token=address(0)` configures **ETH/USD**.                                                      |
| **batchConfigureOracles**      | `(address[] tokens, address[] feeds, uint48[] maxStale, bool[] enabled, bool alsoSetAllowed)`                                                                                                                                 |        **Owner** |                                        — | Optional `alsoSetAllowed` to set `allowedBribeTokens`.                                              |
| **batchSetAllowedBribeTokens** | `(address[] tokens, bool[] allowed)`                                                                                                                                                                                          |        **Owner** |                                        — | Controls accepted bribe tokens.                                                                     |
| **setAllowedBribeToken**       | `(address token, bool allowed)`                                                                                                                                                                                               |        **Owner** |                                        — | Single‑token allow/disallow.                                                                        |
| **getPriceUSD**                | `(address token) → uint256 1e18`                                                                                                                                                                                              |           Public |                                     view | Reverts if oracle missing/stale/bad.                                                                |
| **getOracleMeta**              | `(address token) → (feed, maxStale, enabled, decimals)`                                                                                                                                                                       |           Public |                                     view | Helper for UI/ops.                                                                                  |
| **activePoolsSlice**           | `(uint256 start, uint256 max) → address[]`                                                                                                                                                                                    |           Public |                                     view | Paginates active pools.                                                                             |
| **currentEpochId**             | `() → uint256`                                                                                                                                                                                                                |           Public |                                     view | Epoch start timestamp (aligned).                                                                    |
| **isEpochStart**               | `(uint256 ts) → bool`                                                                                                                                                                                                         |           Public |                                     view | Epoch alignment helper.                                                                             |
| **inVotingWindow**             | `() → bool`                                                                                                                                                                                                                   |           Public |                                     view | Uses Aerodrome Voter window.                                                                        |
| **depositBribe**               | `(address pool, address token, uint256 amount, uint256 epochs)`                                                                                                                                                               |           Public |          **nonReentrant, whenNotPaused** | ERC‑20 path; splits across `epochs` (1–8); each slice must meet `minBribeUSDPerEpoch`.              |
| **depositETHBribe**            | `(address pool, uint256 epochs)`                                                                                                                                                                                              |           Public | **payable, nonReentrant, whenNotPaused** | Requires ETH oracle to be configured/enabled.                                                       |
| **claimTreasuryBribes**        | `(address pool, uint256 epochId, uint256 start, uint256 maxCount)`                                                                                                                                                            |           Public |                         **nonReentrant** | Only if `epoch.executed && pool was voted`; transfers slices to Treasury. Chunked.                  |
| **refundMyBribes**             | `(address pool, uint256 epochId, uint256 start, uint256 maxCount)`                                                                                                                                                            |           Public |                         **nonReentrant** | After `epoch + WEEK + refundGraceSeconds` and if not consumed; refunds caller’s slices.             |
| **pruneBribes**                | `(address pool, uint256 epochId, uint256 maxScan)`                                                                                                                                                                            |           Public |                         **nonReentrant** | Compacts paid/refunded entries (swap‑and‑pop).                                                      |
| **executeVotesWithWeights**    | `(address[] pools, uint256[] weightsBps)`                                                                                                                                                                                     | **Keeper/Owner** |          **nonReentrant, whenNotPaused** | Validates weights (min/cap) and exact 10\_000 sum. Resets then votes via Vault.                     |
| **executeVotesAuto**           | `()`                                                                                                                                                                                                                          | **Keeper/Owner** |          **nonReentrant, whenNotPaused** | Computes proportional weights from **(discounted bribes + base revenue)** with min/cap, then votes. |
| **addPools**                   | `(address[] pools)`                                                                                                                                                                                                           | **Keeper/Owner** |                                        — | Adds active pools (checks gauge exists & `isAlive`).                                                |
| **removePool**                 | `(address pool)`                                                                                                                                                                                                              |        **Owner** |                                        — | Removes from active set.                                                                            |
| **setKeeper**                  | `(address who, bool status)`                                                                                                                                                                                                  |        **Owner** |                                        — | Grants operator rights.                                                                             |
| **setParams**                  | `(uint256 minBribeUSDPerEpoch, uint256 bribeDiscountBPS, uint256 maxPoolAllocationBPS, uint256 minVoteWeightBPS)`                                                                                                             |        **Owner** |                                        — | Caps: discount ≤ 100%; cap ≤ 100%; min within (0,100%].                                             |
| **setRefundGrace**             | `(uint256 seconds_)`                                                                                                                                                                                                          |        **Owner** |                                        — | Refund grace after epoch end.                                                                       |
| **setBaseRevenueForEpoch**     | `(address[] pools, uint256[] epochIds, uint256[] usdAmounts)`                                                                                                                                                                 | **Keeper/Owner** |                                        — | Requires `isEpochStart(epochId)`; stored in USD 1e18.                                               |
| **pause / unpause**            | `()`                                                                                                                                                                                                                          |        **Owner** |                                        — | Halts bribe deposits and voting (claims/refunds use time guards).                                   |
| **emergencyWithdraw**          | `(address token, uint256 amount)`                                                                                                                                                                                             |        **Owner** |                         **nonReentrant** | **Forbidden** for ETH and **allowed bribe tokens**. For stranded ops funds only.                    |
| **view helpers**               | `getActivePools, getPoolInfo, getBribeCount, getBribes, getBribeAt, nextUnpaidBribeIndex, canRefund, nextRefundableBribeIndex, previewClaimTotals, getOptimalAllocation, wasExecuted, canExecuteVotes, getDepositedBribesUSD` |           Public |                                     view | Pagination and state introspection.                                                                 |
| **receive**                    | `()`                                                                                                                                                                                                                          |                — |                                        — | Accepts ETH bribes.                                                                                 |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.iaero.finance/technical-documentation/audit-votingmanageroptimised.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
