# Voting power miscalculation in balanceofnftat function

ZeroLocker.balanceOfNFTAt miscalculates historical voting power by subtracting a negative slope term, granting voting power to users who staked after a snapshot.

- Payout: Contest reward
- Severity: medium
- Program: Zerolend
- Credited: elhaj
- Disclosed: 2024-09-16
- Write-up quality: 6/10
- Original: https://cdn.cantina.xyz/reports/cantina_competition_zerolend_jan2024.pdf#3.2.13
- Library page: https://bugbountylibrary.com/r/cantina%3Azerolend_jan2024%3A3.2.13
- Tags: domain:web3 impact:integrity kind:contest platform:cantina sev:medium tech:solidity technique:source-review vuln:business-logic vuln:governance vuln:oracle-manipulation

**Target:** ZeroLocker.balanceOfNFTAt governance voting power view function
**Root cause:** _balanceOfNFT uses the latest checkpoint's bias/slope and subtracts slope*(_t - lastPoint.ts); when _t precedes lastPoint.ts the negative delta increases bias instead of returning zero, and no binary search over epochs is done.
**Steps:**
1. Alice creates a lock before snapshot time.
2. Advance time; Bob creates a lock after the snapshot timestamp.
3. Call `locker.balanceOfNFTAt(tokenId1, snapshot)` with snapshot < Bob's lastPoint.ts.
4. Observe Bob's returned voting power exceeds his actual bias and Alice's, despite staking after the snapshot.
**How found:** Code review of _balanceOfNFT: noticed it uses only the latest epoch checkpoint without binary-searching history, and that subtracting slope*(_t - lastPoint.ts) with _t < lastPoint.ts yields a negative product that increases bias.
**Exploitation:** Attacker stakes after a governance snapshot and calls balanceOfNFTAt with the past snapshot timestamp; the negative (t - lastPoint.ts) inflates bias, granting unearned voting power to influence the vote. Frontrunning the snapshot call is also noted.
**Escalation:** Text also notes lack of flash-loan protection when _t equals current block.timestamp, potentially allowing temporary balance inflation to double voting power.
**Insight:** In Curve-style vote-escrow lockers, historical balance queries must binary-search userPointHistory to the epoch active at _t; using the latest checkpoint with a signed time delta silently inflates past voting power when _t precedes the checkpoint.
