Pre-auditThree days of senior audit time for $1,500, credited in full toward your Zealynx audit.3 audit days, credited toward your auditSee how →
Back to Blog
When a Safe View Becomes an Unsafe Settlement Input
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:
  1. a context-sensitive balance view used in an economic calculation;
  2. a transfer-time guard that deliberately changes a voting balance; or
  3. 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 VotingEscrow implementation;[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 obligation
2 -> position ownership changes
3 -> voting view is suppressed for the current block
4 -> exit path reads the suppressed voting view
5 -> 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:
  1. ownership transition is security-relevant state, not administrative metadata;
  2. 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:
ContextGuarded valueUnguarded valueWrong choice
Voting after a same-block transfersuppressedunderlying time-decayed weightunguarded value may defeat the voting guard
Economic settlement after that transfersuppressedobligation-specific amountguarded 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:
2
3chargedPenalty
4 == 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.

The property should remain true across transitions that do not explicitly modify the obligation:
1transfer
2approval change
3delegation
4vote checkpoint
5same-block ownership guard
6metadata 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}
5
6function 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:
  1. Separate interfaces. Put voting projections and settlement accounting behind different functions or modules.
  2. Use semantic naming. Encode purpose in names such as settlementBase, currentVotes, and pastVotes.
  3. 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 settlementVoting value may change?Economic obligation should change?Required assertion
transfer in the same blockyesonly if specifiedfee basis remains obligation-correct
transfer in a prior blockyesonly if specifiedno timing-dependent fee bypass
approve an operatornonooperator path uses identical economics
delegate votesyesnodelegation cannot change principal settlement
extend lockyesyes, by rulenew duration and amount feed the documented formula
split or mergeyesyes, by ruleaggregate obligations are conserved
permanent-lock toggleyesyes, by ruleexit eligibility and basis stay coherent
checkpoint updatepossiblyno by itselfcache 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

  1. Which storage field defines principal, and which functions only project voting power?
  2. Can any read return zero because of transfer timing, delegation, checkpoint lag, or flash protection?
  3. Which fee, reward, redemption, and withdrawal paths consume those reads?
  4. Does a transfer move the obligation, clear it, or leave it unchanged?
  5. Are same-block and next-block outcomes economically equivalent when the specification expects them to be?
  6. Can integrations call an alternate view that omits a required voting guard?
  7. Do split and merge operations conserve principal, penalty basis, and voting checkpoints independently?
  8. 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

TermDefinition
InvariantA property that must remain true across valid protocol states and transitions.
Invariant TestingStateful testing that searches for transaction sequences capable of breaking a protocol property.
Vote-Escrowed TokenA locked governance position whose voting power commonly depends on amount and remaining lock time.
Flash LoanUncollateralized liquidity borrowed and repaid within one transaction, relevant to temporary voting-power threats.
Audit ScopeThe 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
[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

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.