Skip to content

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
  • Permitcreate_lock_with_permit forwards 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: uint818.

Access: view, any caller.

Source (khomdev-veforge/src/VotingEscrow.vy:40-43):

@external
@view
def decimals() -> uint8:
    return 18

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:

ve.create_lock(1000 * 10**18, block.timestamp + 52 * WEEK)

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):

@external
@view
def name() -> String[64]:
    return NAME

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):

@external
@view
def symbol() -> String[32]:
    return SYMBOL

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):

@external
@view
def version() -> String[32]:
    return VERSION

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: uint256locked[addr].end; 0 if no lock.

Access: view, any caller.

Source (khomdev-veforge/src/VotingEscrow.vy:619-626):

@external
@view
def locked__end(addr: address) -> uint256:
    return self.locked[addr].end

Cross-links: create_lock · withdraw