# Voting does not take into account end of staking lock period

Magicsea Voter.vote() never checks remaining lock duration, letting a staker vote, withdraw, restake and vote again in the same epoch.

- Payout: Contest reward
- Severity: high
- Program: Magicsea
- Credited: 0xAnmol
- Disclosed: 2024-07-15
- Write-up quality: 6/10
- Original: https://github.com/sherlock-audit/2024-06-magicsea-judging/issues/166
- Library page: https://bugbountylibrary.com/r/sherlock%3A2024-06-magicsea%3AH-5
- Tags: domain:web3 impact:integrity kind:contest platform:sherlock sev:high tech:evm tech:solidity technique:source-review vuln:access-control vuln:business-logic vuln:governance

**Target:** Voter.sol vote() / MlumStaking staking positions
**Root cause:** vote() only checks initialLockDuration >= _minimumLockTime and lockDuration >= _periodDuration, never that the remaining lock period exceeds the epoch duration, so positions expiring mid-epoch remain eligible.
**Steps:**
1. Create a staking position with lockDuration == minimumLockTime (e.g. 2 weeks) via `_pool.createPosition(1 ether, 2 weeks)`.
2. Wait until the voting period starts and call `_voter.vote(1, pools, deltaAmounts)`; second vote on tokenId 1 reverts with `IVoter__AlreadyVoted`.
3. Skip past the lock expiry (`skip(1 weeks + 1)`), call `_pool.withdrawFromPosition(1, 1 ether)`.
4. Re-approve and `_pool.createPosition(1 ether, 2 weeks)` to mint tokenId 2, then `_voter.vote(2, pools, deltaAmounts)` in the same epoch.
5. Observe `_voter.getTotalVotes()` == 2 ether, i.e. same capital counted twice.
**How found:** Read the documented invariant (remaining lock period must exceed epoch time) and compared it to vote()'s actual checks, noticing only initialLockDuration and lockDuration are validated, not remaining time.
**Exploitation:** A staker times their lock to expire inside a biweekly voting epoch: they vote with the position, withdraw after expiry, restake the same tokens into a fresh position, and vote again in the same epoch, doubling their voting weight for LUM emission allocation.
**Escalation:** Repeated every ~90 days (minimum lock period) to periodically double vote weight and bias which pools receive LUM emissions.
**Insight:** When a protocol documents a time-based eligibility invariant, diff the doc against the exact fields the code reads; checking the original lock duration instead of remaining duration is a recurring gap that also makes renew/extend functions dead code.
