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.vyThe 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.
| CRV | veCRV | Locktime |
|---|---|---|
| 1 | 1 | 4 years |
| 1 | 0.75 | 3 years |
| 1 | 0.5 | 2 years |
| 1 | 0.25 | 1 year |
| x | x * n/4 | n |
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 is linearly decreasing since the moment of lock. So does the total voting power . 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 in the public mapping user_point_history. We also change slope and bias for the total voting power and record it in point_history. In addition, when a user's lock is scheduled to end, we schedule change of slopes of 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 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.
Emits: Deposit and Supply
| Input | Type | Description |
|---|---|---|
_value | uint256 | Amount of CRV to deposit |
_unlock_time | uint256 | Timestamp of the unlock time |
<>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.
Emits: Deposit and Supply
| Input | Type | Description |
|---|---|---|
_value | uint256 | Amount of CRV to additionally lock |
<>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.
Emits: Deposit and Supply
| Input | Type | Description |
|---|---|---|
_unlock_time | uint256 | New unlock timestamp |
<>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.
Emits: Deposit and Supply
| Input | Type | Description |
|---|---|---|
_addr | address | Address to deposit for |
_value | uint256 | Amount of tokens to lock |
<>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
<>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: viewGetter for the current veCRV balance (= voting power) of addr at timestamp _t. Defaults to block.timestamp.
These are not real ERC-20 balances. They measure voting weights that decay linearly over time.
Returns: voting power (uint256).
| Input | Type | Description |
|---|---|---|
addr | address | User wallet address |
_t | uint256 | Timestamp; defaults to block.timestamp |
<>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: viewGetter for the veCRV balance (= voting power) of addr at block height _block.
Returns: voting power (uint256) at a specific block.
| Input | Type | Description |
|---|---|---|
addr | address | User wallet address |
_block | uint256 | Block height |
<>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.
>>> VotingEscrow.balanceOfAt("0x7a16fF8270133F063aAb6C9977183D9e72835428", 18483472)
27109584974408936745457887
totalSupply
VotingEscrow.totalSupply(t: uint256 = block.timestamp) -> uint256: viewGetter for the current total supply of veCRV (= total voting power) at timestamp t. Defaults to block.timestamp.
Returns: total voting power (uint256).
| Input | Type | Description |
|---|---|---|
t | uint256 | Timestamp; defaults to block.timestamp |
<>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: viewGetter for the total supply of veCRV (= total voting power) at block height _block.
Returns: total voting power (uint256) at a specific block.
| Input | Type | Description |
|---|---|---|
_block | uint256 | Block height |
<>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.
>>> VotingEscrow.totalSupplyAt(18483472)
652219245965489504779222536
supply
VotingEscrow.supply() -> uint256: viewGetter 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: viewGetter for the locked balance of address arg0. Returns the LockedBalance struct containing the locked amount and the unlock timestamp.
Returns: amount (int128) and unlock time (uint256).
| Input | Type | Description |
|---|---|---|
arg0 | address | User address |
<>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.
>>> VotingEscrow.locked("0x7a16fF8270133F063aAb6C9977183D9e72835428")
27191329036660104386777000, 1808956800
locked__end
VotingEscrow.locked__end(_addr: address) -> uint256: viewGetter for the timestamp when _addr's lock finishes.
Returns: unlock timestamp (uint256).
| Input | Type | Description |
|---|---|---|
_addr | address | User address |
<>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.
>>> VotingEscrow.locked__end("0x7a16fF8270133F063aAb6C9977183D9e72835428")
1808956800
get_last_user_slope
VotingEscrow.get_last_user_slope(addr: address) -> int128: viewGetter for the most recently recorded rate of voting power decrease for addr.
Returns: slope value (int128).
| Input | Type | Description |
|---|---|---|
addr | address | User address |
<>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.
>>> VotingEscrow.get_last_user_slope("0x7a16fF8270133F063aAb6C9977183D9e72835428")
215557846878647453
Checkpoints & History
epoch
VotingEscrow.epoch() -> uint256: viewGetter 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: viewGetter for the global point history at epoch arg0. Each point records the aggregate bias, slope, timestamp, and block number.
Returns: bias (int128), slope (int128), ts (uint256) and blk (uint256).
| Input | Type | Description |
|---|---|---|
arg0 | uint256 | Epoch number |
<>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).
>>> VotingEscrow.point_history(3)
127357905207521710167, 4570173769659, 1597370987, 10655341
user_point_epoch
VotingEscrow.user_point_epoch(arg0: address) -> uint256: viewGetter 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).
Returns: user epoch (uint256).
| Input | Type | Description |
|---|---|---|
arg0 | address | User address |
<>Source code▼
user_point_epoch: public(HashMap[address, uint256])
▶Example▼
This example returns the current checkpoint epoch for a specific user.
>>> VotingEscrow.user_point_epoch("0x7a16fF8270133F063aAb6C9977183D9e72835428")
5
user_point_history
VotingEscrow.user_point_history(arg0: address, arg1: uint256) -> bias: int128, slope: int128, ts: uint256, blk: uint256: viewGetter 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.
Returns: bias (int128), slope (int128), ts (uint256) and blk (uint256).
| Input | Type | Description |
|---|---|---|
arg0 | address | User address |
arg1 | uint256 | User epoch number |
<>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).
>>> VotingEscrow.user_point_history("0x7a16fF8270133F063aAb6C9977183D9e72835428", 1)
127357905207521710167, 4570173769659, 1597565455, 10655341
user_point_history__ts
VotingEscrow.user_point_history__ts(_addr: address, _idx: uint256) -> uint256: viewConvenience getter for the timestamp of checkpoint _idx for _addr.
Returns: timestamp (uint256).
| Input | Type | Description |
|---|---|---|
_addr | address | User address |
_idx | uint256 | User epoch number |
<>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.
>>> VotingEscrow.user_point_history__ts("0x7a16fF8270133F063aAb6C9977183D9e72835428", 1)
1597565455
slope_changes
VotingEscrow.slope_changes(arg0: uint256) -> int128: viewGetter 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.
Returns: signed slope change (int128).
| Input | Type | Description |
|---|---|---|
arg0 | uint256 | Timestamp (rounded to week) |
<>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.
>>> VotingEscrow.slope_changes(1808956800)
-215557846878647453
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.
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: viewGetter 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.
>>> VotingEscrow.smart_wallet_checker()
'0xca719728Ef172d0961768581fdF35CB116e0B7a4'
future_smart_wallet_checker
VotingEscrow.future_smart_wallet_checker() -> address: viewGetter 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).
>>> VotingEscrow.future_smart_wallet_checker()
'0x0000000000000000000000000000000000000000'
commit_smart_wallet_checker
VotingEscrow.commit_smart_wallet_checker(addr: address)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.
| Input | Type | Description |
|---|---|---|
addr | address | New 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()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: viewGetter 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.
>>> VotingEscrow.token()
'0xD533a949740bb3306d119CC777fa900bA034cd52'
name
VotingEscrow.name() -> String[64]: viewGetter 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.
>>> VotingEscrow.name()
'Vote-escrowed CRV'
symbol
VotingEscrow.symbol() -> String[32]: viewGetter 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.
>>> VotingEscrow.symbol()
'veCRV'
version
VotingEscrow.version() -> String[32]: viewGetter 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.
>>> VotingEscrow.version()
'veCRV_1.0.0'
decimals
VotingEscrow.decimals() -> uint256: viewGetter 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.
>>> VotingEscrow.decimals()
18
controller
VotingEscrow.controller() -> address: viewGetter for the Aragon controller of the contract.
Returns: controller address (address).
<>Source code▼
controller: public(address)
▶Example▼
This example returns the Aragon controller address.
>>> VotingEscrow.controller()
'0xc4AD0Ef33A0A4ddA3461c479ccb6c36d1e4B7Be4'
changeController
VotingEscrow.changeController(_newController: address)This function is only callable by the controller of the contract.
Dummy method required for Aragon compatibility.
| Input | Type | Description |
|---|---|---|
_newController | address | New 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: viewView 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).
>>> VotingEscrow.transfersEnabled()
True
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: viewGetter 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).
>>> VotingEscrow.admin()
'0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
future_admin
VotingEscrow.future_admin() -> address: viewGetter 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).
>>> VotingEscrow.future_admin()
'0x0000000000000000000000000000000000000000'
commit_transfer_ownership
VotingEscrow.commit_transfer_ownership(addr: address)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.
Emits: CommitOwnership
| Input | Type | Description |
|---|---|---|
addr | address | New admin address |
<>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()This function is only callable by the admin of the contract.
Function to apply the previously committed ownership transfer.
Emits: ApplyOwnership
<>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()