VotingEscrow¶
Curve-style vote-escrow (veToken). Lock underlying → linear-decay voting power until unlock.
Source: khomdev-veforge/src/VotingEscrow.vy (Vyper 0.4.x)
Consumers: GaugeController · GaugeWeightRouter
Implementation overview¶
- Voting power — bias/slope point history;
balanceOf(addr, t)with revert-on-past-time (A9/M7) - Locks — min 4 weeks, max
MAXTIME(≤ 4 years); unlock rounded to week boundaries - Permit —
create_lock_with_permitforwards to underlying token EIP-2612 (no VE-side EIP-712) - Ownership — snekmate
ownable_2step
flowchart LR
User -->|lock TOKEN| VE[VotingEscrow]
VE -->|balanceOf slope| GC[GaugeController]
VE -->|locked__end| GC
Immutables¶
| Name | Role |
|---|---|
TOKEN |
Underlying lock token |
MAXTIME |
Max lock duration (seconds) |
NAME / SYMBOL / VERSION |
veToken metadata |
Constants¶
| Name | Value | Role |
|---|---|---|
WEEK |
604800 | Lock time rounding |
MAX_LOCK_SECONDS |
4 years | Hard cap on MAXTIME |
MIN_LOCK_WEEKS |
4 | Minimum lock duration |
Events¶
| Event | When |
|---|---|
Deposit |
Lock created / extended / amount increased |
Withdraw |
Lock expired; tokens returned |
Checkpoint |
Global or user bias/slope updated |
Metadata¶
decimals¶
VotingEscrow.decimals() -> uint8
veToken decimal places (always 18).
Returns: uint8 — 18.
Access: view, any caller.
Source (khomdev-veforge/src/VotingEscrow.vy:40-43):
Lock lifecycle¶
create_lock¶
VotingEscrow.create_lock(_value, _unlock_time)
Deposit _value underlying tokens; lock until _unlock_time (rounded down to week).
| Param | Type | Description |
|---|---|---|
_value |
uint256 |
Token amount (> 0, ≤ int128 max) |
_unlock_time |
uint256 |
Unlock epoch; floored to WEEK boundary |
Access: Caller (msg.sender). nonreentrant. Requires no existing lock.
Events: Deposit (type=1).
Reverts if: zero value, existing lock, unlock not in future, exceeds MAXTIME, or < MIN_LOCK_WEEKS duration.
Source (khomdev-veforge/src/VotingEscrow.vy:205-213, _create_lock 172-200):
@external
@nonreentrant
def create_lock(_value: uint256, _unlock_time: uint256):
self._create_lock(msg.sender, _value, _unlock_time)
Example:
Cross-links: withdraw · increase_amount
create_lock_with_permit¶
VotingEscrow.create_lock_with_permit(_value, _unlock_time, _deadline, _v, _r, _s)
EIP-2612 permit on underlying token, then lock (same rules as create_lock).
| Param | Type | Description |
|---|---|---|
_value |
uint256 |
Token amount |
_unlock_time |
uint256 |
Unlock epoch |
_deadline |
uint256 |
Permit expiry |
_v, _r, _s |
uint8, bytes32, bytes32 |
Permit signature |
Access: Caller. nonreentrant. VE does not verify signature — underlying TOKEN.permit does (C1 fix: shared _create_lock, no nested reentrant call).
Events: Deposit (type=1).
Source (khomdev-veforge/src/VotingEscrow.vy:216-235):
@external
@nonreentrant
def create_lock_with_permit(...):
IERC20(TOKEN).permit(msg.sender, self, _value, _deadline, _v, _r, _s)
self._create_lock(msg.sender, _value, _unlock_time)
Cross-links: create_lock
increase_amount¶
VotingEscrow.increase_amount(_value)
Add _value tokens to caller's existing lock (end time unchanged).
| Param | Type | Description |
|---|---|---|
_value |
uint256 |
Additional tokens (> 0) |
Access: Caller. nonreentrant. Requires active non-expired lock.
Events: Deposit (type=2).
Source (khomdev-veforge/src/VotingEscrow.vy:238-265):
@external
@nonreentrant
def increase_amount(_value: uint256):
new_total_u = convert(old_locked.amount, uint256) + _value
self._checkpoint(msg.sender, old_locked, new_locked)
self._safe_transfer_from(TOKEN, msg.sender, self, _value)
Cross-links: create_lock · increase_unlock_time
increase_unlock_time¶
VotingEscrow.increase_unlock_time(_unlock_time)
Extend caller's lock end to _unlock_time (rounded down to week).
| Param | Type | Description |
|---|---|---|
_unlock_time |
uint256 |
New unlock epoch; must exceed current end |
Access: Caller. nonreentrant. Requires active non-expired lock.
Events: Deposit (type=3, value=0).
Reverts if: no lock, lock expired, new time ≤ current end, or exceeds block.timestamp + MAXTIME.
Source (khomdev-veforge/src/VotingEscrow.vy:268-289):
@external
@nonreentrant
def increase_unlock_time(_unlock_time: uint256):
unlock_time = (_unlock_time // WEEK) * WEEK
assert unlock_time > old_locked.end
self._checkpoint(msg.sender, old_locked, new_locked)
Cross-links: increase_amount · create_lock
withdraw¶
VotingEscrow.withdraw()
Withdraw all locked tokens after lock expiry.
Access: Caller. nonreentrant. Lock must be expired with amount > 0.
Events: Withdraw.
Source (khomdev-veforge/src/VotingEscrow.vy:292-310):
@external
@nonreentrant
def withdraw():
assert block.timestamp >= old_locked.end
self._checkpoint(msg.sender, old_locked, new_locked)
self._safe_transfer(TOKEN, msg.sender, value)
Cross-links: create_lock · locked__end
checkpoint¶
VotingEscrow.checkpoint()
Advance global bias/slope history to current block (permissionless poke).
Access: Any caller. nonreentrant.
Events: May emit Checkpoint via internal _checkpoint.
Source (khomdev-veforge/src/VotingEscrow.vy:313-319):
@external
@nonreentrant
def checkpoint():
self._checkpoint(empty(address), empty(LockedBalance), empty(LockedBalance))
Cross-links: balanceOf · totalSupply · GaugeController checkpoint
Metadata views¶
name¶
VotingEscrow.name() -> String[64]
veToken name (immutable, set at deploy).
Returns: String[64] — NAME.
Access: view, any caller.
Source (khomdev-veforge/src/VotingEscrow.vy:421-424):
symbol¶
VotingEscrow.symbol() -> String[32]
veToken symbol (immutable, set at deploy).
Returns: String[32] — SYMBOL.
Access: view, any caller.
Source (khomdev-veforge/src/VotingEscrow.vy:427-430):
version¶
VotingEscrow.version() -> String[32]
Contract version string (immutable, off-chain display only — VE does not implement EIP-712).
Returns: String[32] — VERSION.
Access: view, any caller.
Source (khomdev-veforge/src/VotingEscrow.vy:433-436):
Voting power views¶
balanceOf¶
VotingEscrow.balanceOf(addr, _t=block.timestamp) -> uint256
Voting power for addr at time _t (linear decay from last user checkpoint).
| Param | Type | Description |
|---|---|---|
addr |
address |
Lock holder |
_t |
uint256 |
Query timestamp (default: now) |
Returns: uint256 — decayed bias at _t; 0 if no lock or bias exhausted.
Access: view, any caller.
A9/M7: _t must be ≥ user's last checkpoint timestamp — reverts on past-time queries (unlike Curve). Use balanceOfAt for historical reads.
Source (khomdev-veforge/src/VotingEscrow.vy:439-460):
@external
@view
def balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256:
assert _t >= last_point.ts, "VE: query time before user's last checkpoint"
bias = last_point.bias - last_point.slope * (_t - last_point.ts)
return bias if bias >= 0 else 0
Cross-links: balanceOfAt · get_user_slope · GaugeController
balanceOfAt¶
VotingEscrow.balanceOfAt(addr, _block) -> uint256
Voting power for addr at historical block height _block.
| Param | Type | Description |
|---|---|---|
addr |
address |
Lock holder |
_block |
uint256 |
Block number (≤ current) |
Returns: uint256 — decayed bias at interpolated block time; 0 if before first global checkpoint or no user history.
Access: view, any caller.
Note: Binary-searches user + global point history; interpolates timestamp at _block. Preferred for historical reads vs past-time balanceOf (A9).
Source (khomdev-veforge/src/VotingEscrow.vy:463-513):
@external
@view
def balanceOfAt(addr: address, _block: uint256) -> uint256:
assert _block <= block.number
if _block < self.point_history[0].blk:
return 0
# binary search user_point_history + global epoch interpolation
return convert(bias, uint256)
Cross-links: balanceOf · totalSupplyAt
totalSupply¶
VotingEscrow.totalSupply(_t=block.timestamp) -> uint256
Total voting power across all locks at time _t.
| Param | Type | Description |
|---|---|---|
_t |
uint256 |
Query timestamp (default: now) |
Returns: uint256 — global decayed bias at _t via _supply_at.
Access: view, any caller.
Reverts if: _t before last global checkpoint timestamp.
Source (khomdev-veforge/src/VotingEscrow.vy:538-547):
@external
@view
def totalSupply(_t: uint256 = block.timestamp) -> uint256:
assert _t >= last_point.ts
return self._supply_at(last_point, _t)
Cross-links: totalSupplyAt · balanceOf
totalSupplyAt¶
VotingEscrow.totalSupplyAt(_block) -> uint256
Total voting power at historical block height _block.
| Param | Type | Description |
|---|---|---|
_block |
uint256 |
Block number (≤ current) |
Returns: uint256 — global bias at interpolated block time; 0 before first checkpoint.
Access: view, any caller.
Source (khomdev-veforge/src/VotingEscrow.vy:550-574):
@external
@view
def totalSupplyAt(_block: uint256) -> uint256:
assert _block <= block.number
if _block < self.point_history[0].blk:
return 0
return self._supply_at(point, point.ts + dt)
Cross-links: totalSupply · balanceOfAt
get_user_slope¶
VotingEscrow.get_user_slope(addr) -> int128
User's current vote-decay slope (locked.amount / MAXTIME).
| Param | Type | Description |
|---|---|---|
addr |
address |
Lock holder |
Returns: int128 — slope; 0 if no active lock or expired.
Access: view, any caller.
Note: GaugeController uses this (not balanceOf) for slope-weighted votes (M5).
Source (khomdev-veforge/src/VotingEscrow.vy:604-616):
@external
@view
def get_user_slope(addr: address) -> int128:
if locked.amount <= 0 or locked.end <= block.timestamp:
return 0
return locked.amount // convert(MAXTIME, int128)
Cross-links: GaugeController vote_for_gauge_weights · balanceOf
locked__end¶
VotingEscrow.locked__end(addr) -> uint256
User's lock expiry timestamp (Curve-compatible naming).
| Param | Type | Description |
|---|---|---|
addr |
address |
Lock holder |
Returns: uint256 — locked[addr].end; 0 if no lock.
Access: view, any caller.
Source (khomdev-veforge/src/VotingEscrow.vy:619-626):
Cross-links: create_lock · withdraw