Skip to main content

Voting Escrow (veCRV)

Participating in Curve DAO governance requires that an account have a balance of vote-escrowed CRV (veCRV). veCRV is a non-standard ERC-20 implementation, used within the Aragon DAO to determine each account's voting power.

VotingEscrow.vy

The source code for the VotingEscrow.vy contract can be found on GitHub. The contract is written using Vyper version 0.2.4.

The contract is deployed on Ethereum at 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2.

{ }Contract ABI▼
[{"name":"CommitOwnership","inputs":[{"type":"address","name":"admin","indexed":false}],"anonymous":false,"type":"event"},{"name":"ApplyOwnership","inputs":[{"type":"address","name":"admin","indexed":false}],"anonymous":false,"type":"event"},{"name":"Deposit","inputs":[{"type":"address","name":"provider","indexed":true},{"type":"uint256","name":"value","indexed":false},{"type":"uint256","name":"locktime","indexed":true},{"type":"int128","name":"type","indexed":false},{"type":"uint256","name":"ts","indexed":false}],"anonymous":false,"type":"event"},{"name":"Withdraw","inputs":[{"type":"address","name":"provider","indexed":true},{"type":"uint256","name":"value","indexed":false},{"type":"uint256","name":"ts","indexed":false}],"anonymous":false,"type":"event"},{"name":"Supply","inputs":[{"type":"uint256","name":"prevSupply","indexed":false},{"type":"uint256","name":"supply","indexed":false}],"anonymous":false,"type":"event"},{"outputs":[],"inputs":[{"type":"address","name":"token_addr"},{"type":"string","name":"_name"},{"type":"string","name":"_symbol"},{"type":"string","name":"_version"}],"stateMutability":"nonpayable","type":"constructor"},{"name":"commit_transfer_ownership","outputs":[],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"nonpayable","type":"function"},{"name":"apply_transfer_ownership","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function"},{"name":"commit_smart_wallet_checker","outputs":[],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"nonpayable","type":"function"},{"name":"apply_smart_wallet_checker","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function"},{"name":"create_lock","outputs":[],"inputs":[{"type":"uint256","name":"_value"},{"type":"uint256","name":"_unlock_time"}],"stateMutability":"nonpayable","type":"function"},{"name":"increase_amount","outputs":[],"inputs":[{"type":"uint256","name":"_value"}],"stateMutability":"nonpayable","type":"function"},{"name":"increase_unlock_time","outputs":[],"inputs":[{"type":"uint256","name":"_unlock_time"}],"stateMutability":"nonpayable","type":"function"},{"name":"deposit_for","outputs":[],"inputs":[{"type":"address","name":"_addr"},{"type":"uint256","name":"_value"}],"stateMutability":"nonpayable","type":"function"},{"name":"withdraw","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function"},{"name":"checkpoint","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function"},{"name":"changeController","outputs":[],"inputs":[{"type":"address","name":"_newController"}],"stateMutability":"nonpayable","type":"function"},{"name":"get_last_user_slope","outputs":[{"type":"int128","name":""}],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"view","type":"function"},{"name":"user_point_history__ts","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"_addr"},{"type":"uint256","name":"_idx"}],"stateMutability":"view","type":"function"},{"name":"locked__end","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"_addr"}],"stateMutability":"view","type":"function"},{"name":"balanceOf","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"view","type":"function"},{"name":"balanceOf","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"},{"type":"uint256","name":"_t"}],"stateMutability":"view","type":"function"},{"name":"balanceOfAt","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"},{"type":"uint256","name":"_block"}],"stateMutability":"view","type":"function"},{"name":"totalSupply","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"totalSupply","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"uint256","name":"t"}],"stateMutability":"view","type":"function"},{"name":"totalSupplyAt","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"uint256","name":"_block"}],"stateMutability":"view","type":"function"},{"name":"token","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"supply","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"locked","outputs":[{"type":"int128","name":"amount"},{"type":"uint256","name":"end"}],"inputs":[{"type":"address","name":"arg0"}],"stateMutability":"view","type":"function"},{"name":"epoch","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"point_history","outputs":[{"type":"int128","name":"bias"},{"type":"int128","name":"slope"},{"type":"uint256","name":"ts"},{"type":"uint256","name":"blk"}],"inputs":[{"type":"uint256","name":"arg0"}],"stateMutability":"view","type":"function"},{"name":"user_point_history","outputs":[{"type":"int128","name":"bias"},{"type":"int128","name":"slope"},{"type":"uint256","name":"ts"},{"type":"uint256","name":"blk"}],"inputs":[{"type":"address","name":"arg0"},{"type":"uint256","name":"arg1"}],"stateMutability":"view","type":"function"},{"name":"user_point_epoch","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"arg0"}],"stateMutability":"view","type":"function"},{"name":"slope_changes","outputs":[{"type":"int128","name":""}],"inputs":[{"type":"uint256","name":"arg0"}],"stateMutability":"view","type":"function"},{"name":"controller","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"transfersEnabled","outputs":[{"type":"bool","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"name","outputs":[{"type":"string","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"symbol","outputs":[{"type":"string","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"version","outputs":[{"type":"string","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"decimals","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"future_smart_wallet_checker","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"smart_wallet_checker","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"admin","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"future_admin","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"}]

locktime is denominated in years. The maximum lock duration is four years and the minimum is one week.

CRVveCRVLocktime
114 years
10.753 years
10.52 years
10.251 year
xx * n/4n
warning

When a user locks their CRV tokens for voting, they will receive veCRV based on the lock duration and the amount locked. Locking is not reversible and veCRV tokens are non-transferable. If a user decides to vote-lock their CRV tokens, they will only be able to reclaim the CRV tokens after the lock duration has ended.

Additionally, a user cannot have multiple locks with different expiry dates. However, a lock can be extended, or additional CRV can be added to it at any time.


Implementation Details​

User voting power wiw_{i} is linearly decreasing since the moment of lock. So does the total voting power WW. In order to avoid periodic check-ins, every time the user deposits, or withdraws, or changes the locktime, we record user's slope and bias for the linear function wi(t)w_{i}(t) in the public mapping user_point_history. We also change slope and bias for the total voting power W(t)W(t) and record it in point_history. In addition, when a user's lock is scheduled to end, we schedule change of slopes of W(t)W(t) in the future in slope_changes. Every change involves increasing the epoch by 1.

This way we don't have to iterate over all users to figure out how much W(t)W(t) should change by, neither do we require users to check in periodically. However, we limit the end of user locks to times rounded off by whole weeks.

Slopes and biases change both when a user deposits and locks governance tokens, and when the locktime expires. All the possible expiration times are rounded to whole weeks to make the number of reads from blockchain proportional to the number of missed weeks at most, not the number of users (which is potentially large).


Lock Management​

create_lock​

VotingEscrow.create_lock(_value: uint256, _unlock_time: uint256)

Function to deposit _value CRV into the VotingEscrow and create a new lock until _unlock_time. The unlock time is rounded down to whole weeks.

InputTypeDescription
_valueuint256Amount of CRV to deposit
_unlock_timeuint256Timestamp of the unlock time

Emits: Deposit and Supply events.

<>Source code▼
struct LockedBalance:
amount: int128
end: uint256

locked: public(HashMap[address, LockedBalance])

WEEK: constant(uint256) = 7 * 86400 # all future times are rounded by week
MAXTIME: constant(uint256) = 4 * 365 * 86400 # 4 years

@external
@nonreentrant('lock')
def create_lock(_value: uint256, _unlock_time: uint256):
"""
@notice Deposit `_value` tokens for `msg.sender` and lock until `_unlock_time`
@param _value Amount to deposit
@param _unlock_time Epoch time when tokens unlock, rounded down to whole weeks
"""
self.assert_not_contract(msg.sender)
unlock_time: uint256 = (_unlock_time / WEEK) * WEEK # Locktime is rounded down to weeks
_locked: LockedBalance = self.locked[msg.sender]

assert _value > 0 # dev: need non-zero value
assert _locked.amount == 0, "Withdraw old tokens first"
assert unlock_time > block.timestamp, "Can only lock until time in the future"
assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max"

self._deposit_for(msg.sender, _value, unlock_time, _locked, CREATE_LOCK_TYPE)
▶Example▼

This example creates a new lock of 100 CRV tokens until a specified unlock timestamp.

>>> VotingEscrow.create_lock(100000000000000000000, 1694003759)

increase_amount​

VotingEscrow.increase_amount(_value: uint256)

Function to deposit _value additional CRV tokens to an existing lock without modifying the unlock time.

InputTypeDescription
_valueuint256Amount of CRV to additionally lock

Emits: Deposit and Supply events.

<>Source code▼
struct LockedBalance:
amount: int128
end: uint256

locked: public(HashMap[address, LockedBalance])

@external
@nonreentrant('lock')
def increase_amount(_value: uint256):
"""
@notice Deposit `_value` additional tokens for `msg.sender`
without modifying the unlock time
@param _value Amount of tokens to deposit and add to the lock
"""
self.assert_not_contract(msg.sender)
_locked: LockedBalance = self.locked[msg.sender]

assert _value > 0 # dev: need non-zero value
assert _locked.amount > 0, "No existing lock found"
assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw"

self._deposit_for(msg.sender, _value, 0, _locked, INCREASE_LOCK_AMOUNT)
▶Example▼

This example adds 100 CRV tokens to an existing lock.

>>> VotingEscrow.increase_amount(100000000000000000000)

increase_unlock_time​

VotingEscrow.increase_unlock_time(_unlock_time: uint256)

Function to extend the unlock time on an already existing lock until _unlock_time. The unlock time is rounded down to whole weeks.

InputTypeDescription
_unlock_timeuint256New unlock timestamp

Emits: Deposit and Supply events.

<>Source code▼
struct LockedBalance:
amount: int128
end: uint256

locked: public(HashMap[address, LockedBalance])

WEEK: constant(uint256) = 7 * 86400 # all future times are rounded by week
MAXTIME: constant(uint256) = 4 * 365 * 86400 # 4 years

@external
@nonreentrant('lock')
def increase_unlock_time(_unlock_time: uint256):
"""
@notice Extend the unlock time for `msg.sender` to `_unlock_time`
@param _unlock_time New epoch time for unlocking
"""
self.assert_not_contract(msg.sender)
_locked: LockedBalance = self.locked[msg.sender]
unlock_time: uint256 = (_unlock_time / WEEK) * WEEK # Locktime is rounded down to weeks

assert _locked.end > block.timestamp, "Lock expired"
assert _locked.amount > 0, "Nothing is locked"
assert unlock_time > _locked.end, "Can only increase lock duration"
assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max"

self._deposit_for(msg.sender, 0, unlock_time, _locked, INCREASE_UNLOCK_TIME)
▶Example▼

This example extends the unlock time of an existing lock to a new timestamp.

>>> VotingEscrow.increase_unlock_time(1694003759)

deposit_for​

VotingEscrow.deposit_for(_addr: address, _value: uint256)

Function to deposit _value tokens for _addr and add them to an existing lock. Anyone (even a smart contract) can deposit for someone else, but cannot extend their locktime or deposit for a brand new user.

InputTypeDescription
_addraddressAddress to deposit for
_valueuint256Amount of tokens to lock

Emits: Deposit and Supply events.

<>Source code▼
struct LockedBalance:
amount: int128
end: uint256

locked: public(HashMap[address, LockedBalance])

@external
@nonreentrant('lock')
def deposit_for(_addr: address, _value: uint256):
"""
@notice Deposit `_value` tokens for `_addr` and add to the lock
@dev Anyone (even a smart contract) can deposit for someone else, but
cannot extend their locktime and deposit for a brand new user
@param _addr User's wallet address
@param _value Amount to add to user's lock
"""
_locked: LockedBalance = self.locked[_addr]

assert _value > 0 # dev: need non-zero value
assert _locked.amount > 0, "No existing lock found"
assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw"

self._deposit_for(_addr, _value, 0, self.locked[_addr], DEPOSIT_FOR_TYPE)
▶Example▼

This example deposits 100 CRV tokens into an existing lock owned by another address.

>>> VotingEscrow.deposit_for("0x7a16fF8270133F063aAb6C9977183D9e72835428", 100000000000000000000)

withdraw​

VotingEscrow.withdraw()

Function to withdraw all deposited CRV tokens once a lock has expired.

Emits: Withdraw and Supply events.

<>Source code▼
struct LockedBalance:
amount: int128
end: uint256

locked: public(HashMap[address, LockedBalance])
supply: public(uint256)
token: public(address)

@external
@nonreentrant('lock')
def withdraw():
"""
@notice Withdraw all tokens for `msg.sender`
@dev Only possible if the lock has expired
"""
_locked: LockedBalance = self.locked[msg.sender]
assert block.timestamp >= _locked.end, "The lock didn't expire"
value: uint256 = convert(_locked.amount, uint256)

old_locked: LockedBalance = _locked
_locked.end = 0
_locked.amount = 0
self.locked[msg.sender] = _locked
supply_before: uint256 = self.supply
self.supply = supply_before - value

# old_locked can have either expired <= timestamp or zero end
# _locked has only 0 end
# Both can have >= 0 amount
self._checkpoint(msg.sender, old_locked, _locked)

assert ERC20(self.token).transfer(msg.sender, value)

log Withdraw(msg.sender, value, block.timestamp)
log Supply(supply_before, supply_before - value)
▶Example▼

This example withdraws all CRV tokens after the lock has expired.

>>> VotingEscrow.withdraw()

checkpoint​

VotingEscrow.checkpoint()

Function to record global data to a checkpoint. This updates the global point_history and epoch. Can be called by anyone.

<>Source code▼
ZERO_ADDRESS: constant(address) = 0x0000000000000000000000000000000000000000

struct LockedBalance:
amount: int128
end: uint256

@external
def checkpoint():
"""
@notice Record global data to checkpoint
"""
self._checkpoint(ZERO_ADDRESS, empty(LockedBalance), empty(LockedBalance))
▶Example▼

This example records a global data checkpoint, updating the point_history and epoch.

>>> VotingEscrow.checkpoint()

Voting Power & Balances​

balanceOf​

VotingEscrow.balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256: view

Getter for the current veCRV balance (= voting power) of addr at timestamp _t. Defaults to block.timestamp.

note

These are not real ERC-20 balances. They measure voting weights that decay linearly over time.

InputTypeDescription
addraddressUser wallet address
_tuint256Timestamp; defaults to block.timestamp

Returns: voting power (uint256).

<>Source code▼
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block

user_point_epoch: public(HashMap[address, uint256])
user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch]

@external
@view
def balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256:
"""
@notice Get the current voting power for `msg.sender`
@dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility
@param addr User wallet address
@param _t Epoch time to return voting power at
@return User voting power
"""
_epoch: uint256 = self.user_point_epoch[addr]
if _epoch == 0:
return 0
else:
last_point: Point = self.user_point_history[addr][_epoch]
last_point.bias -= last_point.slope * convert(_t - last_point.ts, int128)
if last_point.bias < 0:
last_point.bias = 0
return convert(last_point.bias, uint256)
▶Example▼

This example returns the current veCRV balance (voting power) of an address. Enter an address and click Query to fetch the value live from the blockchain.

balanceOfAt​

VotingEscrow.balanceOfAt(addr: address, _block: uint256) -> uint256: view

Getter for the veCRV balance (= voting power) of addr at block height _block.

InputTypeDescription
addraddressUser wallet address
_blockuint256Block height

Returns: voting power (uint256) at a specific block.

<>Source code▼
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block

epoch: public(uint256)
point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point
user_point_epoch: public(HashMap[address, uint256])
user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch]

@external
@view
def balanceOfAt(addr: address, _block: uint256) -> uint256:
"""
@notice Measure voting power of `addr` at block height `_block`
@dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime
@param addr User's wallet address
@param _block Block to calculate the voting power at
@return Voting power
"""
assert _block <= block.number

# Binary search
_min: uint256 = 0
_max: uint256 = self.user_point_epoch[addr]
for i in range(128): # Will be always enough for 128-bit numbers
if _min >= _max:
break
_mid: uint256 = (_min + _max + 1) / 2
if self.user_point_history[addr][_mid].blk <= _block:
_min = _mid
else:
_max = _mid - 1

upoint: Point = self.user_point_history[addr][_min]

max_epoch: uint256 = self.epoch
_epoch: uint256 = self.find_block_epoch(_block, max_epoch)
point_0: Point = self.point_history[_epoch]
d_block: uint256 = 0
d_t: uint256 = 0
if _epoch < max_epoch:
point_1: Point = self.point_history[_epoch + 1]
d_block = point_1.blk - point_0.blk
d_t = point_1.ts - point_0.ts
else:
d_block = block.number - point_0.blk
d_t = block.timestamp - point_0.ts
block_time: uint256 = point_0.ts
if d_block != 0:
block_time += d_t * (_block - point_0.blk) / d_block

upoint.bias -= upoint.slope * convert(block_time - upoint.ts, int128)
if upoint.bias >= 0:
return convert(upoint.bias, uint256)
else:
return 0
▶Example▼

This example returns the veCRV balance (voting power) of an address at a specific block height.

totalSupply​

VotingEscrow.totalSupply(t: uint256 = block.timestamp) -> uint256: view

Getter for the current total supply of veCRV (= total voting power) at timestamp t. Defaults to block.timestamp.

InputTypeDescription
tuint256Timestamp; defaults to block.timestamp

Returns: total voting power (uint256).

<>Source code▼
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block

epoch: public(uint256)
point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point

@external
@view
def totalSupply(t: uint256 = block.timestamp) -> uint256:
"""
@notice Calculate total voting power
@dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility
@return Total voting power
"""
_epoch: uint256 = self.epoch
last_point: Point = self.point_history[_epoch]
return self.supply_at(last_point, t)
▶Example▼

This example returns the current total veCRV voting power. The value is fetched live from the blockchain.

totalSupplyAt​

VotingEscrow.totalSupplyAt(_block: uint256) -> uint256: view

Getter for the total supply of veCRV (= total voting power) at block height _block.

InputTypeDescription
_blockuint256Block height

Returns: total voting power (uint256) at a specific block.

<>Source code▼
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block

epoch: public(uint256)
point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point

@external
@view
def totalSupplyAt(_block: uint256) -> uint256:
"""
@notice Calculate total voting power at some point in the past
@param _block Block to calculate the total voting power at
@return Total voting power at `_block`
"""
assert _block <= block.number
_epoch: uint256 = self.epoch
target_epoch: uint256 = self.find_block_epoch(_block, _epoch)

point: Point = self.point_history[target_epoch]
dt: uint256 = 0
if target_epoch < _epoch:
point_next: Point = self.point_history[target_epoch + 1]
if point.blk != point_next.blk:
dt = (_block - point.blk) * (point_next.ts - point.ts) / (point_next.blk - point.blk)
else:
if point.blk != block.number:
dt = (_block - point.blk) * (block.timestamp - point.ts) / (block.number - point.blk)
# Now dt contains info on how far are we beyond point

return self.supply_at(point, point.ts + dt)
▶Example▼

This example returns the total veCRV voting power at a specific block height.

supply​

VotingEscrow.supply() -> uint256: view

Getter for the total amount of CRV tokens locked in the contract.

Returns: locked CRV amount (uint256).

<>Source code▼
supply: public(uint256)
▶Example▼

This example returns the total amount of CRV tokens locked in the contract. The value is fetched live from the blockchain.

locked​

VotingEscrow.locked(arg0: address) -> amount: int128, end: uint256: view

Getter for the locked balance of address arg0. Returns the LockedBalance struct containing the locked amount and the unlock timestamp.

InputTypeDescription
arg0addressUser address

Returns: amount (int128) and unlock time (uint256).

<>Source code▼
struct LockedBalance:
amount: int128
end: uint256

locked: public(HashMap[address, LockedBalance])
▶Example▼

This example returns the locked CRV amount and unlock timestamp for a given address.

locked__end​

VotingEscrow.locked__end(_addr: address) -> uint256: view

Getter for the timestamp when _addr's lock finishes.

InputTypeDescription
_addraddressUser address

Returns: unlock timestamp (uint256).

<>Source code▼
struct LockedBalance:
amount: int128
end: uint256

locked: public(HashMap[address, LockedBalance])

@external
@view
def locked__end(_addr: address) -> uint256:
"""
@notice Get timestamp when `_addr`'s lock finishes
@param _addr User wallet
@return Epoch time of the lock end
"""
return self.locked[_addr].end
▶Example▼

This example returns the unlock timestamp for a given address.

get_last_user_slope​

VotingEscrow.get_last_user_slope(addr: address) -> int128: view

Getter for the most recently recorded rate of voting power decrease for addr.

InputTypeDescription
addraddressUser address

Returns: slope value (int128).

<>Source code▼
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block

user_point_epoch: public(HashMap[address, uint256])
user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch]

@external
@view
def get_last_user_slope(addr: address) -> int128:
"""
@notice Get the most recently recorded rate of voting power decrease for `addr`
@param addr Address of the user wallet
@return Value of the slope
"""
uepoch: uint256 = self.user_point_epoch[addr]
return self.user_point_history[addr][uepoch].slope
▶Example▼

This example returns the most recent rate of voting power decrease for a given address.


Checkpoints & History​

epoch​

VotingEscrow.epoch() -> uint256: view

Getter for the current global epoch. The epoch is incremented by 1 every time a checkpoint is recorded.

Returns: current epoch (uint256).

<>Source code▼
epoch: public(uint256)
▶Example▼

This example returns the current global epoch. The value is fetched live from the blockchain.

point_history​

VotingEscrow.point_history(arg0: uint256) -> bias: int128, slope: int128, ts: uint256, blk: uint256: view

Getter for the global point history at epoch arg0. Each point records the aggregate bias, slope, timestamp, and block number.

InputTypeDescription
arg0uint256Epoch number

Returns: bias (int128), slope (int128), ts (uint256) and blk (uint256).

<>Source code▼
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block

point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point
▶Example▼

This example returns the global point history at epoch 3 (bias, slope, timestamp, block number).

user_point_epoch​

VotingEscrow.user_point_epoch(arg0: address) -> uint256: view

Getter for the current checkpoint epoch for a specific user. This is incremented each time the user's lock state changes (create, increase amount, increase time, withdraw).

InputTypeDescription
arg0addressUser address

Returns: user epoch (uint256).

<>Source code▼
user_point_epoch: public(HashMap[address, uint256])
▶Example▼

This example returns the current checkpoint epoch for a specific user.

user_point_history​

VotingEscrow.user_point_history(arg0: address, arg1: uint256) -> bias: int128, slope: int128, ts: uint256, blk: uint256: view

Getter for a user's point history at a specific user epoch. Each point records the user's bias, slope, timestamp, and block number at that checkpoint.

InputTypeDescription
arg0addressUser address
arg1uint256User epoch number

Returns: bias (int128), slope (int128), ts (uint256) and blk (uint256).

<>Source code▼
user_point_history: public(HashMap[address, Point[1000000000]])  # user -> Point[user_epoch]
▶Example▼

This example returns the point history for a user at their first checkpoint (bias, slope, timestamp, block number).

user_point_history__ts​

VotingEscrow.user_point_history__ts(_addr: address, _idx: uint256) -> uint256: view

Convenience getter for the timestamp of checkpoint _idx for _addr.

InputTypeDescription
_addraddressUser address
_idxuint256User epoch number

Returns: timestamp (uint256).

<>Source code▼
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block

user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch]

@external
@view
def user_point_history__ts(_addr: address, _idx: uint256) -> uint256:
"""
@notice Get the timestamp for checkpoint `_idx` for `_addr`
@param _addr User wallet address
@param _idx User epoch number
@return Epoch time of the checkpoint
"""
return self.user_point_history[_addr][_idx].ts
▶Example▼

This example returns the timestamp of the first checkpoint for a given address.

slope_changes​

VotingEscrow.slope_changes(arg0: uint256) -> int128: view

Getter for scheduled slope changes at a future timestamp. When a lock expires, the global slope decreases. These changes are pre-scheduled so the contract doesn't need to iterate over all users.

InputTypeDescription
arg0uint256Timestamp (rounded to week)

Returns: signed slope change (int128).

<>Source code▼
slope_changes: public(HashMap[uint256, int128])  # time -> signed slope change
▶Example▼

This example returns the scheduled slope change at a specific future timestamp.


SmartWalletChecker​

The SmartWalletChecker is an external contract referenced by VotingEscrow to determine whether smart contracts are allowed to lock CRV. The internal assert_not_contract function checks callers against this contract whenever create_lock, increase_amount, or increase_unlock_time is called.

SmartWalletChecker Upgrade

The SmartWalletChecker was originally used to restrict which smart contracts could lock CRV — contracts needed to be explicitly whitelisted via an approveWallet function, and access could be revoked via revokeWallet. This was in place to prevent tokenizing the escrow.

The checker has since been upgraded to a simple Vyper contract that returns True for any input, effectively removing all smart contract restrictions. Any smart contract can now lock CRV without needing prior approval.

# pragma version 0.4.1
# @title Smart Wallet Whitelist
# @notice Dummy contract bypassing smart wallet check for veCRV.
# Returns 'true' for any input.

@view
@external
def check(_wallet: address) -> bool:
return True

smart_wallet_checker​

VotingEscrow.smart_wallet_checker() -> address: view

Getter for the current SmartWalletChecker contract address.

Returns: SmartWalletChecker contract (address).

<>Source code▼
smart_wallet_checker: public(address)
▶Example▼

This example returns the current SmartWalletChecker contract address.

future_smart_wallet_checker​

VotingEscrow.future_smart_wallet_checker() -> address: view

Getter for the future SmartWalletChecker contract address. Set via commit_smart_wallet_checker and applied via apply_smart_wallet_checker.

Returns: future SmartWalletChecker contract (address).

<>Source code▼
future_smart_wallet_checker: public(address)
▶Example▼

This example returns the future SmartWalletChecker contract address (zero address means no pending change).

commit_smart_wallet_checker​

VotingEscrow.commit_smart_wallet_checker(addr: address)
Guarded Method

This function is only callable by the admin of the contract.

Function to commit a new SmartWalletChecker contract address. Changes need to be applied via apply_smart_wallet_checker.

InputTypeDescription
addraddressNew SmartWalletChecker contract
<>Source code▼
future_smart_wallet_checker: public(address)

@external
def commit_smart_wallet_checker(addr: address):
"""
@notice Set an external contract to check for approved smart contract wallets
@param addr Address of Smart contract checker
"""
assert msg.sender == self.admin
self.future_smart_wallet_checker = addr
▶Example▼

This example commits a new SmartWalletChecker contract address.

>>> VotingEscrow.commit_smart_wallet_checker("0x1234567890abcdef1234567890abcdef12345678")

apply_smart_wallet_checker​

VotingEscrow.apply_smart_wallet_checker()
Guarded Method

This function is only callable by the admin of the contract.

Function to apply the previously committed SmartWalletChecker address.

<>Source code▼
smart_wallet_checker: public(address)
future_smart_wallet_checker: public(address)

@external
def apply_smart_wallet_checker():
"""
@notice Apply setting external contract to check approved smart contract wallets
"""
assert msg.sender == self.admin
self.smart_wallet_checker = self.future_smart_wallet_checker
▶Example▼

This example applies the previously committed SmartWalletChecker address.

>>> VotingEscrow.apply_smart_wallet_checker()

Contract Info​

token​

VotingEscrow.token() -> address: view

Getter for the CRV token address that is locked in the contract.

Returns: CRV token address (address).

<>Source code▼
token: public(address)

@external
def __init__(token_addr: address, _name: String[64], _symbol: String[32], _version: String[32]):
...
self.token = token_addr
...
▶Example▼

This example returns the CRV token address locked in the contract.

name​

VotingEscrow.name() -> String[64]: view

Getter for the name of the token.

Returns: name (String[64]).

<>Source code▼
name: public(String[64])
▶Example▼

This example returns the name of the token.

symbol​

VotingEscrow.symbol() -> String[32]: view

Getter for the symbol of the token.

Returns: symbol (String[32]).

<>Source code▼
symbol: public(String[32])
▶Example▼

This example returns the symbol of the token.

version​

VotingEscrow.version() -> String[32]: view

Getter for the version of the contract.

Returns: version (String[32]).

<>Source code▼
version: public(String[32])
▶Example▼

This example returns the version of the contract.

decimals​

VotingEscrow.decimals() -> uint256: view

Getter for the decimals of the token.

Returns: decimals (uint256).

<>Source code▼
decimals: public(uint256)
▶Example▼

This example returns the number of decimals of the token.

controller​

VotingEscrow.controller() -> address: view

Getter for the Aragon controller of the contract.

Returns: controller address (address).

<>Source code▼
controller: public(address)
▶Example▼

This example returns the Aragon controller address.

changeController​

VotingEscrow.changeController(_newController: address)
Guarded Method

This function is only callable by the controller of the contract.

Dummy method required for Aragon compatibility.

InputTypeDescription
_newControlleraddressNew controller address
<>Source code▼
controller: public(address)

@external
def changeController(_newController: address):
"""
@dev Dummy method required for Aragon compatibility
"""
assert msg.sender == self.controller
self.controller = _newController
▶Example▼

This example changes the Aragon controller address.

>>> VotingEscrow.changeController("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")

transfersEnabled​

VotingEscrow.transfersEnabled() -> bool: view

View method required for Aragon compatibility. Always returns True.

Returns: transfers enabled (bool).

<>Source code▼
transfersEnabled: public(bool)

@external
def __init__(token_addr: address, _name: String[64], _symbol: String[32], _version: String[32]):
...
self.transfersEnabled = True
...
▶Example▼

This example returns whether transfers are enabled (always True for Aragon compatibility).


Admin Controls​

The CurveOwnershipAgent (0x40907540d8a6C65c637785e8f8B742ae6b0b9968) is the current admin of the VotingEscrow. Any changes to admin-controlled parameters require a successfully passed DAO vote.

admin​

VotingEscrow.admin() -> address: view

Getter for the current admin of the contract.

Returns: admin (address).

<>Source code▼
admin: public(address)  # Can and will be a smart contract
▶Example▼

This example returns the current admin of the contract (CurveOwnershipAgent).

future_admin​

VotingEscrow.future_admin() -> address: view

Getter for the future admin of the contract. Set via commit_transfer_ownership and applied via apply_transfer_ownership.

Returns: future admin (address).

<>Source code▼
future_admin: public(address)
▶Example▼

This example returns the future admin address (zero address means no pending transfer).

commit_transfer_ownership​

VotingEscrow.commit_transfer_ownership(addr: address)
Guarded Method

This function is only callable by the admin of the contract.

Function to commit the transfer of ownership to addr. Changes need to be applied via apply_transfer_ownership.

InputTypeDescription
addraddressNew admin address

Emits: CommitOwnership event.

<>Source code▼
event CommitOwnership:
admin: address

future_admin: public(address)

@external
def commit_transfer_ownership(addr: address):
"""
@notice Transfer ownership of VotingEscrow contract to `addr`
@param addr Address to have ownership transferred to
"""
assert msg.sender == self.admin # dev: admin only
self.future_admin = addr
log CommitOwnership(addr)
▶Example▼

This example commits a new admin address for the ownership transfer.

>>> VotingEscrow.commit_transfer_ownership("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")

apply_transfer_ownership​

VotingEscrow.apply_transfer_ownership()
Guarded Method

This function is only callable by the admin of the contract.

Function to apply the previously committed ownership transfer.

Emits: ApplyOwnership event.

<>Source code▼
event ApplyOwnership:
admin: address

admin: public(address) # Can and will be a smart contract
future_admin: public(address)

@external
def apply_transfer_ownership():
"""
@notice Apply ownership transfer
"""
assert msg.sender == self.admin # dev: admin only
_admin: address = self.future_admin
assert _admin != ZERO_ADDRESS # dev: admin not set
self.admin = _admin
log ApplyOwnership(_admin)
▶Example▼

This example applies the previously committed ownership transfer.

>>> VotingEscrow.apply_transfer_ownership()