Back to Blog 

DeFiWeb3 SecuritySolidityTesting
When a Safe View Becomes an Unsafe Settlement Input
14 min
A smart contract view can return the correct value for governance and the wrong value for settlement at the same time.
This is not a contradiction. It is a semantic mismatch.
Vote-escrow systems often expose several ways to read a position: current voting power, historical voting power, locked principal, time-decayed weight, or a value suppressed after a recent transfer. Each answer can be correct within its intended security boundary. Trouble starts when a fee, withdrawal, redemption, or reward path treats one of those answers as a universal balance.
This article compares a public Neverland finding with the transfer guard documented in Velodrome's voting escrow, an opposing Velocimeter finding about unguarded balance views, and OpenZeppelin's checkpoint model. The comparison supports one narrow design rule:
A settlement calculation should consume the state that defines the economic obligation, not a context-sensitive projection created for another security goal.
The Neverland case study provides the engagement narrative. This study removes the client name from the mechanism and asks where the same class of semantic coupling can appear.
TL;DR
- A balance function is not merely a number. It carries assumptions about time, ownership, checkpoints, and purpose.
- Same-block transfer suppression can be appropriate for voting power while being unsafe as the basis of an early-exit penalty.
- Removing suppression globally is not the answer. A public Velocimeter finding describes the opposite risk when alternate voting views omit the transfer protection.
- The structural fix is role-specific reads: settlement state for settlement, voting state for voting, and historical checkpoints for historical queries.
- Tests should cross economic actions with every transition that can alter, suppress, or stale their inputs.
Methodology and evidence boundaries
The comparison is mechanism-led. A source enters the core set when it documents one of three things:
- a context-sensitive balance view used in an economic calculation;
- a transfer-time guard that deliberately changes a voting balance; or
- a checkpoint interface that distinguishes current state from historical state.
The evidence set contains:
- the Composable Security report for Neverland and its recorded retest;[1]
- the current public Velodrome
VotingEscrowimplementation;[2] - Sherlock issue 286 from the Velocimeter review;[3]
- OpenZeppelin's ERC-721 and governance documentation;[4][5]
- the Zealynx case study for attribution and evidence limits.[6]
Four statement types are kept separate:
- Source fact reports what a cited source states or implements.
- Zealynx classification groups behavior by semantic role. It is not a new severity rating.
- Inference is a design conclusion drawn across the sources.
- Limitation identifies what the public evidence does not establish.
This is a purposive comparison, not a prevalence study. It does not establish how common this bug family is. It also does not claim that the compared systems share code, deployment state, impact, or remediation history.
The hidden type system inside a balance
Solidity gives each of these functions the same return type:
1function lockedPrincipal(uint256 tokenId) external view returns (uint256);2function currentVotingPower(uint256 tokenId) external view returns (uint256);3function pastVotingPower(uint256 tokenId, uint256 timepoint) external view returns (uint256);
At the ABI boundary, all three values are
uint256. At the protocol boundary, they are different types.A useful mental model is:
1SettlementAmount(position, time)2VotingPower(position, time, ownershipContext)3HistoricalVotes(account, checkpoint)
The compiler cannot stop a developer from passing
VotingPower into a formula that expects SettlementAmount. The names and documentation might help, but the real protection comes from architecture and tests.Zealynx classifies this failure family as semantic type confusion: a value is numerically valid but belongs to the wrong security domain.
Anchor: transfer suppression crossed into an exit fee
The Neverland report describes a vote-escrow NFT whose
balanceOfNFT() view returned zero after an ownership change in the current block. The report explains that the early-withdrawal penalty used this view as its calculation basis. A transfer followed by earlyWithdraw() in the same block could therefore reduce the calculated penalty to zero.[1]The relevant sequence can be expressed without protocol-specific names:
1position has an active economic obligation2 -> position ownership changes3 -> voting view is suppressed for the current block4 -> exit path reads the suppressed voting view5 -> obligation is calculated from zero
Each local rule has a plausible purpose:
- the NFT remains transferable;
- recently transferred voting power is not immediately reusable;
- early withdrawal charges a percentage of a balance.
The composition fails because "balance" changes meaning between the final two steps.
The report records a change from the flash-protected view to
_balanceOfNFTAt(tokenId, block.timestamp) and marks the finding fixed after retest.[1] That is a source-reported remediation in the reviewed scope. It does not independently prove deployment status or the absence of other issues.Comparison 1: Velodrome makes the transfer guard explicit
Velodrome's public
VotingEscrow source records ownershipChange[tokenId] = block.number during transfer and labels the assignment as flash NFT protection.[2]Its public code therefore makes two facts visible:
- ownership transition is security-relevant state, not administrative metadata;
- a balance read can legitimately depend on the block in which ownership changed.
The source also exposes role-specific behavior. For example,
depositManaged checks _balanceOfNFTAt(tokenId, block.timestamp), while delegation rejects an ownership change in the current block.[2] The details are system-specific, but the architecture demonstrates that one position may need several read paths with different semantics.Zealynx classifies this as explicit context separation. The existence of several read paths is not itself a weakness. The review question is whether each caller selects the path matching its obligation.
Comparison 2: the opposite mistake also exists
Sherlock issue 286 in the Velocimeter review describes the inverse hazard. According to the issue,
balanceOfNFT() returned zero after a same-block ownership change, but alternate functions did not apply the same check. The submitter argued that consumers using those alternate functions for voting power could miss the flash-loan protection.[3]The two public findings point in opposite directions:
| Context | Guarded value | Unguarded value | Wrong choice |
|---|---|---|---|
| Voting after a same-block transfer | suppressed | underlying time-decayed weight | unguarded value may defeat the voting guard |
| Economic settlement after that transfer | suppressed | obligation-specific amount | guarded value may erase the obligation |
This matters because the safe recommendation is not "always use the raw balance" or "always use the protected balance."
The safe recommendation is:
Define which semantic value each operation requires, then make that choice explicit and testable.
The Velocimeter issue is a contest finding with its own source label and judging context.[3] This article does not reassess its severity or assert exploitation. It uses the record to show that the same family of functions can fail through both over-application and under-application of a transfer guard.
Comparison 3: checkpoints answer a different question
OpenZeppelin's governance documentation distinguishes current voting units from past votes. Its
Votes interfaces expose current and historical queries, while VotesExtended states that past balance and delegate queries operate at a specific timepoint and require that timepoint to be in the past.[5]This separation is not cosmetic. A historical governance query asks:
1What voting state was recorded at the proposal's timepoint?
A settlement query asks something else:
1What economic obligation is attached to this position now?
A checkpoint can be perfectly correct for the first question and stale or irrelevant for the second. Similarly, a current voting projection can be correct for governance while unsuitable for principal accounting.
OpenZeppelin's ERC-721 documentation adds another boundary: ownership and approvals change through transfer, and approvals are cleared when the token moves.[4] Any protocol that tokenizes a financial position should therefore treat transfer as a state transition that can affect callers, permissions, delegation, checkpoints, and downstream economic actions.
A taxonomy of context-sensitive read failures
The sources support four useful classes.
1. Protective suppression leaks into settlement
A governance or anti-flash guard returns zero, but an exit, fee, reward, or redemption path interprets zero as absence of economic value.
The Neverland finding is the anchor.[1]
2. An unguarded read leaks into voting
A raw or historical-style balance path bypasses a protection expected by the voting consumer.
The Velocimeter issue is the comparison.[3]
3. Historical state is treated as current obligation
A checkpoint is selected because it is easy to query, even though the operation needs current principal, current ownership, or a post-transition value.
OpenZeppelin's explicit current and past interfaces show why the distinction should remain visible.[5]
4. Current projection is treated as stored principal
Time-decayed voting weight is used where the protocol needs the amount originally locked, the amount still escrowed, or another accounting quantity.
Velodrome's source describes voting weight as decaying linearly over time.[2] That makes a vote projection unsuitable as a generic synonym for principal unless the economic specification deliberately says otherwise.
The invariant: obligations survive unrelated projections
For an early-exit mechanism, a general property is:
1For every position eligible for early exit:23chargedPenalty4 == penaltyRule(economicPositionState)
Working auditors in your corner, all year
Zealynx Insiders: weekly live sessions, 1:1 advisory, pair-auditing, and Krait runs on your code, from an audit firm that publishes its reports.
No spam. Unsubscribe anytime.
Building a protocol? Start with a three-day pre-audit, credited toward your audit →The property should remain true across transitions that do not explicitly modify the obligation:
1transfer2approval change3delegation4vote checkpoint5same-block ownership guard6metadata update
This does not mean transfers can never change economic rights. A protocol may intentionally transfer the obligation with the NFT or prohibit transfer while a condition holds. The invariant is narrower:
A transition must not alter settlement merely because it alters a projection used by another subsystem.
For testing, metamorphic relations make this concrete:
1penalty(exit(position))2 == penalty(transferThenExit(position))
when the specification says ownership transfer preserves the obligation.
A second relation tests temporal boundaries:
1penalty(exit in transfer block)2 == penalty(exit one block later)
unless block timing is itself an explicit term of the economic rule.
Design pattern: name values by role, not shape
Generic names such as
balance, weight, and amount hide assumptions. Prefer interfaces that expose purpose:1function settlementBase(uint256 tokenId) internal view returns (uint256) {2 LockedBalance memory lock = locked[tokenId];3 return uint256(uint128(lock.amount));4}56function votingPower(uint256 tokenId) public view returns (uint256) {7 if (ownershipChange[tokenId] == block.number) return 0;8 return decayedWeightAt(tokenId, block.timestamp);9}
The example is illustrative, not a drop-in fix. A real protocol must define how permanent locks, expired locks, splits, merges, managed positions, and fee rounding affect settlement.
Three structural controls are more durable than a caller-by-caller convention:
- Separate interfaces. Put voting projections and settlement accounting behind different functions or modules.
- Use semantic naming. Encode purpose in names such as
settlementBase,currentVotes, andpastVotes. - Restrict sensitive reads. Keep raw accounting helpers internal where possible so integrations cannot mistake them for supported voting interfaces.
A transition matrix for review
A unit test that creates a lock and exits it is not enough. The useful test surface crosses actions with state transitions.
| Transition before settlement | Voting value may change? | Economic obligation should change? | Required assertion |
|---|---|---|---|
| transfer in the same block | yes | only if specified | fee basis remains obligation-correct |
| transfer in a prior block | yes | only if specified | no timing-dependent fee bypass |
| approve an operator | no | no | operator path uses identical economics |
| delegate votes | yes | no | delegation cannot change principal settlement |
| extend lock | yes | yes, by rule | new duration and amount feed the documented formula |
| split or merge | yes | yes, by rule | aggregate obligations are conserved |
| permanent-lock toggle | yes | yes, by rule | exit eligibility and basis stay coherent |
| checkpoint update | possibly | no by itself | cache or history writes do not rewrite principal |
The strongest tests are stateful. Randomized sequences should combine transfer, approval, delegation, lock extension, split, merge, and exit, then assert economic conservation after every permitted settlement.
Review questions for protocol teams
- Which storage field defines principal, and which functions only project voting power?
- Can any read return zero because of transfer timing, delegation, checkpoint lag, or flash protection?
- Which fee, reward, redemption, and withdrawal paths consume those reads?
- Does a transfer move the obligation, clear it, or leave it unchanged?
- Are same-block and next-block outcomes economically equivalent when the specification expects them to be?
- Can integrations call an alternate view that omits a required voting guard?
- Do split and merge operations conserve principal, penalty basis, and voting checkpoints independently?
- Does the fix preserve the security property that motivated the original guard?
The final question prevents a common repair error: fixing settlement by globally removing a voting protection.
What this research does not establish
The comparison does not establish that every vote-escrow implementation needs same-block transfer suppression. That depends on transferability, voting snapshots, delegation, and integration design.
It does not establish that Velodrome or OpenZeppelin contains the Neverland issue. Their public code and documentation are used to compare semantic boundaries, not to allege a vulnerability.
It does not independently reproduce the Neverland or Velocimeter proofs of concept. The article reports the mechanisms described by their public records.[1][3]
The Neverland report records the issue as fixed after retest, but public evidence does not establish production deployment, monetary exposure, exploitation, or funds saved.[1][6]
Finally, the corpus is too small and intentionally selected to support frequency claims. The defensible conclusion is architectural, not statistical.
The durable rule
Context-sensitive views are useful. They let governance systems suppress recently transferred voting power, retrieve historical checkpoints, and express time-decayed influence.
They become dangerous when downstream code forgets the context.
The Neverland finding shows a voting guard erasing an exit penalty.[1] The Velocimeter issue describes the opposite boundary, where alternate views could omit a guard expected by voting consumers.[3] Velodrome's source and OpenZeppelin's checkpoint APIs show why multiple views exist in the first place.[2][5]
There is no universal balance.
There is locked principal. There is current voting power. There is historical voting power. There is settlement value. Treating them as separate semantic types gives reviewers something precise to test:
Every economic action must read the state that defines its obligation, across every transition that can change the meaning of a shared view.
FAQ
1. Why can a correct balance function be unsafe?
Because correctness depends on purpose. A function that correctly suppresses same-block voting power can still be the wrong source for a withdrawal fee based on locked principal.
2. Should protocols remove same-block transfer guards?
Not generally. The public Velocimeter finding illustrates why voting consumers may require such a guard.[3] Settlement should use an obligation-specific value without weakening governance protection.
3. What is the best invariant for an early-exit penalty?
If transfer does not change the economic obligation, the configured penalty should remain the same before and after transfer, including at same-block and next-block boundaries. See invariant testing for the broader testing model.
4. Are historical checkpoints safe for settlement?
Only if the economic specification explicitly defines settlement from that checkpoint. Historical governance state and current settlement state answer different questions.
5. Why are veNFT transfers especially important to test?
A vote-escrowed token can combine ERC-721 ownership, time-decayed voting power, delegation, and locked principal. Transfer crosses several of those domains at once.
Glossary
| Term | Definition |
|---|---|
| Invariant | A property that must remain true across valid protocol states and transitions. |
| Invariant Testing | Stateful testing that searches for transaction sequences capable of breaking a protocol property. |
| Vote-Escrowed Token | A locked governance position whose voting power commonly depends on amount and remaining lock time. |
| Flash Loan | Uncollateralized liquidity borrowed and repaid within one transaction, relevant to temporary voting-power threats. |
| Audit Scope | The contracts, versions, integrations, and assumptions included in a security review. |
Sources
[1] https://raw.githubusercontent.com/ComposableSecurity/.github/main/reports/2025_08_Neverland.pdf - Composable Security, Neverland Money Smart Contract Security Audit
[2] https://github.com/velodrome-finance/contracts/blob/main/contracts/VotingEscrow.sol - Velodrome Finance, VotingEscrow source
[3] https://github.com/sherlock-audit/2024-06-velocimeter-judging/issues/286 - Sherlock, Velocimeter issue 286
[4] https://docs.openzeppelin.com/contracts/5.x/api/token/erc721 - OpenZeppelin Contracts, ERC-721 API
[5] https://docs.openzeppelin.com/contracts/5.x/api/governance - OpenZeppelin Contracts, governance and Votes APIs
[6] https://zealynx.io/case-studies/neverland-early-withdrawal-penalty - Zealynx, Neverland case study
Working auditors in your corner, all year
Zealynx Insiders: weekly live sessions, 1:1 advisory, pair-auditing, and Krait runs on your code, from an audit firm that publishes its reports.
No spam. Unsubscribe anytime.
Building a protocol? Start with a three-day pre-audit, credited toward your audit →