Understanding Voting Escrows
Overview
What exactly is a Voting Escrow contract? Basically, it allows users to lock tokens for a chosen period of time to gain voting power, which can be used for protocol governance or other incentives. The voting power is proportional to both the deposited amount and the lock's duration, incentivizing longer commitments. At the time of locking, the initial voting power w
is calculated as:
w = a × t / tmax
where a
is the amount of tokens locked, t
is the lock duration (from creation to expiration), and tmax
is the maximum lock duration (typically 2 or 4 years).

The voting power decreases linearly over time until the lock expires, resembling a graph that starts at w
and slopes downward. The voting power (balance) at any time can be calculated as:
(a / tmax) × (lockEndTime - block.timestamp)
Users can interact with their locks via:
create_lock()
: Create a new lock with a token amount and duration.increase_amount()
: Add more tokens to an existing lock.increase_unlock_time()
: Extend the lock's end time.withdraw()
: Remove tokens after the lock expires.
Each of these actions triggers the _checkpoint
function, which updates the contract's state to reflect changes in voting power. But how are locks represented in the contract? How does the contract keep track of the voting power of every lock at every single point in time?
Implementation Details
The voting escrow contract tracks each lock's voting power using a combination of user-specific and global data structures:
- LockedBalance: Stores the
amount
of locked tokens and end timeend
for a giventokenId
(representing an NFT for the lock). - Point: A snapshot of voting power at a specific time, defined as:
struct Point {
int128 bias; // Current voting power
int128 slope; // Rate of voting power decay (tokens per second)
uint256 ts; // Timestamp of the point
uint256 blk; // Block number of the point
}
- user_point_history: A nested mapping (
tokenId => Point[user_epoch]
) that stores a history of voting power snapshots for each lock, indexed by a user-specific epoch. - point_history: A mapping (
epoch => Point
) that stores global voting power snapshots, indexed by a global epoch. - slope_changes: A mapping (
time => slope_change
) that schedules changes to the global slope at specific timestamps, typically when locks expire.
User-Specific Voting Power
When a user creates or modifies a lock, the contract calculates:
- Slope: The rate at which voting power decreases, given by:
slope = amount / tmax
- Bias: The voting power at the checkpoint's timestamp, calculated as:
bias = slope × (lockEndTime - block.timestamp)
These values are stored in a new Point
in user_point_history[tokenId][user_epoch]
, where user_epoch
is incremented for each update to the user's lock. To calculate the voting power of a lock at any time t
:
- Find the latest
Point
inuser_point_history
wherepoint.ts <= t
(via binary search). - Compute the voting power:
w = point.bias - point.slope × (t - point.ts)
- If
balance < 0
ort >= lockEndTime
, setbalance = 0
.
Note: In some implementations (e.g., Velodrome v1), the
balanceOfNFTAt
function simply gets the latest entry inuser_point_history[tokenId]
and extrapolates the voting power from that point. This means the returned voting power is only accurate if the user did not extend or add to their lock in the past. This limitation was fixed in Velodrome v2, where a binary search is performed to find the latest entry before or at timestampt
.
Total Voting Power
Tracking the total voting power is more complex, as it must account for all users' locks, each with different amounts, durations, and end times. A naive approach would require iterating over all users' locks to sum their balances, which is impractical on-chain due to gas costs. Instead, the contract uses a global Point
in point_history
to represent the aggregate voting power, updated incrementally via the _checkpoint
function.
Here's how it works:
- When a user creates, modifies, or withdraws a lock,
_checkpoint
records the user's new slope and bias inuser_point_history
, as explained above. - It then updates the global
Point
inpoint_history
by adjusting the global slope and bias to reflect the user's changes (i.e., adding the difference between the new and old slopes and biases). - When a lock's end time is set,
_checkpoint
schedules a slope reduction inslope_changes
at that timestamp to account for the lock's expiration. - To optimize gas usage, lock end times are rounded to the end of a week (
lockEndTime = (lockEndTime / WEEK) * WEEK
, whereWEEK = 604800 seconds
). This limits the number of unique timestamps inslope_changes
, making updates proportional to the number of weeks rather than the number of users. - To calculate the total supply at time
t
, the contract retrieves the latestPoint
inpoint_history
wherepoint.ts <= t
, then iterates over weekly timestamps up tot
, applyingslope_changes
to update the slope and bias for each week.
This was a high-level overview of what the _checkpoint
function does. In the next chapter, we dive deeper into how exactly the function works.
Checkpoint Math
The _checkpoint
function is the heart of the Voting Escrow contract. It updates and records both global and per-user voting power whenever a lock is created, modified, or withdrawn.
Function arguments:
tokenId
: The NFT identifier for the user's lock (or 0 for global-only updates).old_locked
: The previousLockedBalance
(amount and end time) for the lock.new_locked
: The newLockedBalance
after the user's action.
Detailed breakdown:
- Calculate Slopes and Biases
For both the old and new locks (u_old
,u_new
), the function calculates:slope
: The rate of voting power decay (amount / MAXTIME
).bias
: The current voting power (slope × (lock_end - now)
).
- Retrieve Scheduled Slope Changes
The function checks theslope_changes
mapping for any existing scheduled slope adjustments at the old and new lock end times.- If
new_locked.end == old_locked.end
, reuse the same slope change. - Otherwise, get the new slope from
slope_changes[new_locked.end]
.
- If
- Update Global Voting Power History
- Get the latest global
Point
frompoint_history[epoch]
. - Compute a
block_slope
to extrapolate block numbers for historical points. - Iterate over weekly timestamps (
t_i = (last_checkpoint / WEEK) * WEEK + WEEK
) from the last checkpoint time toblock.timestamp
(capped at 255 iterations, around ~5 years):- Update global slope:
slope += slope_changes[t_i]
- Update global bias:
bias -= slope × (t_i - last_checkpoint)
- Update the block number using the calculated
block_slope
from above - Increment the epoch counter:
_epoch += 1
- If
t_i = block.timestamp
, setblk = block.number
for accuracy and exit the loop - If not, store the updated point in
point_history[epoch]
and repeat.
- Update global slope:
- Get the latest global
- Apply User Changes to Global Point
Nowpoint_history
is filled untilt = block.timestamp
, but the changes from the new user lock aren't reflected yet:- Add
u_new.slope - u_old.slope
tolast_point.slope
. - Add
u_new.bias - u_old.bias
tolast_point.bias
. - Store the updated global
Point
inpoint_history[epoch]
- Add
- Schedule Future Slope Changes
Theslope_changes
mapping is updated to reflect when a user's voting power will stop contributing:- If it's not a newly created lock (
old_locked.end > block.timestamp
):- Add the original slope
u_old.slope
back toslope_changes[old_locked.end]
(canceling its earlier subtraction). - If the user added to their existing lock (
new_locked.end == old_locked.end
), subtractu_new.slope
fromslope_changes[old_locked.end]
.
- Add the original slope
- If it's a newly created lock or an extension of an existing lock:
- Subtract the new slope
u_new.slope
fromslope_changes[new_locked.end]
- Subtract the new slope
- If it's not a newly created lock (
- Store User Lock Data
- Increment the user's epoch (
user_point_epoch[tokenId]
). - Save the new user point (
u_new
) at the current block and timestamp inuser_point_history
.
- Increment the user's epoch (
Conclusion
The Voting Escrow model is a mechanism for aligning long-term incentives in decentralized protocols. By locking tokens over time, users gain voting power that decays linearly until the lock expires, encouraging sustained commitment and active participation.
In this article, we explored the technical foundations of Voting Escrows and how they are implemented in practice, with a focus on how the contract efficiently tracks both individual and total voting power. At the core of this system is the _checkpoint
function, which updates and records global and per-user voting power whenever a lock is created or modified.
Understanding the vote-escrow architecture is important for developers working on governance systems, ve-tokenomics, or any protocol using time-based staking. It lays the groundwork for further developments like gauge voting, reward distribution, or on-chain governance, which build directly on top of the ve-model.